20б. USB-C и UCPD: питание и роли по кабелю
Раздел 20 разобрал USB-устройство: линии D+ и D−, тактовая 48 МГц, подтяжка, классы. Раздел 20а — хост и OTG. У разъёма Type-C есть ещё две линии, CC1 и CC2, и данные по ним не идут. По ним источник и потребитель узнают друг друга, выясняют, какой стороной вставлен кабель и сколько тока можно брать. Поверх той же линии идёт протокол Power Delivery: по нему договариваются о напряжении выше 5 В. У G0 и G4 для этого есть блок UCPD. Здесь — резисторы на CC, «мёртвая батарея» и её выводы, кадр Power Delivery (модель на ПК), делители UCPD и код потребителя по регистрам.
Опоры: справочные руководства RM0440 (G4, глава 46 UCPD) и RM0444 (G0,
глава 38), даташиты STM32G474 (DS12288) и STM32G071 (DS12232) из паков
Keil, драйвер LL UCPD из пака G4, пример USBPD_Analyzer для платы
STM32G071B-DISCO и библиотека STM32_USBPD_Library из пакета
STM32Cube_FW_G0 V1.6.0. Самих стандартов USB Type-C и Power Delivery на
диске нет, поэтому номиналов Rp и порогов напряжения на CC здесь нет:
в документах ST их не приводят.
Как это называется в документации
Оригинал Перевод CC, configuration channel линия конфигурации: CC1 и CC2 в разъёме Rp, Rd, Ra подтяжка источника к питанию; подтяжка потребителя к земле; нагрузка кабеля с питанием VCONN source, sink, DRP источник, потребитель, порт с двумя ролями dead battery мёртвая батарея: Rd есть и у обесточенного кристалла VCONN питание электроники кабеля по свободной линии CC USB Power Delivery (PD) протокол договора о напряжении и токе по линии CC BMC, bi-phase mark coding двухфазное кодирование: переход на каждой границе бита 4b5b, K-code код «4 бита → 5 бит»; K-код — служебный пятибитный символ ordered set, SOP, EOP упорядоченный набор из четырёх K-кодов; начало и конец пакета PDO, Source_Capabilities объект данных питания; сообщение источника «что я умею» hbit_clk тактовая половины бита tTransitionWindow, tInterFrameGap окно проверки тишины на линии; пауза между кадрами
20б.1 Линии CC: кто источник, кто потребитель
В разъёме Type-C две линии конфигурации, CC1 и CC2, а в кабеле провод CC один. На какую из двух линий он придёт, зависит от того, какой стороной вставили вилку. Источник подтягивает обе свои линии к питанию резисторами Rp, потребитель стягивает обе свои к земле резисторами Rd. Через кабель Rp и Rd встречаются только на одной линии, и получается делитель:
- источник видит Rd на одной из линий — потребитель подключён, а номер линии говорит об ориентации кабеля;
- потребитель видит напряжение делителя, и по его уровню узнаёт, какой ток источник разрешает: ток по умолчанию, 1,5 А или 3 А. Какой именно — источник выбирает номиналом Rp;
- провод CC один, поэтому вторая линия потребителя свободна. Кабель со своей электроникой вешает на неё Ra и просит питание VCONN.
Встроенный Rd у UCPD — 5,1 кОм (DS12288, примечание к таблице выводов;
RM0440, раздел про выводы отладки). Rp у UCPD тоже встроены, «всех
номиналов», и выбираются полем ANASUBMODE. Роль задаёт бит ANAMODE
регистра UCPD_CR: 0 — источник, 1 — потребитель. Уровень на каждой
линии после фильтра дребезга UCPD кладёт в поля TYPEC_VSTATE_CC1 и
TYPEC_VSTATE_CC2 регистра UCPD_SR. Что значат их значения, зависит
от роли (таблица 428 RM0440, таблица 237 RM0444):
Роль, ANASUBMODE |
0 | 1 | 2 | 3 |
|---|---|---|---|---|
| источник, 01 — ток по умолчанию | vRa | vRd | vOPEN | — |
| источник, 10 — 1,5 А | vRa | vRd | vOPEN | — |
| источник, 11 — 3 А | vRa | vRd | vOPEN | — |
| потребитель | vRa | vRd-USB | vRd-1.5 | vRd-3.0 |
Источнику TYPEC_VSTATE = 1 говорит «потребитель есть», 0 — «кабель с
Ra», 2 — «пусто». Потребителю 1, 2 и 3 — ток по умолчанию, 1,5 А и 3 А,
а 0 — источника нет; в LL это значение называется
LL_UCPD_SNK_CCx_VOPEN. Чтобы поменять разрешённый ток на ходу,
источник просто перезаписывает ANASUBMODE, а у потребителя
срабатывает событие TYPECEVT и меняется TYPEC_VSTATE.
Потребителю, которому хватает 5 В и тока по умолчанию, UCPD не обязателен: источнику нужен только Rd на CC. UCPD нужен, когда надо знать разрешённый ток, ориентацию кабеля или договориться по Power Delivery.
UCPD не управляет ни ключом VBUS, ни ключом VCONN (примечание к таблице
428): у источника ими командует программа по состоянию CC. Пример в
таблице 429 RM0440: источник 3 А увидел Rd на CC1, ждёт в состоянии
AttachWait 100–200 мс, увидел Ra на CC2 — подаёт туда VCONN внешним
ключом и отключает CC2 от своего блока полем CCENABLE.
20б.2 Мёртвая батарея: какие выводы соединить
Прибор с разряженным аккумулятором питается только от USB-C. Но источник не подаст VBUS, пока не увидит Rd, а микроконтроллер без питания Rd не включит. Поэтому у UCPD есть Rd «мёртвой батареи»: он подключён к CC, пока кристалл обесточен, если на вывод DBCC приходит высокий уровень. Для этого на плате DBCC1 соединяют с CC1, DBCC2 — с CC2. Прибору, которому это не нужно (только источник), RM велит посадить оба вывода DBCC на землю.
| STM32G474, UCPD1 | STM32G071, UCPD1 | |
|---|---|---|
| CC1 | PB6 | PA8 |
| CC2 | PB4 (он же NJTRST) | PB15 |
| DBCC1 | PA9 | PA9 |
| DBCC2 | PA10 | PA10 |
| снять Rd мёртвой батареи | PWR_CR3.UCPD1_DBDIS (бит 14) |
строб SYSCFG_CFGR1.UCPD1_STROBE (бит 9) |
У G071 в части малых корпусов роль DBCC у PC6 и PB0, и второй блок UCPD2 на PD0–PD3 (таблица 12 DS12232). Если DBCC соединены с CC, то PA9 и PA10 висят на линиях CC, и USART1 или I2C1 на них уже не посадить.
После сброса Rd мёртвой батареи включён сам — по уровню на DBCC. Отключают его так:
- G4. Сначала настроить UCPD (
ANAMODE,ANASUBMODE), потом поставитьUCPD1_DBDISвPWR_CR3: тогда Rd мёртвой батареи уходит и начинает работать то, что настроено. RM0440 советует ставить этот бит всегда — и когда UCPD не нужен, и когда нужен. - G0. Сначала настроить UCPD, потом записать 1 в
UCPD1_STROBE: строб применяет настройку к выводам. Если UCPD выключен, тот же строб просто снимает подтяжку. Программа, которой UCPD не нужен, делает строб при старте (примечание 3 к таблице выводов DS12232).
В Standby UCPD не работает, но настроенные подтяжки остаются; без питания остаётся Rd мёртвой батареи (таблица 430 RM0440). Питание самого UCPD по даташитам: только потребитель — 3,0–3,6 В, источник и потребитель — 3,135–3,465 В.
20б.3 Кадр Power Delivery: что добавляет UCPD
Сообщение Power Delivery идёт по той же линии CC, на которой провод кабеля. Кадр (рисунок 677 RM0440):
| Часть | Кто даёт | 4b5b |
|---|---|---|
| преамбула — разгон приёмника | UCPD | нет |
| SOP — четыре K-кода | UCPD, из UCPD_TX_ORDSET |
да |
| заголовок, 2 байта | программа | да |
| объекты данных по 4 байта | программа | да |
| CRC-32 | UCPD | да |
| EOP — один K-код | UCPD | да |
Каждые 4 бита данных заменяются пятибитным кодом (таблица 424):
| 4 бита | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|---|
| 5 бит | 11110 | 01001 | 10100 | 10101 | 01010 | 01011 | 01110 | 01111 |
| 4 бита | 8 | 9 | A | B | C | D | E | F |
| 5 бит | 10010 | 10011 | 10110 | 10111 | 11010 | 11011 | 11100 | 11101 |
Служебные K-коды: Sync-1 — 11000, Sync-2 — 10001, Sync-3 — 00110, RST-1 — 00111, RST-2 — 11001, EOP — 01101. Пять бит кода записаны от b4 к b0, а на линию первым уходит b0; у байта первой идёт младшая тетрада (рисунки 675 и 676).
Четыре K-кода составляют упорядоченный набор (таблица 425):
| Набор | K-код 1 | K-код 2 | K-код 3 | K-код 4 |
|---|---|---|---|---|
| SOP | Sync-1 | Sync-1 | Sync-1 | Sync-2 |
| SOP’ | Sync-1 | Sync-1 | Sync-3 | Sync-3 |
| SOP’’ | Sync-1 | Sync-3 | Sync-1 | Sync-3 |
| Hard Reset | RST-1 | RST-1 | RST-1 | RST-2 |
| Cable Reset | RST-1 | Sync-1 | RST-1 | Sync-3 |
Какие наборы ловить, приёмнику задают биты RXORDSETEN в UCPD_CFG1
(бит 0 — SOP). Hard Reset прерывает даже идущую передачу: UCPD
обрывает её кодом EOP и шлёт сброс (бит TXHRST). Приёмник принимает
набор, если верны любые три K-кода из четырёх.
CRC-32 — тот же многочлен 0x04C11DB7, что у блока CRC (RM0440, глава
CRC). Приёмник прогоняет через регистр CRC и данные, и сам CRC и в конце
сравнивает регистр с постоянным остатком: совпал — RXMSGEND, нет —
ещё и RXERR.
Поверх 4b5b биты идут кодом BMC: на каждой границе бита уровень меняется, у единицы — ещё и в середине. Такой сигнал не зависит от полярности и почти не несёт постоянной составляющей, а частоту приёмник восстанавливает по преамбуле.
20б.4 Модель на ПК: Source_Capabilities по битам
Модель _verify/ucpd20b/ucpd20b.py делает то, что UCPD делает
аппаратно: собирает сообщение, считает CRC, кодирует 4b5b и BMC, потом
декодирует обратно. Сообщение — Source_Capabilities с одним объектом:
фиксированные 5 В и 3 А.
- Заголовок 0x11A1: тип сообщения 1 (Source_Capabilities), роль данных
DFP, ревизия 3.0, роль питания — источник, номер сообщения 0, один
объект данных. Раскладка полей — из
demo_disco.c(USBPD_Analyzer), значения — изusbpd_def.h. - Объект 0x0001912C: тип «фиксированное питание», напряжение 100 × 50 мВ,
ток 300 × 10 мА (поля
USBPD_PDO_SRC_FIXED_*вusbpd_def.h). - В передатчик программа пишет 6 байт:
A1 11 2C 91 01 00, это и естьTX_PAYSZ= 6.
CRC этих шести байт — 0x312C2EDA; zlib.crc32 и сдвиговый регистр с
многочленом 0x04C11DB7 дают одно и то же. После данных и CRC в регистре
остаётся 0xC704DD7B — и для этого сообщения, и для трёх других. RM0440 и
RM0444 печатают 0xC704DD78: отличие в двух младших битах. zlib по тому
же кадру оставляет 0xDEBB20E3, а это 0xC704DD7B с битами в обратном
порядке. Похоже на опечатку в RM. Программе остаток не нужен: его
сверяет UCPD и сообщает результат флагом RXERR.
Кадр после преамбулы: SOP 20 бит + данные 60 + CRC 40 + EOP 5 = 125 бит.
Из 5 бит кода полезных 4 — 80 %. При скорости 285,7 кбит/с (делители
по умолчанию, 20б.5) кадр без преамбулы идёт 437,5 мкс. Первые 20 бит на
линии, SOP: 00011000110001110001.
Кодер BMC в модели:
def bmc(bits, level=0): # переход на каждой границе бита, для 1 — ещё в середине
out = []
for b in bits:
level ^= 1
out.append(level)
if b:
level ^= 1
out.append(level)
return out
Из 125 бит получается 250 полубитов. Единиц 66, нулей 59, переходов 191 = 125 границ + 66 середин; сумма полубитов (+1 и −1) — 2, то есть постоянной составляющей почти нет. Обратный разбор — BMC, 5 бит, 4 бита — находит SOP и EOP, заголовок 0x11A1, объект 0x0001912C и тот же остаток CRC. Инвертированный сигнал декодируется так же. Один испорченный бит в объекте даёт остаток 0x0F202F54 — сообщение отброшено. SOP с одним испорченным K-кодом принят, с двумя — нет.
20б.5 Делители: 300 кбит/с из HSI16
UCPD тактируется от HSI16 (ucpd_ker_ck) и делит его трижды
(рисунок 674 RM0440):
PSC_USBPDCLK— предделитель /1, /2, /4, /8, /16 →ucpd_clk. RM советует держатьucpd_clkв пределах 6–9 МГц;HBITCLKDIV— делительucpd_clkна значение поля плюс один (1…64) →hbit_clk, тактовая половины бита, около 600 кГц; бит — два полубита, отсюда около 300 кбит/с;TRANSWIN— окно проверки тишины перед передачей, в полубитах (поле плюс один, рекомендовано 0x09 — 10); окно должно быть 12–20 мкс. Линия тихая, если в окне меньше трёх переходов;IFRGAP— делительucpd_clkдля паузы между кадрами (поле плюс один); по RM, 15 — для номинальной частоты.
Все четыре поля — в UCPD_CFG1, и пишутся они только при UCPDEN = 0.
Что даёт каждый предделитель:
PSC_USBPDCLK |
ucpd_clk |
в 6–9 МГц | делитель, ближайший к 600 кГц | hbit_clk |
бит |
|---|---|---|---|---|---|
| /1 | 16,0 МГц | нет | /27 | 592,6 кГц | 296,3 кбит/с |
| /2 | 8,0 МГц | да | /13 | 615,4 кГц | 307,7 кбит/с |
| /4 | 4,0 МГц | нет | /7 | 571,4 кГц | 285,7 кбит/с |
| /8 | 2,0 МГц | нет | /3 | 666,7 кГц | 333,3 кбит/с |
| /16 | 1,0 МГц | нет | /2 | 500,0 кГц | 250,0 кбит/с |
В рекомендованную полосу попадает только /2. LL_UCPD_StructInit так и
делает: /2 → 8,0 МГц, HBITCLKDIV = 0x0D (/14) → 571,4 кГц, бит
285,7 кбит/с, полубит 1,750 мкс, бит 3,500 мкс. TRANSWIN = 0x7 — 8
полубитов, 14,0 мкс; рекомендованное RM 0x09 дало бы 17,5 мкс; оба в
пределах 12–20 мкс. IFRGAP = 0x10 — деление на 17. В регистр уходит
UCPD_CFG1 = 0x23C0D, с разрешённым приёмом SOP (RXORDSETEN, бит 0) —
0x123C0D. Пример RM HBITCLKDIV = 0x1A (/27) рассчитан на ucpd_clk
16 МГц без предделителя: 592,6 кГц и 296,3 кбит/с. /13 при 8 МГц дало
бы 615,4 кГц — ближе к 600, но выше; допуск скорости задаёт стандарт PD,
его на диске нет, поэтому оценки «годится или нет» здесь не будет.
Пример USBPD_Analyzer платы STM32G071B-DISCO и библиотека
STM32_USBPD_Library берут те же LL_UCPD_StructInit.
20б.6 Код: потребитель по регистрам
Файл _verify/ucpd20b/ucpd20b.c собирается armclang под STM32G474 и
STM32G071. Настройка потребителя:
void ucpd_sink_init(void)
{
#if defined(STM32G474xx)
RCC->AHB2ENR |= RCC_AHB2ENR_GPIOBEN;
RCC->APB1ENR1 |= RCC_APB1ENR1_PWREN;
RCC->APB1ENR2 |= RCC_APB1ENR2_UCPD1EN;
GPIOB->PUPDR &= ~GPIO_PUPDR_PUPD4; /* PB4 после сброса — NJTRST с подтяжкой */
GPIOB->MODER |= GPIO_MODER_MODE4 | GPIO_MODER_MODE6; /* CC2 = PB4, CC1 = PB6 — аналоговый режим */
#elif defined(STM32G071xx)
RCC->IOPENR |= RCC_IOPENR_GPIOAEN | RCC_IOPENR_GPIOBEN;
RCC->APBENR2 |= RCC_APBENR2_SYSCFGEN;
RCC->APBENR1 |= RCC_APBENR1_UCPD1EN;
GPIOA->MODER |= GPIO_MODER_MODE8; /* CC1 = PA8 */
GPIOB->MODER |= GPIO_MODER_MODE15; /* CC2 = PB15 */
#endif
UCPD1->CFG1 = UCPD_CFG1_DIVS | UCPD_CFG1_RXORDSETEN_0; /* пишется только при UCPDEN = 0; приём SOP */
UCPD1->CFG1 |= UCPD_CFG1_UCPDEN;
UCPD1->CR = UCPD_CR_ANAMODE | UCPD_CR_CCENABLE; /* пишется только при UCPDEN = 1 */
#if defined(STM32G474xx)
PWR->CR3 |= PWR_CR3_UCPD_DBDIS; /* Rd мёртвой батареи — долой, дальше Rd от UCPD */
#elif defined(STM32G071xx)
SYSCFG->CFGR1 |= SYSCFG_CFGR1_UCPD1_STROBE; /* строб: применить ANAMODE к выводам */
#endif
}
Порядок здесь не случаен: UCPD_CFG1 пишется только при выключенном
блоке, UCPD_CR — только при включённом, а Rd мёртвой батареи снимают
после того, как задана роль. Выключение блока (UCPDEN = 0) сбрасывает
все управляющие биты — после него настройку повторяют. _Static_assert
в файле сверяют с заголовками CMSIS адрес UCPD1 (0x4000A000), значение
UCPD_CFG1 = 0x23C0D, положение полей TYPEC_VSTATE (биты 16 и 18),
ANAMODE | CCENABLE = 0xE00 и биты UCPD1_DBDIS и UCPD1_STROBE.
Опрос линий:
int ucpd_sink_poll(unsigned *cc2)
{
uint32_t sr = UCPD1->SR;
uint32_t v1 = (sr & UCPD_SR_TYPEC_VSTATE_CC1) >> UCPD_SR_TYPEC_VSTATE_CC1_Pos;
uint32_t v2 = (sr & UCPD_SR_TYPEC_VSTATE_CC2) >> UCPD_SR_TYPEC_VSTATE_CC2_Pos;
if (v1 != CC_OPEN && v2 == CC_OPEN) { *cc2 = 0; return (int)v1; }
if (v1 == CC_OPEN && v2 != CC_OPEN) { *cc2 = 1; return (int)v2; }
return CC_OPEN; /* обе открыты — никого; обе заняты — не кабель */
}
Для G474 armclang с -O2 делает из неё 18 команд — одно чтение
UCPD1->SR по адресу 0x4000 A014 и два извлечения поля UBFX:
MOV r1,#0xa014
MOVT r1,#0x4000
LDR r2,[r1,#0]
UBFX r1,r2,#16,#2
UBFX r2,r2,#18,#2
CBZ r1,{pc}+0x8 ; 0x1a
CBNZ r2,{pc}+0x6 ; 0x1a
MOVS r3,#0
B {pc}+0x10 ; 0x28
CMP r1,#0
MOV r1,#0
BNE {pc}+0xa ; 0x2a
CBZ r2,{pc}+0x8 ; 0x2a
MOVS r3,#1
MOV r1,r2
STR r3,[r0,#0]
MOV r0,r1
BX lr
Нашли источник — ucpd_sink_attach ставит PHYCCSEL на его линию и
включает приёмник PHYRXEN. Устойчивость подключения (сколько держать
уровень, прежде чем поверить) задаёт стандарт Type-C; UCPD сам
отфильтровывает только дребезг и всплески. Отправка сообщения:
int ucpd_send(const uint8_t *msg, unsigned len)
{
unsigned i = 0;
uint32_t sr;
UCPD1->TX_ORDSET = PD_SOP;
UCPD1->TX_PAYSZ = len; /* с заголовком, без CRC */
UCPD1->CR |= UCPD_CR_TXSEND; /* TXMODE = 0: обычное сообщение */
do {
sr = UCPD1->SR;
if ((sr & UCPD_SR_TXIS) && i < len)
UCPD1->TXDR = msg[i++]; /* TXIS: регистр пуст, следующий байт */
} while (!(sr & (UCPD_SR_TXMSGSENT | UCPD_SR_TXMSGDISC)));
UCPD1->ICR = UCPD_ICR_TXMSGSENTCF | UCPD_ICR_TXMSGDISCCF;
return (sr & UCPD_SR_TXMSGSENT) ? 0 : -1;
}
PD_SOP = 0x8E318 — четыре K-кода по 5 бит, первый в младших битах,
как велит описание TX_ORDSET; то же значение даёт
LL_UCPD_ORDERED_SET_SOP. Для сообщения из 20б.4 len = 6. UCPD сам
добавит преамбулу, CRC и EOP. TXMSGDISC означает, что передача
отброшена: на линии идёт приём или сильная помеха. Это только
физический уровень. Протокол Power Delivery — подтверждения, таймеры,
запрос напряжения — делает STM32_USBPD_Library.
20б.7 Грабли
На G474 не цепляется JTAG, на PB4 — чужой уровень. PB4 — это и
NJTRST, и CC2. После сброса на нём может висеть Rd мёртвой батареи
5,1 кОм — его включает высокий уровень на PA10 (DBCC2). Для JTAG RM0440
требует поставить UCPD1_DBDIS.
G071: PA8 или PB15 тянет к земле, хотя UCPD не используется. Rd
мёртвой батареи включён после сброса. Нужен строб UCPD1_STROBE при
старте.
Настройка UCPD не действует. UCPD_CFG1 пишется только при
UCPDEN = 0, UCPD_CR — только при UCPDEN = 1. Запись не в то время
молча пропадает.
Роль задана, а на CC всё ещё Rd. На G4 пока не поставлен
UCPD1_DBDIS, на G0 пока не было строба, работает Rd мёртвой батареи,
а не настройка.
Без питания прибор не заряжается от USB-C. DBCC1 и DBCC2 не соединены с CC1 и CC2 — Rd мёртвой батареи нет, источник не видит потребителя и не даёт VBUS.
Источник выдаёт VBUS, когда никто не подключён. UCPD ключом VBUS не управляет. Включать его программе — только по vRd на одной из линий.
UCPD на 3,0 В не работает источником. Источнику и потребителю сразу нужно 3,135–3,465 В; от 3,0 В — только потребитель.
Своя проверка CRC с числом из RM не сходится. RM печатает остаток
0xC704DD78, модель и zlib дают 0xC704DD7B. Проверять CRC вручную не
нужно — это делает UCPD и ставит RXERR.
20б.8 Итоги
- Источник тянет CC вверх через Rp, потребитель вниз через Rd
(5,1 кОм у UCPD); по линии с Rd видна ориентация, по уровню — ток:
TYPEC_VSTATE1, 2, 3 — ток по умолчанию, 1,5 А и 3 А. - Мёртвая батарея: DBCC1 → CC1, DBCC2 → CC2 на плате; снимают после
настройки роли —
PWR_CR3.UCPD1_DBDIS(G4) или стробSYSCFG_CFGR1.UCPD1_STROBE(G0). На G474 CC2 — это PB4/NJTRST. - Кадр: преамбула, SOP (4 K-кода, 20 бит), заголовок и данные в 4b5b, CRC-32 с многочленом 0x04C11DB7, EOP; поверх — BMC.
- Source_Capabilities 5 В 3 А: 6 байт, 125 бит, 250 полубитов, 437,5 мкс; CRC 0x312C2EDA, остаток 0xC704DD7B (в RM — 0xC704DD78).
- Делители по умолчанию: HSI16 / 2 = 8 МГц, / 14 = 571,4 кГц на полубит,
285,7 кбит/с; окно 14 мкс;
UCPD_CFG1= 0x23C0D.
Полные исходники
ucpd20b.py — модель физического уровня Power Delivery на ПК.
# -*- coding: utf-8 -*-
"""Раздел 20б: модель физического уровня USB Power Delivery на ПК — то, что UCPD делает
аппаратно. Собирает сообщение Source_Capabilities, считает CRC-32, кодирует 4b5b и BMC,
декодирует обратно и сверяет остаток CRC; плюс расчёт делителей UCPD из RM0440.
Опоры (все на диске):
RM0440, глава 46 (G4) и RM0444, глава 38 (G0): таблица 424 4b5b, таблица 425 ordered sets,
таблица 426 «три K-кода из четырёх», таблица 427 размеры, рис. 676 порядок битов,
остаток CRC, CFG1: PSC_USBPDCLK, HBITCLKDIV, TRANSWIN, IFRGAP;
RM0440, глава CRC: многочлен CRC-32 (Ethernet) 0x4C11DB7;
stm32g4xx_ll_ucpd.c: LL_UCPD_StructInit (PSC /2, HBITCLKDIV 0x0D, TRANSWIN 0x7, IFRGAP 0x10);
stm32g4xx_ll_ucpd.h: LL_UCPD_SYNC1/2, LL_UCPD_ORDERED_SET_SOP;
usbpd_def.h (STM32_USBPD_Library из STM32Cube_FW_G0_V1.6.0): поля PDO и типы сообщений;
demo_disco.c (USBPD_Analyzer для STM32G071B-DISCO): раскладка заголовка сообщения.
Преамбулу модель не строит: её длину задаёт стандарт PD, в RM её нет.
"""
import sys, zlib
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
# ── 1. Делители UCPD ─────────────────────────────────────────────────────
HSI16 = 16_000_000 # ucpd_ker_ck = HSI16 (RM0440, 46.4.2)
PSC_CODES = {0: 1, 1: 2, 2: 4, 3: 8, 4: 16} # PSC_USBPDCLK[2:0] (RM0440, 46.7.1)
print("== делители UCPD ==")
for code, div in PSC_CODES.items():
f = HSI16 / div
best = min(range(1, 65), key=lambda d: abs(f / d - 600e3)) # hbit_clk ~600 кГц (рис. 674)
ok = "да" if 6e6 <= f <= 9e6 else "нет"
print("PSC код %d (/%d): ucpd_clk %.1f МГц, в 6..9 МГц: %s; ближе всего к 600 кГц — /%d: hbit %.1f кГц, бит %.1f кбит/с"
% (code, div, f / 1e6, ok, best, f / best / 1e3, f / best / 2e3))
psc, hbit_div, transwin_code, ifrgap_code = 2, 0x0D + 1, 0x7, 0x10 # LL_UCPD_StructInit
ucpd_clk = HSI16 / psc
hbit = ucpd_clk / hbit_div
bitrate = hbit / 2
t_win = (transwin_code + 1) / hbit
t_win_rm = (0x09 + 1) / hbit # TRANSWIN 0x09 = 10, «recommended»
print("LL_UCPD_StructInit: PSC /%d -> ucpd_clk %.1f МГц; HBITCLKDIV 0x0D -> /%d -> hbit_clk %.1f кГц; бит %.1f кбит/с"
% (psc, ucpd_clk / 1e6, hbit_div, hbit / 1e3, bitrate / 1e3))
print("половина бита %.3f мкс, бит %.3f мкс" % (1e6 / hbit, 1e6 / bitrate))
print("TRANSWIN 0x7 -> %d полубитов = %.1f мкс; RM 0x09 -> 10 полубитов = %.1f мкс; окно RM 12..20 мкс: %s"
% (transwin_code + 1, t_win * 1e6, t_win_rm * 1e6, "да" if 12e-6 <= t_win <= 20e-6 and 12e-6 <= t_win_rm <= 20e-6 else "нет"))
rm_ex = HSI16 / 1 / 27 # пример RM: HBITCLKDIV 0x1A = 27
print("пример RM HBITCLKDIV 0x1A (/27) при PSC /1: hbit %.1f кГц, бит %.1f кбит/с" % (rm_ex / 1e3, rm_ex / 2e3))
CFG1 = (1 << 17) | (transwin_code << 11) | (ifrgap_code << 6) | (hbit_div - 1)
print("CFG1 без RXORDSETEN = 0x%05X" % CFG1)
print("CFG1 с приёмом SOP (RXORDSETEN бит 0) = 0x%06X" % (CFG1 | (1 << 20)))
# ── 2. 4b5b и ordered sets ───────────────────────────────────────────────
# RM0440, таблица 424: код 5b записан от b4 к b0
T424 = ["11110", "01001", "10100", "10101", "01010", "01011", "01110", "01111",
"10010", "10011", "10110", "10111", "11010", "11011", "11100", "11101"]
SYM = {i: int(s, 2) for i, s in enumerate(T424)}
K = {"Sync-1": int("11000", 2), "Sync-2": int("10001", 2), "RST-1": int("00111", 2),
"RST-2": int("11001", 2), "EOP": int("01101", 2), "Sync-3": int("00110", 2)}
SOP = ["Sync-1", "Sync-1", "Sync-1", "Sync-2"] # таблица 425
txordset = sum(K[k] << (5 * i) for i, k in enumerate(SOP)) # «bit 0 of K-code1 is first»
print("\n== 4b5b ==")
print("Sync-1 = 0x%02X, Sync-2 = 0x%02X, Sync-3 = 0x%02X, EOP = 0x%02X" % (K["Sync-1"], K["Sync-2"], K["Sync-3"], K["EOP"]))
print("TXORDSET для SOP = 0x%05X" % txordset)
def bits5(v): # передача от бита 0 (рис. 675)
return [(v >> i) & 1 for i in range(5)]
def enc_byte(b): # младшая тетрада первой (рис. 676)
return bits5(SYM[b & 0xF]) + bits5(SYM[b >> 4])
# ── 3. Сообщение Source_Capabilities ─────────────────────────────────────
# заголовок: MessageType 5 | PortDataRole 1 | SpecRev 2 | PortPowerRole 1 | MessageID 3 | NDO 3 | Ext 1
MSG_SRC_CAPABILITIES, DFP, REV30, SRC = 0x01, 0x01, 0x02, 0x01 # usbpd_def.h
header = MSG_SRC_CAPABILITIES | (DFP << 5) | (REV30 << 6) | (SRC << 8) | (0 << 9) | (1 << 12)
volt_mv, cur_ma = 5000, 3000
pdo = (0 << 30) | ((volt_mv // 50) << 10) | (cur_ma // 10) # Fixed: 50 мВ и 10 мА
payload = header.to_bytes(2, "little") + pdo.to_bytes(4, "little")
print("\n== Source_Capabilities: 5 В, 3 А ==")
print("заголовок 0x%04X, PDO 0x%08X (напряжение %d x 50 мВ, ток %d x 10 мА)" % (header, pdo, volt_mv // 50, cur_ma // 10))
print("полезная нагрузка %d байт (TXPAYSZ): %s" % (len(payload), " ".join("%02X" % b for b in payload)))
# ── 4. CRC-32 ────────────────────────────────────────────────────────────
POLY = 0x04C11DB7 # RM0440, глава CRC: CRC-32 (Ethernet)
def reg_feed(reg, bits): # сдвиговый регистр, биты в порядке передачи
for b in bits:
fb = ((reg >> 31) & 1) ^ b
reg = ((reg << 1) & 0xFFFFFFFF) ^ (POLY if fb else 0)
return reg
def lsb_bits(data):
return [(x >> i) & 1 for x in data for i in range(8)]
crc = zlib.crc32(payload) & 0xFFFFFFFF
reg = reg_feed(0xFFFFFFFF, lsb_bits(payload))
crc_reg = int("".join(str(((~reg) >> (31 - i)) & 1) for i in range(32))[::-1], 2) # ~reg, от бита 31 к 0
print("\n== CRC-32 ==")
print("CRC = 0x%08X (zlib), сдвиговый регистр: 0x%08X, совпадают: %s" % (crc, crc_reg, "да" if crc == crc_reg else "нет"))
frame_bytes = payload + crc.to_bytes(4, "little")
res = reg_feed(0xFFFFFFFF, lsb_bits(frame_bytes))
print("остаток регистра после данных и CRC: 0x%08X" % res)
zres = zlib.crc32(frame_bytes) ^ 0xFFFFFFFF # zlib: регистр, отражённый и без итоговой инверсии
zres_rev = int(format(zres, "032b")[::-1], 2)
print("zlib по кадру с CRC: регистр 0x%08X, в обратном порядке битов 0x%08X, совпадает с моделью: %s"
% (zres, zres_rev, "да" if zres_rev == res else "нет"))
print("RM0440 и RM0444 печатают остаток 0xC704DD78; отличие от модели в битах: 0x%08X" % (res ^ 0xC704DD78))
# варианты: без начальных единиц и без инверсии остаток другой
for init, inv in ((0, False), (0xFFFFFFFF, False), (0, True), (0xFFFFFFFF, True)):
r = reg_feed(init, lsb_bits(payload))
tx = (~r & 0xFFFFFFFF) if inv else r
tx_bits = [(tx >> (31 - i)) & 1 for i in range(32)]
print("вариант: начало 0x%08X, инверсия %s -> остаток 0x%08X" % (init, "да" if inv else "нет", reg_feed(init, lsb_bits(payload) + tx_bits)))
# остаток не зависит от содержимого
same = all(reg_feed(0xFFFFFFFF, lsb_bits(p + zlib.crc32(p).to_bytes(4, "little"))) == res
for p in (b"\x41\x00", bytes(range(30)), payload[::-1]))
print("остаток одинаков для других сообщений: %s" % ("да" if same else "нет"))
# ── 5. Кадр: SOP, данные, CRC, EOP -> 4b5b -> BMC ───────────────────────
line = []
for k in SOP:
line += bits5(K[k])
for b in frame_bytes:
line += enc_byte(b)
line += bits5(K["EOP"])
n_sop, n_data, n_crc = 20, 10 * len(payload), 40
print("\n== кадр ==")
print("бит после преамбулы: SOP %d + данные %d + CRC %d + EOP 5 = %d" % (n_sop, n_data, n_crc, len(line)))
print("кодовая эффективность 4b5b: 4/5 = %.0f %%" % (4 / 5 * 100))
t_frame = len(line) / bitrate
print("время кадра без преамбулы при %.1f кбит/с: %.1f мкс" % (bitrate / 1e3, t_frame * 1e6))
print("первые 20 бит (SOP): %s" % "".join(map(str, line[:20])))
def bmc(bits, level=0): # переход на каждой границе бита, для 1 — ещё в середине
out = []
for b in bits:
level ^= 1
out.append(level)
if b:
level ^= 1
out.append(level)
return out
def bmc_dec(half):
return [1 if half[2 * i] != half[2 * i + 1] else 0 for i in range(len(half) // 2)]
half = bmc(line)
edges = sum(1 for i in range(1, len(half)) if half[i] != half[i - 1]) + 1 # первый полубит — тоже переход
ones = sum(line)
print("BMC: %d полубитов, единиц %d, нулей %d, переходов %d = %d границ + %d середин"
% (len(half), ones, len(line) - ones, edges, len(line), ones))
dc = sum(1 if h else -1 for h in half)
print("постоянная составляющая (сумма полубитов +1/-1): %d" % dc)
inv = bmc_dec([h ^ 1 for h in half]) == line
print("декодер не зависит от полярности: %s" % ("да" if inv else "нет"))
# ── 6. Приём: BMC -> 5b -> 4b, проверка CRC ──────────────────────────────
rx = bmc_dec(half)
REV = {v: k for k, v in SYM.items()}
KREV = {v: k for k, v in K.items()}
syms = [sum(b << i for i, b in enumerate(rx[j:j + 5])) for j in range(0, len(rx), 5)]
sop_ok = [KREV.get(s) for s in syms[:4]] == SOP
data = syms[4:-1]
dec = bytes(REV[data[2 * i]] | (REV[data[2 * i + 1]] << 4) for i in range(len(data) // 2))
rx_res = reg_feed(0xFFFFFFFF, lsb_bits(dec))
print("\n== приём ==")
print("SOP распознан: %s; EOP: %s; байт %d; заголовок 0x%04X; PDO 0x%08X; остаток 0x%08X; CRC верен: %s"
% ("да" if sop_ok else "нет", "да" if KREV.get(syms[-1]) == "EOP" else "нет", len(dec) - 4,
int.from_bytes(dec[:2], "little"), int.from_bytes(dec[2:6], "little"), rx_res, "да" if rx_res == res else "нет"))
bad = bytearray(dec)
bad[3] ^= 0x04
print("один испорченный бит в PDO: остаток 0x%08X, CRC верен: %s"
% (reg_feed(0xFFFFFFFF, lsb_bits(bytes(bad))), "да" if reg_feed(0xFFFFFFFF, lsb_bits(bytes(bad))) == res else "нет"))
# «три K-кода из четырёх» (таблица 426)
def sop_valid(codes):
return sum(1 for a, b in zip(codes, SOP) if a == b) >= 3
one_bad = ["Sync-1", "RST-1", "Sync-1", "Sync-2"]
two_bad = ["Sync-1", "RST-1", "Sync-1", "RST-2"]
print("SOP с одним испорченным K-кодом принят: %s; с двумя: %s"
% ("да" if sop_valid(one_bad) else "нет", "да" if sop_valid(two_bad) else "нет"))
# ── 7. Токи по Rp: кодировка ANASUBMODE и TYPEC_VSTATE (таблица 428) ─────
print("\n== таблица 428 ==")
for sub, name in ((1, "Default USB"), (2, "1.5A"), (3, "3.0A")):
print("источник ANASUBMODE %d%d -> %s; потребитель видит TYPEC_VSTATE = %d (vRd-%s)"
% (sub >> 1, sub & 1, name, sub, {1: "USB", 2: "1.5", 3: "3.0"}[sub]))
print("потребитель: TYPEC_VSTATE = 0 -> линия открыта, источника нет (LL_UCPD_SNK_CCx_VOPEN)")
ucpd20b.c — UCPD по регистрам: потребитель на STM32G474.
/* 20б: UCPD по регистрам — потребитель (sink). Rd на обеих линиях CC, ждём источник,
узнаём ориентацию кабеля и ток, который он объявил резистором Rp.
Собирается armclang: -DSTM32G474xx (UCPD1 на PB6/PB4) или -DSTM32G071xx (UCPD1 на PA8/PB15). */
#include <stdint.h>
/* делители — как в LL_UCPD_StructInit: HSI16 / 2 = 8 МГц, полубит / 14, окно / 8, промежуток / 17 */
#define UCPD_CFG1_DIVS ((1u << UCPD_CFG1_PSC_UCPDCLK_Pos) | (7u << UCPD_CFG1_TRANSWIN_Pos) | \
(16u << UCPD_CFG1_IFRGAP_Pos) | (13u << UCPD_CFG1_HBITCLKDIV_Pos))
enum cc_level { CC_OPEN = 0, CC_DEFAULT = 1, CC_1A5 = 2, CC_3A0 = 3 }; /* TYPEC_VSTATE у потребителя */
_Static_assert(UCPD1_BASE == 0x4000A000u, "UCPD1 на APB");
_Static_assert(UCPD_CFG1_DIVS == 0x23C0Du, "CFG1: PSC /2, TRANSWIN 7, IFRGAP 16, HBITCLKDIV 13");
_Static_assert(UCPD_SR_TYPEC_VSTATE_CC1_Pos == 16u && UCPD_SR_TYPEC_VSTATE_CC2_Pos == 18u, "уровни CC1 и CC2 в SR");
_Static_assert((UCPD_CR_ANAMODE | UCPD_CR_CCENABLE) == 0xE00u, "потребитель, обе линии CC");
#if defined(STM32G474xx)
_Static_assert(PWR_CR3_UCPD_DBDIS == (1u << 14), "G4: снятие Rd мёртвой батареи — PWR_CR3");
#elif defined(STM32G071xx)
_Static_assert(SYSCFG_CFGR1_UCPD1_STROBE == (1u << 9), "G0: применение настройки — строб в SYSCFG");
#endif
void ucpd_sink_init(void)
{
#if defined(STM32G474xx)
RCC->AHB2ENR |= RCC_AHB2ENR_GPIOBEN;
RCC->APB1ENR1 |= RCC_APB1ENR1_PWREN;
RCC->APB1ENR2 |= RCC_APB1ENR2_UCPD1EN;
GPIOB->PUPDR &= ~GPIO_PUPDR_PUPD4; /* PB4 после сброса — NJTRST с подтяжкой */
GPIOB->MODER |= GPIO_MODER_MODE4 | GPIO_MODER_MODE6; /* CC2 = PB4, CC1 = PB6 — аналоговый режим */
#elif defined(STM32G071xx)
RCC->IOPENR |= RCC_IOPENR_GPIOAEN | RCC_IOPENR_GPIOBEN;
RCC->APBENR2 |= RCC_APBENR2_SYSCFGEN;
RCC->APBENR1 |= RCC_APBENR1_UCPD1EN;
GPIOA->MODER |= GPIO_MODER_MODE8; /* CC1 = PA8 */
GPIOB->MODER |= GPIO_MODER_MODE15; /* CC2 = PB15 */
#endif
UCPD1->CFG1 = UCPD_CFG1_DIVS | UCPD_CFG1_RXORDSETEN_0; /* пишется только при UCPDEN = 0; приём SOP */
UCPD1->CFG1 |= UCPD_CFG1_UCPDEN;
UCPD1->CR = UCPD_CR_ANAMODE | UCPD_CR_CCENABLE; /* пишется только при UCPDEN = 1 */
#if defined(STM32G474xx)
PWR->CR3 |= PWR_CR3_UCPD_DBDIS; /* Rd мёртвой батареи — долой, дальше Rd от UCPD */
#elif defined(STM32G071xx)
SYSCFG->CFGR1 |= SYSCFG_CFGR1_UCPD1_STROBE; /* строб: применить ANAMODE к выводам */
#endif
}
/* Опрос: 0 — источника нет, иначе ток по Rp (1 — по умолчанию, 2 — 1,5 А, 3 — 3 А).
*cc2 = 1, если источник на CC2 (кабель перевёрнут). */
int ucpd_sink_poll(unsigned *cc2)
{
uint32_t sr = UCPD1->SR;
uint32_t v1 = (sr & UCPD_SR_TYPEC_VSTATE_CC1) >> UCPD_SR_TYPEC_VSTATE_CC1_Pos;
uint32_t v2 = (sr & UCPD_SR_TYPEC_VSTATE_CC2) >> UCPD_SR_TYPEC_VSTATE_CC2_Pos;
if (v1 != CC_OPEN && v2 == CC_OPEN) { *cc2 = 0; return (int)v1; }
if (v1 == CC_OPEN && v2 != CC_OPEN) { *cc2 = 1; return (int)v2; }
return CC_OPEN; /* обе открыты — никого; обе заняты — не кабель */
}
/* Источник найден: приёмник Power Delivery — на ту линию CC, где он */
void ucpd_sink_attach(unsigned cc2)
{
uint32_t cr = UCPD1->CR & ~UCPD_CR_PHYCCSEL;
UCPD1->CR = cr | (cc2 ? UCPD_CR_PHYCCSEL : 0u) | UCPD_CR_PHYRXEN;
}
/* Отправка: программа даёт только байты заголовка и объектов данных; преамбулу, SOP, 4b5b,
CRC-32, EOP и BMC добавляет UCPD. 0 — ушло, -1 — отброшено (линия занята). */
#define PD_SOP (0x18u | (0x18u << 5) | (0x18u << 10) | (0x11u << 15)) /* Sync-1 x3, Sync-2 */
_Static_assert(PD_SOP == 0x8E318u, "SOP: четыре K-кода по 5 бит, первый — в младших битах");
int ucpd_send(const uint8_t *msg, unsigned len)
{
unsigned i = 0;
uint32_t sr;
UCPD1->TX_ORDSET = PD_SOP;
UCPD1->TX_PAYSZ = len; /* с заголовком, без CRC */
UCPD1->CR |= UCPD_CR_TXSEND; /* TXMODE = 0: обычное сообщение */
do {
sr = UCPD1->SR;
if ((sr & UCPD_SR_TXIS) && i < len)
UCPD1->TXDR = msg[i++]; /* TXIS: регистр пуст, следующий байт */
} while (!(sr & (UCPD_SR_TXMSGSENT | UCPD_SR_TXMSGDISC)));
UCPD1->ICR = UCPD_ICR_TXMSGSENTCF | UCPD_ICR_TXMSGDISCCF;
return (sr & UCPD_SR_TXMSGSENT) ? 0 : -1;
}
Дальше: раздел 20в «USB HID на практике: дескриптор отчёта и составное устройство» — сторона устройства: клавиатура, мышь и клавиши громкости на одном интерфейсе.