Embedded HandbookПриложения
Все разделы › III. Периферия
STM32G474, STM32G071 · уровень средний · 27 мин чтения
Открыть в интерактивном справочнике

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 встречаются только на одной линии, и получается делитель:

Линии CC разъёма Type-C: у источника подтяжки Rp к питанию на CC1 и CC2, у потребителя Rd 5,1 кОм на землю, выводы DBCC включают Rd и без питания; в кабеле один провод CC, поэтому источник по линии, где увидел Rd, узнаёт ориентацию кабеля, а потребитель по уровню на линии — допустимый ток

Встроенный 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. Отключают его так:

В 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 А.

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):

Все четыре поля — в 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 Итоги

Полные исходники

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 на практике: дескриптор отчёта и составное устройство» — сторона устройства: клавиатура, мышь и клавиши громкости на одном интерфейсе.

Открыть в интерактивном справочнике