Embedded HandbookПриложения
Все разделы › XXV. Мини-проекты
STM32U585 · уровень средний · 72 мин чтения
Открыть в интерактивном справочнике

198ш. Мини-проект: USB-COM и клавиатура в одном приборе на B-U585I-IOT02A — составное устройство CDC + HID

Раздел 20 назвал составным устройство с несколькими классами сразу, но показал только виртуальный COM-порт. Раздел 20в собрал клавиатуру и мышь на одном интерфейсе HID. В 198ф плата B-U585I-IOT02A была флешкой, в 198ч — хостом. Здесь та же плата по одному кабелю USB-C показывает компьютеру два устройства разом: COM-порт с командной консолью (состояние, журнал датчиков) и клавиатуру, у которой одна клавиша — кнопка USER. Нажал — плата «печатает» заготовленную строку в любое окно, где стоит курсор.

В пакете платы B-U585I-IOT02A_BSP 1.1.0 (Keil) есть два отдельных примера устройства на стеке MDK-Middleware — VirtualCOM (мост USB — USART1) и HID (кнопка и светодиоды через свою программу на ПК). Составного примера нет. Мы берём из примеров то, что относится к плате, — выводы, тактирование, настройку OTG_FS — и пишем остальное сами: дескрипторы, точку 0, оба класса и драйвер ядра OTG_FS регистрами. Это 973 строки в семи файлах; библиотеку они не заменят, зато в них виден каждый байт.

Проверено без платы. Дескрипторы, точка 0, CDC, HID и консоль собраны gcc на компьютере и прогнаны против модели хоста: она опознаёт устройство запрос за запросом, разбирает дескриптор конфигурации своим разборщиком, открывает порт, шлёт команды и читает отчёты клавиатуры. Драйвер OTG_FS на компьютере проверен частично — модель регистров показывает, что он пишет в регистры FIFO и точек. Вся прошивка собрана armclang под Cortex-M33; на плате она не запускалась.

Опоры: RM0456 Rev 4 (STM32U5, глава 72 «OTG_FS»: 72.2.3, таблица 740, 72.6.2, 72.11.3, регистры 72.15); даташит DS13086 (STM32U585xx, таблица 84 — HSI48); заголовок stm32u585xx.h и HAL (stm32u5xx_ll_usb.c, stm32u5xx_hal_pcd_ex.c) из пакета STM32U5xx_DFP 2.2.1; пакет платы B-U585I-IOT02A_BSP 1.1.0: примеры USB/Device/VirtualCOM и HID (лицензия Apache-2.0), драйвер b_u585i_iot02a_usbpd_pwr.c (BSD-3-Clause); MDK-Middleware 8.1.0 (usb_cdc.h, usbd_config_desc_macros.h, usbd_config_desc_0.c; лицензия Arm EULA — только пересказ и короткие цитаты); драйвер USBD_STM32.c из пакета CMSIS-Driver_STM32 1.3.0; составное устройство CDC + HID из github.com/betaflight/betaflight, коммит 744f95f (файл usbd_cdc_hid.c, лицензия GPL-3.0, только сверка); Zephyr 4.4.2 (include/zephyr/usb/class/hid.h, пример samples/subsys/usb/cdc_acm). Тексты спецификаций USB 2.0, CDC и HID не использовались: коды и форматы взяты из этих исходников. Лицензия: свой код раздела — свободно.

Как это называется в документации

Оригинал Перевод
Interface Association Descriptor (IAD) дескриптор объединения интерфейсов в одну функцию
function (в составном устройстве) функция: один или несколько интерфейсов под одним драйвером
Communications Class interface, Data Class interface интерфейс управления CDC и интерфейс данных CDC
Abstract Control Model (ACM) модель абстрактного управления: подкласс CDC для COM-порта
functional descriptor: Header, Call Management, Union функциональные дескрипторы CDC: заголовок, управление вызовами, объединение
line coding (dwDTERate, bCharFormat, bParityType, bDataBits) формат линии: скорость, стоп-биты, чётность, число бит
SERIAL_STATE notification уведомление о состоянии линии через прерывающую точку
zero-length packet (ZLP) пустой пакет: конец данных, кратных размеру пакета
wTotalLength полная длина дескриптора конфигурации со всеми вложенными
soft disconnect (SDIS) программное отключение: подтяжка D+ снята
B-session valid override (BVALOEN, BVALOVAL) подмена сигнала «VBUS есть» программой
USB turnaround time (TRDT) время разворота шины: запас на ответ устройства

198ш.1 Что за прибор и что видит хост

Прибор для хоста — три интерфейса и две функции:

Составное устройство на B-U585I-IOT02A: хост видит COM-порт под драйвером usbser и клавиатуру HID; разъём CN1 USB-C, ядро OTG_FS на PA11 и PA12, 48 МГц от HSI48 с подстройкой CRS; дескриптор объединения IAD связывает интерфейсы 0 и 1 в функцию CDC класса 02/02/01; интерфейс 0 с прерывающей точкой 0x81 на 16 байт, интерфейс 1 с массовыми точками 0x02 и 0x82 по 64 байта, интерфейс 2 HID с прерывающей точкой 0x83 на 8 байт; модули cdc.c с правилом DTR равно порт открыт, console.c с командами status, log, macro, type, hidkbd.c с отчётом 8 байт и кнопкой PC13 с антидребезгом 20 мс; память FIFO 320 слов: приёмный 64, передающие 16, 16, 64 и 16, занято 176; итог прогона и размеры прошивки

Что есть в примерах пакета. Оба запускают стек двумя вызовами, USBD_Initialize(0U) и USBD_Connect(0U); дескрипторы стек строит сам из файлов настройки. В VirtualCOM: VID 0xC251, PID 0x5905, прерывающая точка 1 на 16 байт с опросом 2 мс, массовые точки 2 по 64 байта. В HID: точка 1, пакет 4 байта, опрос 16 мс. Описание обоих: процессор 160 МГц, USB 48 МГц, OTG_FS на разъёме CN1 (USB Type-C).

Составное устройство стек MDK собирать умеет: макрос USBD0_USE_IAD включается, когда рядом с CDC есть другой класс, и тогда в дескрипторе устройства появляется класс USB_DEVICE_CLASS_MISCELLANEOUS (0xEF) с подклассом 2 и протоколом 1 — «смотри дескрипторы объединения». Готовое устройство CDC + HID есть в betaflight (usbd_cdc_hid.c, библиотека ST): полётный контроллер виден как COM-порт и устройство HID. С ним мы сверим длину и состав дескриптора.

Файлы проекта:

Файл Что в нём
usbdesc.c дескрипторы: устройство, конфигурация, отчёт HID, строки
usbcore.c точка 0: стандартные запросы, раздача запросов классам
cdc.c запросы CDC, уведомление, кольца приёма и передачи
hidkbd.c запросы HID, набор текста отчётами, кнопка
console.c команды и журнал
otg_u585.c, otg_regs.h драйвер OTG_FS регистрами
main_u585.c 48 МГц, CRS, VDDUSB, выводы, главный цикл

Ядро не знает регистров: пакеты оно отдаёт и получает через восемь функций drv_*. На плате их даёт otg_u585.c, в модели — host.c.


198ш.2 Дескрипторы: устройство, IAD и конфигурация байт за байтом

Дескриптор устройства. Отличие от одиночного CDC — три байта класса:

const uint8_t usb_device_desc[18] = {
    18, 0x01,                   /* bLength, DEVICE */
    0x00, 0x02,                 /* bcdUSB 2.00 */
    0xEF, 0x02, 0x01,           /* класс «разное», подкласс 2, протокол 1: смотри IAD */
    EP0_SIZE,                   /* bMaxPacketSize0 */
    USB_VID & 0xFF, USB_VID >> 8,
    USB_PID & 0xFF, USB_PID >> 8,
    0x00, 0x01,                 /* bcdDevice 1.00 */
    1, 2, 3,                    /* строки: изготовитель, изделие, серийный номер */
    1                           /* одна конфигурация */
};

VID и PID здесь — заглушки для макета (20.6): у изделия они свои.

Зачем IAD. Функция CDC занимает два интерфейса — управления и данных. Хосту надо знать, что интерфейсы 0 и 1 — одно устройство под одним драйвером, а интерфейс 2 — другое. Это и говорит IAD: 8 байт перед первым интерфейсом функции — первый номер, число интерфейсов и класс функции. Одиночному интерфейсу HID объединение не нужно. Класс устройства EF/02/01 предупреждает хост, что в конфигурации есть IAD.

Дескриптор конфигурации. Все вложенные дескрипторы идут одним массивом, хост читает его целиком по wTotalLength:

const uint8_t usb_config_desc[CONFIG_DESC_SIZE] = {
    /* конфигурация */
    9, 0x02, CONFIG_DESC_SIZE, 0x00,    /* wTotalLength = 100 */
    3,                          /* bNumInterfaces */
    1,                          /* bConfigurationValue */
    0,                          /* iConfiguration */
    0x80,                       /* bmAttributes: питание от шины */
    50,                         /* bMaxPower: 50 x 2 мА = 100 мА */
    /* IAD: интерфейсы 0 и 1 — одна функция CDC */
    8, 0x0B, IF_CDC_COMM, 2,    /* bFirstInterface, bInterfaceCount */
    0x02, 0x02, 0x01,           /* класс CDC, подкласс ACM, протокол AT */
    0,                          /* iFunction */
    /* интерфейс 0: управление CDC */
    9, 0x04, IF_CDC_COMM, 0, 1, /* одна точка */
    0x02, 0x02, 0x01, 0,        /* CDC, ACM, AT */
    5, 0x24, 0x00, 0x10, 0x01,  /* Header: bcdCDC 1.10 */
    5, 0x24, 0x01, 0x00, IF_CDC_DATA,   /* Call Management: вызовами не управляем */
    4, 0x24, 0x02, 0x02,        /* ACM: линия и её состояние (SET/GET_LINE_CODING, SERIAL_STATE) */
    5, 0x24, 0x06, IF_CDC_COMM, IF_CDC_DATA,    /* Union: главный 0, подчинённый 1 */
    7, 0x05, EP_CDC_NOTIFY, EP_TYPE_INTR, CDC_NOTIFY_SIZE, 0x00, 16,   /* уведомления, опрос 16 мс */
    /* интерфейс 1: данные CDC */
    9, 0x04, IF_CDC_DATA, 0, 2, /* две точки */
    0x0A, 0x00, 0x00, 0,        /* CDC Data */
    7, 0x05, EP_CDC_OUT, EP_TYPE_BULK, CDC_DATA_SIZE, 0x00, 0,
    7, 0x05, EP_CDC_IN, EP_TYPE_BULK, CDC_DATA_SIZE, 0x00, 0,
    /* интерфейс 2: HID, загрузочная клавиатура */
    9, 0x04, IF_HID, 0, 1,      /* одна точка */
    0x03, 0x01, 0x01, 0,        /* HID, подкласс 1 (загрузочный), протокол 1 (клавиатура) */
    9, 0x21, 0x11, 0x01, 0x00,  /* HID: bcdHID 1.11, страна 0 */
    1, 0x22, HID_REPORT_DESC_SIZE, 0x00,    /* один дескриптор отчёта, 63 байта */
    7, 0x05, EP_HID_IN, EP_TYPE_INTR, HID_REPORT_SIZE, 0x00, HID_INTERVAL_MS
};

Тот же массив глазами хоста — разбор из модели: слева смещение, дальше байты и расшифровка.

  0: 09 02 64 00 03 01 00 80 32
     CONFIGURATION: wTotalLength 100, интерфейсов 3, номер 1, bmAttributes 0x80, 100 мА
  9: 08 0B 00 02 02 02 01 00
     IAD: интерфейсы 0…1 — одна функция, класс 02/02/01
 17: 09 04 00 00 01 02 02 01 00
     INTERFACE 0: точек 1, класс 02/02/01
 26: 05 24 00 10 01
     CDC, подтип 00 Header: bcdCDC 1.10
 31: 05 24 01 00 01
     CDC, подтип 01 Call Management: возможности 0x00, интерфейс данных 1
 36: 04 24 02 02
     CDC, подтип 02 ACM: возможности 0x02
 40: 05 24 06 00 01
     CDC, подтип 06 Union: главный 0, подчинённый 1
 45: 07 05 81 03 10 00 10
     ENDPOINT 0x81: прерывающая IN, пакет 16, bInterval 16 (интерфейс 0)
 52: 09 04 01 00 02 0A 00 00 00
     INTERFACE 1: точек 2, класс 0A/00/00
 61: 07 05 02 02 40 00 00
     ENDPOINT 0x02: массовая OUT, пакет 64, bInterval 0 (интерфейс 1)
 68: 07 05 82 02 40 00 00
     ENDPOINT 0x82: массовая IN, пакет 64, bInterval 0 (интерфейс 1)
 75: 09 04 02 00 01 03 01 01 00
     INTERFACE 2: точек 1, класс 03/01/01
 84: 09 21 11 01 00 01 22 3F 00
     HID: bcdHID 1.11, дескрипторов 1, тип 0x22, длина 63
 93: 07 05 83 03 08 00 0A
     ENDPOINT 0x83: прерывающая IN, пакет 8, bInterval 10 (интерфейс 2)
итог разбора: пройдено 100 байт из 100, wTotalLength 100, интерфейсов 3 (заявлено 3), точек 4, IAD 1, функциональных CDC 4, HID 1 — сходится

Что здесь важно:

Дескриптор отчёта клавиатуры — язык из 20в.2, поэтому без разбора:

const uint8_t usb_hid_report_desc[HID_REPORT_DESC_SIZE] = {
    0x05, 0x01,                 /* Usage Page (Generic Desktop) */
    0x09, 0x06,                 /* Usage (Keyboard) */
    0xA1, 0x01,                 /* Collection (Application) */
    0x05, 0x07,                 /*   Usage Page (Keyboard) */
    0x19, 0xE0, 0x29, 0xE7,     /*   Usage Min/Max: восемь модификаторов */
    0x15, 0x00, 0x25, 0x01,     /*   Logical Min/Max: 0 … 1 */
    0x75, 0x01, 0x95, 0x08,     /*   Report Size 1, Report Count 8 */
    0x81, 0x02,                 /*   Input (Data, Var, Abs): байт 0 */
    0x95, 0x01, 0x75, 0x08,     /*   Report Count 1, Report Size 8 */
    0x81, 0x01,                 /*   Input (Const): байт 1, резерв */
    0x95, 0x05, 0x75, 0x01,     /*   Report Count 5, Report Size 1 */
    0x05, 0x08,                 /*   Usage Page (LEDs) */
    0x19, 0x01, 0x29, 0x05,     /*   Usage Min/Max: пять светодиодов */
    0x91, 0x02,                 /*   Output (Data, Var, Abs) */
    0x95, 0x01, 0x75, 0x03,     /*   Report Count 1, Report Size 3 */
    0x91, 0x01,                 /*   Output (Const): добивка до байта */
    0x95, 0x06, 0x75, 0x08,     /*   Report Count 6, Report Size 8 */
    0x15, 0x00, 0x25, 0x65,     /*   Logical Min/Max: 0 … 101 */
    0x05, 0x07,                 /*   Usage Page (Keyboard) */
    0x19, 0x00, 0x29, 0x65,     /*   Usage Min/Max: коды клавиш 0 … 101 */
    0x81, 0x00,                 /*   Input (Data, Array): байты 2…7 */
    0xC0                        /* End Collection */
};

63 байта, 32 элемента, вход 8 байт, выход 1 байт (светодиоды). У макроса HID_KEYBOARD_REPORT_DESC() Zephyr те же длина, число элементов и отчёты; отличаются порядок Report Size и Report Count и флаг добивки (у Zephyr 0x03, у нас 0x01).


198ш.3 Точка 0: опознание, строки и серийный номер

Управляющая передача — это SETUP из 8 байт, потом данные в одну сторону пакетами до 64 байт, потом пустой пакет в обратную («состояние»). Стадию данных IN ведут три функции:

static void ep0_next(void)
{
    uint16_t n = c.tx_left < EP0_SIZE ? c.tx_left : EP0_SIZE;
    drv_ep0_tx(c.tx, n);
    c.tx += n;
    c.tx_left -= n;
}
static void ep0_send(const uint8_t *p, uint16_t len, uint16_t wlength)
{
    if (len > wlength) len = wlength;
    c.tx = p;
    c.tx_left = len;
    c.tx_zlp = len < wlength && len != 0 && len % EP0_SIZE == 0;
    ep0_next();
}
void usb_ep0_in_done(void)
{
    if (c.tx_left) ep0_next();
    else if (c.tx_zlp) { c.tx_zlp = 0; drv_ep0_tx(0, 0); }
}

Разбор запроса. Стандартные запросы обрабатывает ядро, запросы класса уходят в cdc.c или hidkbd.c по номеру интерфейса в wIndex:

void usb_setup(const uint8_t s[8])
{
    const uint8_t *p = 0;
    uint16_t len = 0, wlength = (uint16_t)(s[6] | s[7] << 8);
    uint8_t type = (s[0] >> 5) & 3u, iface = s[4];
    int r = REQ_STALL;

    c.rx_want = 0;
    if (type == 0) {
        r = standard_request(s, &p, &len);
    } else if (type == 1 && (s[0] & 0x1Fu) == 1 && c.config) {  /* запрос класса к интерфейсу */
        if (iface == IF_CDC_COMM) r = cdc_setup(s, &p, &len);
        else if (iface == IF_HID) r = hid_setup(s, &p, &len);
    }
    if (r == REQ_IN && (s[0] & 0x80u)) {
        ep0_send(p, len, wlength);
    } else if (r == REQ_OUT && !(s[0] & 0x80u) && wlength && wlength <= sizeof c.buf) {
        memcpy(c.setup, s, 8);              /* данные придут следующими пакетами OUT */
        c.rx_len = 0;
        c.rx_want = wlength;
    } else if (r == REQ_NODATA) {
        ep0_status();
    } else {
        drv_ep0_stall();
    }
}
static int standard_request(const uint8_t s[8], const uint8_t **p, uint16_t *len)
{
    uint8_t rcpt = s[0] & 0x1Fu, req = s[1];
    uint16_t wvalue = (uint16_t)(s[2] | s[3] << 8), windex = (uint16_t)(s[4] | s[5] << 8);
    uint8_t *h;
    switch (req) {
    case 0x00:                  /* GET_STATUS */
        if (rcpt == 2 && (h = halt_flag((uint8_t)windex)) != 0) c.buf[0] = *h;
        else if (rcpt == 2 && (windex & 0x7Fu) != 0) return REQ_STALL;
        else c.buf[0] = 0;      /* питание от шины, будить хост не умеем */
        c.buf[1] = 0;
        *p = c.buf; *len = 2;
        return REQ_IN;
    case 0x01: case 0x03:       /* CLEAR_FEATURE, SET_FEATURE: только ENDPOINT_HALT */
        if (rcpt != 2 || wvalue != 0 || (h = halt_flag((uint8_t)windex)) == 0) return REQ_STALL;
        *h = req == 0x03;
        drv_ep_stall((uint8_t)windex, *h);
        return REQ_NODATA;
    case 0x05:                  /* SET_ADDRESS: у OTG адрес пишут сразу, до стадии состояния */
        drv_set_address((uint8_t)(wvalue & 0x7Fu));
        return REQ_NODATA;
    case 0x06:                  /* GET_DESCRIPTOR */
        return get_descriptor(s[3], s[2], windex, p, len) ? REQ_IN : REQ_STALL;
    case 0x08:                  /* GET_CONFIGURATION */
        c.buf[0] = c.config;
        *p = c.buf; *len = 1;
        return REQ_IN;
    case 0x09:                  /* SET_CONFIGURATION */
        if (wvalue > 1) return REQ_STALL;
        set_config((uint8_t)wvalue);
        return REQ_NODATA;
    case 0x0A:                  /* GET_INTERFACE: запасных настроек нет */
        if (!c.config || windex > IF_HID) return REQ_STALL;
        c.buf[0] = 0;
        *p = c.buf; *len = 1;
        return REQ_IN;
    case 0x0B:                  /* SET_INTERFACE */
        return (c.config && windex <= IF_HID && wvalue == 0) ? REQ_NODATA : REQ_STALL;
    default:
        return REQ_STALL;
    }
}
static int get_descriptor(uint8_t type, uint8_t index, uint16_t windex, const uint8_t **p, uint16_t *len)
{
    switch (type) {
    case 0x01: *p = usb_device_desc; *len = sizeof usb_device_desc; return 1;
    case 0x02: *p = usb_config_desc; *len = sizeof usb_config_desc; return index == 0;
    case 0x03: *p = c.buf; *len = usb_string_desc(index, c.buf); return *len != 0;
    case 0x21: *p = usb_config_desc + HID_DESC_OFFSET; *len = 9; return windex == IF_HID;    /* HID внутри конфигурации */
    case 0x22: *p = usb_hid_report_desc; *len = sizeof usb_hid_report_desc; return windex == IF_HID;
    default: return 0;          /* 0x06 DEVICE_QUALIFIER: устройство только полной скорости — отказ */
    }
}

SET_CONFIGURATION открывает четыре точки и взводит приём:

static void set_config(uint8_t v)
{
    if (c.config) {
        drv_ep_close(EP_CDC_NOTIFY);
        drv_ep_close(EP_CDC_OUT);
        drv_ep_close(EP_CDC_IN);
        drv_ep_close(EP_HID_IN);
    }
    c.config = v;
    c.halt_cdc_out = c.halt_cdc_in = c.halt_hid_in = c.halt_notify = 0;
    cdc_reset();
    hid_reset();
    if (v) {
        drv_ep_open(EP_CDC_NOTIFY, EP_TYPE_INTR, CDC_NOTIFY_SIZE);
        drv_ep_open(EP_CDC_OUT, EP_TYPE_BULK, CDC_DATA_SIZE);
        drv_ep_open(EP_CDC_IN, EP_TYPE_BULK, CDC_DATA_SIZE);
        drv_ep_open(EP_HID_IN, EP_TYPE_INTR, HID_REPORT_SIZE);
        drv_ep_rx_arm(EP_CDC_OUT);
    }
}

Данные OUT (формат линии, светодиоды) копятся, пока не придёт wLength байт, и только тогда уходят в класс:

void usb_ep0_out(const uint8_t *p, uint16_t n)
{
    if (!c.rx_want) return;                 /* пустой пакет — стадия состояния после IN */
    if (n > c.rx_want - c.rx_len) n = (uint16_t)(c.rx_want - c.rx_len);
    memcpy(c.buf + c.rx_len, p, n);
    c.rx_len = (uint16_t)(c.rx_len + n);
    if (c.rx_len < c.rx_want) return;
    c.rx_want = 0;
    int r = c.setup[4] == IF_CDC_COMM ? cdc_setup_data(c.setup, c.buf, c.rx_len)
                                      : hid_setup_data(c.setup, c.buf, c.rx_len);
    if (r == REQ_NODATA) ep0_status();
    else drv_ep0_stall();
}

Опознание в модели. Хост делает то, что делают системы: дескриптор устройства, адрес, конфигурация сначала на 9 байт, потом целиком, строки:

сброс шины: конфигурация 0
SETUP 80 06 00 01 00 00 40 00  GET_DESCRIPTOR(DEVICE, 64): IN 18 = 18 байт
  12 01 00 02 EF 02 01 40 09 12 01 00 00 01 01 02 03 01
  bcdUSB 2.00, класс EF/02/01, пакет точки 0 — 64, VID 1209, PID 0001, строки 1 2 3, конфигураций 1
SETUP 00 05 05 00 00 00 00 00  SET_ADDRESS(5): состояние принято
  адрес устройства: 5
SETUP 80 06 00 01 00 00 12 00  GET_DESCRIPTOR(DEVICE, 18): IN 18 = 18 байт
SETUP 80 06 00 06 00 00 0A 00  GET_DESCRIPTOR(DEVICE_QUALIFIER): STALL
SETUP 80 06 00 02 00 00 09 00  GET_DESCRIPTOR(CONFIGURATION, 9): IN 9 = 9 байт
  wTotalLength = 100
SETUP 80 06 00 02 00 00 40 00  GET_DESCRIPTOR(CONFIGURATION, 64): IN 64 = 64 байт
SETUP 80 06 00 02 00 00 64 00  GET_DESCRIPTOR(CONFIGURATION, 100): IN 64+36 = 100 байт
SETUP 80 06 00 02 00 00 FF 00  GET_DESCRIPTOR(CONFIGURATION, 255): IN 64+36 = 100 байт
SETUP 80 06 00 03 00 00 FF 00  GET_DESCRIPTOR(STRING 0): IN 4 = 4 байт
  длина 4: язык 0x0409
SETUP 80 06 01 03 09 04 FF 00  GET_DESCRIPTOR(STRING 1): IN 30 = 30 байт
  длина 30: «STM32 Handbook»
SETUP 80 06 02 03 09 04 FF 00  GET_DESCRIPTOR(STRING 2): IN 40 = 40 байт
  длина 40: «U585 COM + keyboard»
SETUP 80 06 03 03 09 04 FF 00  GET_DESCRIPTOR(STRING 3): IN 50 = 50 байт
  длина 50: «002700415632500D20363452»
SETUP 80 06 04 03 09 04 FF 00  GET_DESCRIPTOR(STRING 4): STALL
SETUP A1 21 00 00 00 00 07 00  класс до настройки: GET_LINE_CODING: STALL
SETUP 00 09 01 00 00 00 00 00  SET_CONFIGURATION(1): состояние принято
  открыты точки: 0x81 прерывающая 16; 0x82 массовая 64; 0x02 массовая 64 (приём взведён: 1); 0x83 прерывающая 8;
SETUP 80 08 00 00 00 00 01 00  GET_CONFIGURATION: IN 1 = 1 байт
  конфигурация 1

На что смотреть:

Строки. Дескриптор строки — 2 байта заголовка и текст в UTF-16LE. Строка 0 — список языков, у нас один: 0x0409. Серийный номер — 96 бит номера кристалла (у U585 он лежит по адресу 0x0BFA0700, UID_BASE) в виде 24 шестнадцатеричных знаков: 2 + 24 × 2 = 50 байт. В модели номер придуман.

void usb_serial_from_uid(const uint32_t uid[3])
{
    static const char hex[] = "0123456789ABCDEF";
    for (int w = 0; w < 3; w++)
        for (int i = 0; i < 8; i++)
            serial[w * 8 + i] = hex[(uid[w] >> (28 - 4 * i)) & 0xFu];
}
uint16_t usb_string_desc(uint8_t index, uint8_t *buf)
{
    static const char *const text[] = { "STM32 Handbook", "U585 COM + keyboard", serial };
    if (index == 0) {                       /* список языков: 0x0409, английский (США) */
        buf[0] = 4; buf[1] = 0x03; buf[2] = 0x09; buf[3] = 0x04;
        return 4;
    }
    if (index > 3) return 0;
    const char *s = text[index - 1];
    uint16_t n = 2;
    while (*s && n + 2u <= EP0_SIZE) {
        buf[n++] = (uint8_t)*s++;
        buf[n++] = 0;
    }
    buf[0] = (uint8_t)n;
    buf[1] = 0x03;
    return n;
}

198ш.4 CDC: формат линии, DTR и консоль

У ACM три запроса класса и одно уведомление. Коды — из usb_cdc.h MDK: SET_LINE_CODING 0x20, GET_LINE_CODING 0x21, SET_CONTROL_LINE_STATE 0x22, SEND_BREAK 0x23; уведомление SERIAL_STATE — тоже 0x20, но оно идёт от устройства по прерывающей точке.

int cdc_setup(const uint8_t s[8], const uint8_t **data, uint16_t *len)
{
    uint16_t wvalue = (uint16_t)(s[2] | s[3] << 8), wlength = (uint16_t)(s[6] | s[7] << 8);
    switch (s[1]) {
    case 0x20:                              /* SET_LINE_CODING: 7 байт придут стадией данных */
        return (s[0] == 0x21 && wlength == 7) ? REQ_OUT : REQ_STALL;
    case 0x21:                              /* GET_LINE_CODING */
        if (s[0] != 0xA1) return REQ_STALL;
        *data = line_coding;
        *len = 7;
        return REQ_IN;
    case 0x22: {                            /* SET_CONTROL_LINE_STATE: бит 0 — DTR, бит 1 — RTS */
        uint8_t was = dtr;
        if (s[0] != 0x21) return REQ_STALL;
        dtr = wvalue & 1u;
        rts = (wvalue >> 1) & 1u;
        if (dtr && !was)                    /* терминал открыл порт: уведомление и приветствие */
            notify_pending = banner_pending = 1;    /* сами тексты уйдут из главного цикла */
        if (!dtr && was) {                  /* порт закрыт: недосланное никому не нужно */
            dropped += tx_head - tx_tail;
            tx_tail = tx_head;
            tx_zlp = 0;
        }
        return REQ_NODATA;
    }
    default:                                /* SEND_BREAK и прочее не объявлено в bmCapabilities */
        return REQ_STALL;
    }
}
int cdc_setup_data(const uint8_t s[8], const uint8_t *p, uint16_t n)
{
    if (s[1] != 0x20 || n != 7) return REQ_STALL;
    memcpy(line_coding, p, 7);
    return REQ_NODATA;
}

Формат линии — 7 байт: скорость (4 байта, младшим вперёд), стоп-биты, чётность, число бит. 00 C2 01 00 — это 0x0001C200 = 115 200. За нашим портом нет UART, поэтому скорость мы только помним и возвращаем; на обмен она не влияет. Запрос всё равно принимаем и возвращаем то же в GET_LINE_CODING.

DTR — «порт открыт». SET_CONTROL_LINE_STATE несёт в wValue два бита: бит 0 — DTR, бит 1 — RTS (о них со стороны программы на ПК — 115б). Терминалы при открытии порта обычно ставят DTR. Для прибора это единственный признак, что его текст кто-то читает:

OUT  6 байт "help\r\n": ACK
порт не открыт (DTR = 0): пакеты IN —, всего 0 байт
  выброшено байт: 50
SETUP A1 21 00 00 00 00 07 00  GET_LINE_CODING: IN 7 = 7 байт
  00 C2 01 00 00 00 08 -> 115200 бит/с, 8 бит
SETUP 21 20 00 00 00 00 07 00  SET_LINE_CODING: OUT 80 25 00 00 00 00 08, состояние принято
  скорость в устройстве: 9600
SETUP 21 20 00 00 00 00 07 00  SET_LINE_CODING: OUT 00 C2 01 00 00 00 08, состояние принято
  скорость в устройстве: 115200
SETUP 21 22 03 00 00 00 00 00  SET_CONTROL_LINE_STATE(DTR|RTS): состояние принято
уведомление из точки 0x81, 10 байт: A1 20 00 00 00 00 02 00 03 00
приветствие: пакеты IN 31, всего 31 байт
  > U585 console ready, type help

Уведомление A1 20 00 00 00 00 02 00 03 00 — это заголовок как у SETUP (тип 0xA1, код 0x20, интерфейс 0, длина 2) и два байта состояния: 0x03 — биты «несущая приёма» и «несущая передачи» (CDC_SERIAL_STATE_RX_CARRIER, CDC_SERIAL_STATE_TX_CARRIER).

Приём и передача. Приём — как в 20.7: в прерывании только сложить в кольцо. Если в кольце не осталось места на целый пакет, точку не взводят — хост получает NAK и ждёт:

int cdc_rx(const uint8_t *p, uint16_t n)
{
    for (uint16_t i = 0; i < n; i++) {
        rx_ring[rx_head % RX_RING] = p[i];
        rx_head++;
    }
    if (RX_RING - (rx_head - rx_tail) >= CDC_DATA_SIZE) return 1;
    rx_paused = 1;                          /* точку не взводим: хост получит NAK и подождёт */
    return 0;
}
uint32_t cdc_write(const char *s, uint32_t n)
{
    uint32_t i = 0;
    if (!dtr) {
        dropped += n;
        return 0;
    }
    while (i < n && tx_head - tx_tail < TX_RING) {
        tx_ring[tx_head % TX_RING] = (uint8_t)s[i++];
        tx_head++;
    }
    dropped += n - i;
    return i;
}
void cdc_poll(void)
{
    if (!usb_configured()) return;
    if (rx_paused && RX_RING - (rx_head - rx_tail) >= CDC_DATA_SIZE) {
        rx_paused = 0;
        drv_ep_rx_arm(EP_CDC_OUT);
    }
    if (banner_pending) {
        banner_pending = 0;
        cdc_write("U585 console ready, type help\r\n", 31);
    }
    if (notify_pending) {                   /* SERIAL_STATE: «несущая есть, готов» */
        static const uint8_t n[10] = { 0xA1, 0x20, 0, 0, IF_CDC_COMM, 0, 2, 0, 0x03, 0x00 };
        memcpy(notify_pkt, n, sizeof n);
        if (drv_ep_tx(EP_CDC_NOTIFY, notify_pkt, sizeof n) == 0) notify_pending = 0;
    }
    if (tx_busy || !dtr) return;
    uint16_t n = 0;
    while (n < CDC_DATA_SIZE && tx_tail != tx_head) tx_pkt[n++] = tx_ring[tx_tail++ % TX_RING];
    if (n == 0 && !tx_zlp) return;
    if (drv_ep_tx(EP_CDC_IN, tx_pkt, n) != 0) {     /* точка занята: вернуть байты в кольцо */
        tx_tail -= n;
        return;
    }
    tx_busy = 1;
    tx_zlp = n == CDC_DATA_SIZE && tx_tail == tx_head;     /* полный пакет был последним: нужен пустой */
}

Команды приходят как попало — модель режет status и log на три пакета, а ответ в 136 байт уходит тремя:

OUT  3 байт "sta": ACK
OUT  9 байт "tus\r\nlog\r": ACK
OUT  1 байт "\n": ACK
ответ на status и log: пакеты IN 64 64 8, всего 136 байт
  > cfg=1 dtr=1 rts=1 baud=115200 leds=0 reports=0 log=3
  > seq,time_s,temp_C,rh_pct
  > 0,0,-15.00,30.00
  > 1,10,-13.69,30.71
  > 2,20,-12.38,31.42
OUT  8 байт "reboot\r\n": ACK
незнакомая команда: пакеты IN 21, всего 21 байт
  > ERR unknown command
ровно 64 байта: пакеты IN 64 0, всего 64 байт
130 байт: пакеты IN 64 64 2, всего 130 байт
главный цикл стоит, хост шлёт 3 пакета по 64 байта: принято 2, NAK 1
главный цикл прошёл: повтор третьего пакета — ACK
SETUP 21 22 00 00 00 00 00 00  SET_CONTROL_LINE_STATE(0): состояние принято
  порт закрыт с 100 байтами в очереди: выброшено 100
после закрытия: пакеты IN —, всего 0 байт
SETUP 21 22 01 00 00 00 00 00  SET_CONTROL_LINE_STATE(DTR): состояние принято
уведомление: 10 байт
порт открыт снова: пакеты IN 31, всего 31 байт
  > U585 console ready, type help

Три вещи из этого прогона:

Консоль собирает строки по \r или \n и выполняет:

void console_poll(void)
{
    static char line[64];
    static uint8_t pos;
    int ch;
    while ((ch = cdc_getc()) >= 0) {
        if (ch == '\r' || ch == '\n') {
            line[pos] = 0;
            pos = 0;
            run(line);
        } else if (pos + 1u < sizeof line) {
            line[pos++] = (char)ch;         /* длинная строка обрезается */
        }
    }
}
static void run(char *line)
{
    if (strcmp(line, "status") == 0) {
        cmd_status();
    } else if (strcmp(line, "log") == 0) {
        cmd_log();
    } else if (strncmp(line, "macro ", 6) == 0) {    /* что набирает кнопка */
        button_macro_set(line + 6);
        put("OK macro\r\n");
    } else if (strncmp(line, "type ", 5) == 0) {     /* набрать текст клавиатурой сейчас */
        kbd_type(line + 5);
        put("OK type\r\n");
    } else if (strcmp(line, "help") == 0) {
        put("status | log | macro <text> | type <text> | help\r\n");
    } else if (line[0]) {
        put("ERR unknown command\r\n");
    }
}

198ш.5 HID: кнопка печатает строку

Отчёт загрузочной клавиатуры — 8 байт: модификаторы, резерв, шесть кодов (20в.4). Чтобы «напечатать» символ, шлют отчёт с его кодом, потом отчёт без него. Символ в код переводит таблица раскладки США — та же, что в 198ч, только в обратную сторону:

int kbd_lookup(char ch, uint8_t *usage, uint8_t *mods)
{
    if (ch == '\n') ch = '\r';
    for (uint8_t i = 0; i < sizeof plain - 1; i++) {
        if (i == 0x32u - 0x04u) continue;   /* клавиши 0x32 на клавиатуре США нет */
        if (plain[i] == ch) { *usage = (uint8_t)(0x04u + i); *mods = 0; return 1; }
        if (shifted[i] == ch) { *usage = (uint8_t)(0x04u + i); *mods = 0x02; return 1; }  /* левый Shift */
    }
    return 0;
}

Когда нужно отпускание. Хост считает символом код, которого не было в прошлом отчёте (198ч.3). Два одинаковых кода подряд без отпускания между ними — для хоста одна долгая клавиша. Одинаковый код — это не только «ll», но и «5» с «%»: это одна клавиша, разница только в Shift.

int kbd_next_report(uint8_t r[8])
{
    uint8_t usage, mods;
    memset(r, 0, 8);
    while (type_tail != type_head) {
        if (!kbd_lookup(type_ring[type_tail % TYPE_RING], &usage, &mods)) {
            type_tail++;                    /* такого символа на клавиатуре нет — пропуск */
            continue;
        }
        if (held && (!roll || held == usage)) {
            held = 0;                       /* отчёт «всё отпущено», символ остаётся в очереди */
            return 1;
        }
        r[0] = mods;
        r[2] = usage;
        held = usage;
        type_tail++;
        return 1;
    }
    if (held) {                             /* текст кончился: отпустить последнюю клавишу */
        held = 0;
        return 1;
    }
    return 0;
}

Отчёт уходит, когда точка свободна; хост забирает по одному отчёту за опрос, раз в bInterval = 10 мс:

void hid_poll(void)
{
    if (!usb_configured() || in_busy) return;
    if (!kbd_next_report(report)) return;
    if (drv_ep_tx(EP_HID_IN, report, sizeof report) == 0) {
        in_busy = 1;
        sent++;
        memcpy(last, report, sizeof last);
    }
}

Кнопка. USER на PC13, нажатая даёт единицу (так её читает драйвер ввода-вывода пакета платы: BSP_PB_GetState(BUTTON_USER) == 1U). Антидребезг — 20 мс подряд одного уровня, набор — по фронту нажатия:

void button_tick(uint8_t pressed)
{
    if (pressed == btn_state) {
        btn_count = 0;
        return;
    }
    if (++btn_count < DEBOUNCE_MS) return;
    btn_state = pressed;
    btn_count = 0;
    if (pressed) kbd_type(macro);
}

Прогон: дребезг 6 мс, кнопку держат 200 мс. Макрос по умолчанию — «Hello, U585!» и Enter, 13 символов:

R 02 00 0B 00 00 00 00 00
R 00 00 00 00 00 00 00 00
R 00 00 08 00 00 00 00 00
R 00 00 00 00 00 00 00 00
R 00 00 0F 00 00 00 00 00

…и так далее, 26 отчётов. Первый — 02 00 0B: левый Shift и код 0x0B, «H».

кнопка, отпускание после каждого символа: отчётов 26, хост набрал «Hello, U585!<Enter>»
  кнопка: дребезг 6 мс, удержание 200 мс — макрос набран 1 раз, отчётов отправлено 26
без отпусканий (13 отчётов): хост набрал «Helo, U585!<Enter>»

Последняя строка — что было бы, если бы прошивка слала одни нажатия: «ll» слилось в одну «l». Другой путь — режим kbd_set_roll(1): отпускать только там, где без этого нельзя:

R 00 00 0F 00 00 00 00 00
R 00 00 00 00 00 00 00 00
R 00 00 0F 00 00 00 00 00

— так выглядит «ll» в обоих режимах, а всего отчётов выходит 15 вместо 26: 13 нажатий, одно отпускание между «l» и «l» и одно в конце.

тот же текст, отпускание только где нужно: отчётов 15, хост набрал «Hello, U585!<Enter>»

При опросе раз в 10 мс это 150 мс против 260 мс на строку. По умолчанию режим выключен: в нём Shift меняется в том же отчёте, что и клавиша, и как это примет каждая система, на модели не проверить.

Макрос, где нужны все отпускания, — «55%»: три символа на одной клавише. Его задали из COM-порта и набрали кнопкой; режим тот же, экономный:

OUT 11 байт "macro 55%\r\n": ACK
ответ: пакеты IN 10, всего 10 байт
  > OK macro
R 00 00 22 00 00 00 00 00
R 00 00 00 00 00 00 00 00
R 00 00 22 00 00 00 00 00
R 00 00 00 00 00 00 00 00
R 02 00 22 00 00 00 00 00
R 00 00 00 00 00 00 00 00
макрос «55%» с кнопки: отчётов 6, хост набрал «55%»

Две функции работают вместе: команда type пришла по COM-порту, а текст ушёл клавиатурой.

OUT 11 байт "type Ab1!\r\n": ACK
ответ: пакеты IN 9, всего 9 байт
  > OK type
команда type из COM-порта: отчётов 8, хост набрал «Ab1!»

Запросы класса HID. Хост клавиатуры шлёт SET_IDLE с нулём («отчёт только при изменении»), может спросить текущий отчёт и зажечь светодиоды:

int hid_setup(const uint8_t s[8], const uint8_t **data, uint16_t *len)
{
    uint16_t wlength = (uint16_t)(s[6] | s[7] << 8);
    switch (s[1]) {
    case 0x01:                              /* GET_REPORT: входной отчёт (тип 1) — текущее состояние */
        if (s[0] != 0xA1 || s[3] != 1) return REQ_STALL;
        *data = last;
        *len = sizeof last;
        return REQ_IN;
    case 0x02:                              /* GET_IDLE */
        if (s[0] != 0xA1) return REQ_STALL;
        *data = &idle_rate;
        *len = 1;
        return REQ_IN;
    case 0x03:                              /* GET_PROTOCOL */
        if (s[0] != 0xA1) return REQ_STALL;
        *data = &protocol;
        *len = 1;
        return REQ_IN;
    case 0x09:                              /* SET_REPORT: выходной отчёт (тип 2) — светодиоды, 1 байт */
        return (s[0] == 0x21 && s[3] == 2 && wlength == 1) ? REQ_OUT : REQ_STALL;
    case 0x0A:                              /* SET_IDLE: старший байт wValue — период в единицах 4 мс */
        if (s[0] != 0x21) return REQ_STALL;
        idle_rate = s[3];
        return REQ_NODATA;
    case 0x0B:                              /* SET_PROTOCOL: 0 — загрузочный, 1 — отчётов */
        if (s[0] != 0x21 || s[2] > 1) return REQ_STALL;
        protocol = s[2];
        return REQ_NODATA;
    default:
        return REQ_STALL;
    }
}
SETUP 21 09 00 02 02 00 01 00  SET_REPORT(светодиоды): OUT 02, состояние принято
SETUP 21 0B 00 00 02 00 00 00  SET_PROTOCOL(0): состояние принято
SETUP A1 03 00 00 02 00 01 00  GET_PROTOCOL: IN 1 = 1 байт
  протокол 0
OUT  8 байт "status\r\n": ACK
ответ: пакеты IN 55, всего 55 байт
  > cfg=1 dtr=1 rts=0 baud=115200 leds=2 reports=55 log=3

leds=2 — бит Caps Lock. У клавиатуры без точки OUT светодиоды приходят запросом SET_REPORT по точке 0 (20в.1). reports=55 — все отчёты прогона: 26 + 15 + 6 + 8.

Неверные запросы получают отказ, а остановку точки хост ставит и снимает стандартными запросами:

SETUP 81 06 00 22 00 00 3F 00  GET_DESCRIPTOR(HID REPORT) к интерфейсу 0: STALL
SETUP 21 20 00 00 00 00 06 00  SET_LINE_CODING, 6 байт: STALL
SETUP 21 23 00 00 00 00 00 00  SEND_BREAK: STALL
SETUP 21 20 00 00 02 00 07 00  SET_LINE_CODING к интерфейсу HID: STALL
SETUP 02 03 00 00 83 00 00 00  SET_FEATURE(ENDPOINT_HALT, 0x83): состояние принято
SETUP 82 00 00 00 83 00 02 00  GET_STATUS(точка 0x83): IN 2 = 2 байт
  01 00, точка остановлена: 1
SETUP 02 01 00 00 83 00 00 00  CLEAR_FEATURE(ENDPOINT_HALT, 0x83): состояние принято
SETUP 82 00 00 00 83 00 02 00  GET_STATUS(точка 0x83): IN 2 = 2 байт
  00 00, точка остановлена: 0
SETUP 82 00 00 00 84 00 02 00  GET_STATUS(точка 0x84): STALL
сброс шины: конфигурация 0, DTR 0, точка 0x82 открыта: 0

198ш.6 OTG_FS на U585: память FIFO, точки, 48 МГц и плата

Сколько точек. Ядро OTG_FS у STM32U575/585 — точка 0 и по пять точек IN и OUT (RM0456, 72.2.3; в таблице 740 — 6 двунаправленных точек с нулевой). Нам нужны три номера: 1 (уведомления), 2 (данные в обе стороны), 3 (HID).

Память FIFO. У ядра своя память пакетов — 1,25 КБ (72.11; в таблице 740 она округлена до 1,2 КБ), это 320 слов по 32 бита. Программа делит её сама: один общий приёмный FIFO и по одному передающему на каждую точку IN. Значения регистров после сброса для этого не годятся: в GRXFSIZ стоит 0x200 — 512 слов, больше всей памяти.

Приёмный FIFO по формуле из 72.11.3: (5 × число управляющих точек + 8) + (наибольший пакет / 4 + 1) + 2 × число точек OUT + 1. У нас точки OUT две — нулевая и данные CDC: 13 + 17 + 4 + 1 = 35 слов. С запасом на второй пакет, как советует RM0456, — 52. Берём 64. Передающий FIFO — не меньше пакета своей точки и не меньше 16 слов (поле INEPTXFD: «Minimum value is 16»).

void otg_fifo_setup(void)
{
    static const uint16_t tx[4] = { FIFO_TX0_WORDS, FIFO_TX1_WORDS, FIFO_TX2_WORDS, FIFO_TX3_WORDS };
    uint32_t start = FIFO_RX_WORDS;
    REG(R_GRXFSIZ) = FIFO_RX_WORDS;
    REG(R_DIEPTXF0) = (uint32_t)tx[0] << 16 | start;      /* глубина в старшей половине, начало в младшей */
    start += tx[0];
    for (uint32_t i = 1; i < 4; i++) {
        REG(R_DIEPTXF(i)) = (uint32_t)tx[i] << 16 | start;
        start += tx[i];
    }
}

Модель регистров: otg_u585.c собран на компьютере, блок OTG — обычная память, функции те же.

после сброса: GRXFSIZ 0x200, DIEPTXF0 0x02000200, DIEPTXF1 0x02000400 — конец TX1 на слове 1536 из 320
otg_fifo_setup(): GRXFSIZ 0x00000040, DIEPTXF0 0x00100040, DIEPTXF1 0x00100050, DIEPTXF2 0x00400060, DIEPTXF3 0x001000A0
  RX общий приёмный начало   0, глубина  64 слов =  256 байт (нужно не меньше 140)
  TX0 точка 0       начало  64, глубина  16 слов =   64 байт (нужно не меньше 64)
  TX1 уведомления CDC начало  80, глубина  16 слов =   64 байт (нужно не меньше 16)
  TX2 данные CDC   начало  96, глубина  64 слов =  256 байт (нужно не меньше 64)
  TX3 отчёты HID   начало 160, глубина  16 слов =   64 байт (нужно не меньше 8)
  занято 176 слов из 320 (704 байт из 1280), свободно 144 слов; встык без наложений: да
  RX по формуле RM0456: наименьший 35 слов, с запасом на два пакета 52 слов

Занято 176 слов из 320. Точке данных CDC отдано 64 слова — четыре пакета; остаток, 144 слова, можно отдать ей же, если понадобится скорость.

Точки. Открыть точку — записать в её регистр тип, размер пакета, номер FIFO и бит «активна», и разрешить её прерывание:

void drv_ep_open(uint8_t addr, uint8_t type, uint16_t mps)
{
    uint8_t n = addr & 0x0Fu;
    if (addr & 0x80u) {                 /* IN: свой FIFO с тем же номером, что у точки */
        REG(R_DIEPCTL(n)) = mps | (uint32_t)type << EPCTL_EPTYP_Pos | (uint32_t)n << EPCTL_TXFNUM_Pos
                          | EPCTL_SD0PID | EPCTL_USBAEP | EPCTL_SNAK;
        REG(R_DAINTMSK) |= 1u << n;
    } else {
        out_mps[n] = mps;
        REG(R_DOEPCTL(n)) = mps | (uint32_t)type << EPCTL_EPTYP_Pos | EPCTL_SD0PID | EPCTL_USBAEP | EPCTL_SNAK;
        REG(R_DAINTMSK) |= 1u << (16u + n);
    }
}
drv_ep_open(): DIEPCTL1 0x184C8010, DIEPCTL2 0x18888040, DIEPCTL3 0x18CC8008, DOEPCTL2 0x9C088040, DAINTMSK 0x0004000E
drv_ep_rx_arm(0x02): DOEPTSIZ2 0x00080040
drv_ep_tx(0x83, 8 байт): 0, DIEPTSIZ3 0x00080008, DIEPCTL3 0x9CCC8008, в окне FIFO 3 последнее слово 0x00000000
drv_ep_tx(0x83) повторно, пока EPENA стоит: -1
drv_set_address(5): DCFG 0x00000050

В DIEPCTL1 = 0x184C8010 читается: пакет 0x10, бит 15 — точка активна, тип 3 (прерывающая) в битах 19:18, FIFO номер 1 в битах 25:22. Биты SNAK и SD0PID (оба только для записи) в настоящем регистре читаются нулём; в модели это память, и они видны. Передача — один пакет:

int drv_ep_tx(uint8_t addr, const uint8_t *p, uint16_t len)
{
    uint8_t n = addr & 0x0Fu;
    if (REG(R_DIEPCTL(n)) & EPCTL_EPENA) return -1;       /* прошлый пакет ещё не ушёл */
    REG(R_DIEPTSIZ(n)) = 1u << TSIZ_PKTCNT_Pos | len;
    REG(R_DIEPCTL(n)) |= EPCTL_EPENA | EPCTL_CNAK;
    for (uint16_t i = 0; i < len; i += 4) {
        uint32_t w = p[i];
        if (i + 1u < len) w |= (uint32_t)p[i + 1] << 8;
        if (i + 2u < len) w |= (uint32_t)p[i + 2] << 16;
        if (i + 3u < len) w |= (uint32_t)p[i + 3] << 24;
        REG(R_DFIFO(n)) = w;
    }
    return 0;
}

Пока хост не забрал пакет, бит EPENA стоит, и следующий вызов вернёт −1 — на этом держатся tx_busy в cdc.c и in_busy в hidkbd.c.

Запуск ядра. Порядок тот же, что в HAL (USB_CoreInit, USB_SetCurrentMode, USB_DevInit): встроенный приёмопередатчик, сброс ядра, режим устройства, FIFO, маски прерываний, и только в конце — снять SDIS, то есть включить подтяжку D+:

void otg_init(void)
{
    REG(R_GAHBCFG) &= ~GAHBCFG_GINT;
    REG(R_GUSBCFG) |= GUSBCFG_PHYSEL;                     /* встроенный приёмопередатчик FS */
    while (!(REG(R_GRSTCTL) & GRSTCTL_AHBIDL)) { }
    REG(R_GRSTCTL) |= GRSTCTL_CSRST;                      /* сброс ядра */
    while (REG(R_GRSTCTL) & GRSTCTL_CSRST) { }
    REG(R_GCCFG) |= GCCFG_PWRDWN;                         /* 1 — приёмопередатчик включён */
    REG(R_GUSBCFG) = (REG(R_GUSBCFG) & ~GUSBCFG_TRDT_Msk) | GUSBCFG_FDMOD | 6u << GUSBCFG_TRDT_Pos;
    REG(R_DCTL) |= DCTL_SDIS;                             /* пока не готовы — подтяжка D+ снята */
    REG(R_GCCFG) &= ~GCCFG_VBDEN;                         /* VBUS не следим: считаем, что он есть */
    REG(R_GOTGCTL) |= GOTGCTL_BVALOEN | GOTGCTL_BVALOVAL;
    REG(R_PCGCCTL) = 0;
    REG(R_DCFG) |= DCFG_DSPD_FS;
    otg_fifo_setup();
    flush_fifos();
    REG(R_DIEPMSK) = EPINT_XFRC;
    REG(R_DOEPMSK) = EPINT_XFRC | EPINT_STUP;
    REG(R_DAINTMSK) = 0;
    REG(R_GINTSTS) = 0xBFFFFFFFu;
    REG(R_GINTMSK) = GINT_USBRST | GINT_ENUMDNE | GINT_RXFLVL | GINT_IEPINT | GINT_OEPINT;
    REG(R_GAHBCFG) |= GAHBCFG_GINT;
    REG(R_DCTL) &= ~DCTL_SDIS;                            /* подтяжка D+: хост видит устройство */
}

TRDT = 6 — значение HAL для частоты шины AHB от 32 МГц. Контроль VBUS выключен, как в примере пакета (vbus_sensing_enable = DISABLE): HAL в этом случае ставит те же BVALOEN и BVALOVAL. Смещения регистров и биты otg_regs.h при сборке сверяет с stm32u585xx.h файл otg_selfcheck.c — четырнадцать _Static_assert.

Приём: ядро кладёт в общий FIFO слово состояния и данные; SETUP и данные OUT различает поле PKTSTS:

static void rx_pop(void)
{
    uint32_t s = REG(R_GRXSTSP);
    uint32_t n = RXSTS_EPNUM(s), cnt = RXSTS_BCNT(s), sts = RXSTS_PKTSTS(s);
    if (sts != PKTSTS_OUT_DATA && sts != PKTSTS_SETUP_DATA) return;    /* «передача окончена» — без данных */
    uint8_t *dst = sts == PKTSTS_SETUP_DATA ? setup_pkt : rx_buf[n ? 1 : 0];
    for (uint32_t i = 0; i < cnt; i += 4) {
        uint32_t w = REG(R_DFIFO(0));
        for (uint32_t k = 0; k < 4 && i + k < cnt; k++)
            if (i + k < 64u) dst[i + k] = (uint8_t)(w >> (8 * k));
    }
    if (sts == PKTSTS_OUT_DATA) rx_len[n ? 1 : 0] = (uint16_t)cnt;
}
void otg_irq(void)
{
    uint32_t st = REG(R_GINTSTS) & REG(R_GINTMSK);
    if (st & GINT_USBRST) {
        REG(R_GINTSTS) = GINT_USBRST;
        REG(R_DCFG) &= ~DCFG_DAD_Msk;
        REG(R_DAINTMSK) = 1u | 1u << 16;                  /* точка 0 в обе стороны */
        usb_reset();
        ep0_out_arm();
    }
    if (st & GINT_ENUMDNE) {
        REG(R_GINTSTS) = GINT_ENUMDNE;
        REG(R_DIEPCTL(0)) &= ~0x3u;                       /* MPSIZ точки 0: 00 — 64 байта */
        REG(R_DCTL) |= DCTL_CGINAK;
    }
    while (REG(R_GINTSTS) & GINT_RXFLVL) rx_pop();
    if (st & GINT_OEPINT) {
        uint32_t eps = REG(R_DAINT) >> 16;
        for (uint8_t n = 0; n < 4; n++) {
            if (!(eps & 1u << n)) continue;
            uint32_t f = REG(R_DOEPINT(n));
            REG(R_DOEPINT(n)) = f;
            if (f & EPINT_STUP) {
                usb_setup(setup_pkt);
                ep0_out_arm();
            } else if (f & EPINT_XFRC) {
                if (n == 0) {
                    usb_ep0_out(rx_buf[0], rx_len[0]);
                    rx_len[0] = 0;
                    ep0_out_arm();
                } else if (usb_ep_out(n, rx_buf[1], rx_len[1])) {
                    drv_ep_rx_arm(n);
                }
            }
        }
    }
    if (st & GINT_IEPINT) {
        uint32_t eps = REG(R_DAINT) & 0xFFFFu;
        for (uint8_t n = 0; n < 4; n++) {
            if (!(eps & 1u << n)) continue;
            uint32_t f = REG(R_DIEPINT(n));
            REG(R_DIEPINT(n)) = f;
            if (!(f & EPINT_XFRC)) continue;
            if (n == 0) usb_ep0_in_done();
            else usb_ep_in_done(0x80u | n);
        }
    }
}

48 МГц без кварца. Оба примера устройства в пакете берут 48 МГц от HSI48 (RCC_CLK48CLKSOURCE_HSI48) и блок CRS не включают. HSI48 по таблице 84 DS13086 после заводской калибровки — 47,5…48,5 МГц при 30 °C, то есть ±1 %, а по температуре — от −3 до +2 %; USB требует ±0,25 % (раздел 20). Устройству помогает CRS: хост шлёт SOF раз в 1 мс, и блок подстраивает HSI48 по ним (20а.5). Источник метки «SOF от USB» — значение по умолчанию (RCC_CRS_SYNC_SOURCE_USB в HAL), остаётся включить счётчик и автоподстройку:

void usb_clock_init(void)
{
    RCC->CR |= RCC_CR_HSI48ON;                          /* свой генератор 48 МГц */
    while (!(RCC->CR & RCC_CR_HSI48RDY)) { }
    RCC->CCIPR1 &= ~RCC_CCIPR1_ICLKSEL;                 /* 00: 48 МГц для USB — от HSI48 */
    RCC->APB1ENR1 |= RCC_APB1ENR1_CRSEN;
    CRS->CFGR = (CRS->CFGR & ~CRS_CFGR_SYNCSRC) | CRS_CFGR_SYNCSRC_1;  /* метка — SOF от USB */
    CRS->CR |= CRS_CR_AUTOTRIMEN | CRS_CR_CEN;          /* подстройка HSI48 по кадрам хоста */

    RCC->AHB3ENR |= RCC_AHB3ENR_PWREN;
    PWR->SVMCR |= PWR_SVMCR_USV;                        /* VDDUSB есть: снять изоляцию выводов USB */

    RCC->AHB2ENR1 |= RCC_AHB2ENR1_GPIOAEN | RCC_AHB2ENR1_GPIOCEN | RCC_AHB2ENR1_OTGEN;
    GPIOA->AFR[1] = (GPIOA->AFR[1] & ~(0xFFu << 12)) | 0xAAu << 12;     /* PA11, PA12 — AF10 */
    GPIOA->OSPEEDR |= 0xFu << 22;
    GPIOA->MODER = (GPIOA->MODER & ~(0xFu << 22)) | 0xAu << 22;         /* альтернативная функция */
    GPIOC->MODER &= ~(3u << 26);                        /* PC13 — вход */
}

Бит USV в PWR_SVMCR сообщает кристаллу, что питание VDDUSB есть; HAL делает это функцией HAL_PWREx_EnableVddUSB. Системную частоту 160 МГц даёт SystemClock_Config примера, её мы не трогаем.

Разъём USB-C. Линии CC разъёма идут на PA15 и PB15 (UCPD1), пример оставляет их аналоговыми и UCPD не запускает. Питанием порта на плате ведает TCPP03 (198ч.2). В примерах устройства его драйвер не вызывается — вызовы стоят под #ifdef RTE_Drivers_USBH0. По комментарию драйвера состояние TCPP03 после включения — Hibernate: «Dead Batteries Enabled», режим для потребителя. То есть подтяжки Rd, по которым хост USB-C узнаёт устройство и подаёт VBUS (20б.2), даёт сам TCPP03. Так ли разведена плата, скажет схема MB1551; с ней раздел не сверялся.

Что нужно системе. Ничего ставить не надо. По описанию примера VirtualCOM, новые Windows сами заводят порт «USB Serial Device (COMx)»; в приложенном к примеру файле установки драйвер — usbser.sys, а функции составного устройства названы по номеру первого интерфейса: MI_00, MI_02. У нас MI_00 — COM-порт, MI_02 — клавиатура. В Linux порт берёт драйвер cdc_acm и даёт устройство ttyACM (строка cdc_acm 9-1:1.0: ttyACM1: USB ACM device — из описания примера Zephyr). Клавиатуру обе системы берут штатным драйвером HID. На настоящих системах наш прибор не проверялся.

Размеры — armclang 6.24, -Os, объектные файлы (без стартового кода и SystemClock_Config):

файл                код       RO       RW       ZI
usbdesc.c           204      232       25        0
usbcore.c          1094       64        0       92
cdc.c              1104      103        0     1260
hidkbd.c           1014      220       50      163
console.c           924       88        0      133
otg_u585.c         1300       88        0      148
main_u585.c         350       32        4        4
otg_selfcheck.c       0        0        0        0
всего              5990      827       79     1800
флеш-память (код + RO + RW): 6896 байт, ОЗУ (RW + ZI): 1879 байт

Из 1879 байт ОЗУ 1260 — кольца CDC: 128 на приём, 1024 на передачу и буферы пакетов.


198ш.7 Грабли

IAD без класса EF/02/01 или класс без IAD. Они ходят парой: и стек MDK, и betaflight ставят класс устройства EF/02/01 вместе с IAD перед первым интерфейсом функции CDC. Класс 0x02 годится только одиночному CDC.

wTotalLength не сошёлся. Добавили дескриптор и забыли длину. Хост читает ровно wTotalLength байт: лишнее отрежет, недостающее примет за мусор. Считать сумму на этапе компиляции, как _Static_assert в usbdesc.c.

Union и IAD говорят разное. Интерфейсы переставили (HID первым, как у betaflight), IAD поправили, а в Union и Call Management остались старые номера. Номера интерфейсов — в четырёх местах: IAD, Union, Call Management и сами интерфейсы.

Последний пакет вышел ровно 64 байта. Передачу кончает короткий пакет; за полным последним нужен пустой, иначе для хоста она не закончена.

Текст копится и вываливается при открытии порта. Прибор писал в очередь, не глядя на DTR. Смотреть на DTR и выбрасывать вывод, пока порт закрыт; но программа, которая открывает порт и не ставит DTR, тогда ничего не увидит — это надо знать.

Двойные буквы пропадают. Между одинаковыми кодами нет отчёта отпускания: «Hello» стало «Helo». То же с «5» и «%» — код один.

FIFO не поделены. Значения после сброса — по 512 слов на каждый при памяти в 320. Каждая точка IN — свой FIFO не меньше 16 слов, все встык, сумма не больше 320.

FIFO меньше 16 слов. Драйвер USBD_STM32.c из CMSIS-Driver_STM32 1.3.0 задаёт передающий FIFO как (ep_max_packet_size + 3U) / 4U слов: для точки HID примера с пакетом 4 байта это 1 слово. Комментарий в самой HAL_PCDEx_SetTxFiFo говорит «TXn min size = 16 words», и RM0456 — тоже. Как ведёт себя ядро с таким FIFO, мы не проверяли; свои размеры держим не меньше 16.

48 МГц от HSI48 без CRS. ±1 % при 30 °C и до −3 % по температуре против нужных ±0,25 %. Примеры пакета CRS не включают.

Подтяжка D+ раньше времени. Пока прошивка не готова отвечать, хост не должен видеть устройство. HAL делает так же: USB_DevInit ставит SDIS, снимает его USB_DevConnect — последним действием запуска.

Серийный номер одинаков у всех приборов. Строка зашита в прошивку (в примере — «0001A0000000»): у двух приборов она совпадёт. Номер брать из UID_BASE.


198ш.8 Итоги

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

usbdesc.c — дескрипторы составного устройства CDC ACM + HID.

/* usbdesc.c — дескрипторы составного устройства CDC ACM + HID. Раздел 198ш. */
#include "usbdev.h"

const uint8_t usb_device_desc[18] = {
    18, 0x01,                   /* bLength, DEVICE */
    0x00, 0x02,                 /* bcdUSB 2.00 */
    0xEF, 0x02, 0x01,           /* класс «разное», подкласс 2, протокол 1: смотри IAD */
    EP0_SIZE,                   /* bMaxPacketSize0 */
    USB_VID & 0xFF, USB_VID >> 8,
    USB_PID & 0xFF, USB_PID >> 8,
    0x00, 0x01,                 /* bcdDevice 1.00 */
    1, 2, 3,                    /* строки: изготовитель, изделие, серийный номер */
    1                           /* одна конфигурация */
};

const uint8_t usb_config_desc[CONFIG_DESC_SIZE] = {
    /* конфигурация */
    9, 0x02, CONFIG_DESC_SIZE, 0x00,    /* wTotalLength = 100 */
    3,                          /* bNumInterfaces */
    1,                          /* bConfigurationValue */
    0,                          /* iConfiguration */
    0x80,                       /* bmAttributes: питание от шины */
    50,                         /* bMaxPower: 50 x 2 мА = 100 мА */
    /* IAD: интерфейсы 0 и 1 — одна функция CDC */
    8, 0x0B, IF_CDC_COMM, 2,    /* bFirstInterface, bInterfaceCount */
    0x02, 0x02, 0x01,           /* класс CDC, подкласс ACM, протокол AT */
    0,                          /* iFunction */
    /* интерфейс 0: управление CDC */
    9, 0x04, IF_CDC_COMM, 0, 1, /* одна точка */
    0x02, 0x02, 0x01, 0,        /* CDC, ACM, AT */
    5, 0x24, 0x00, 0x10, 0x01,  /* Header: bcdCDC 1.10 */
    5, 0x24, 0x01, 0x00, IF_CDC_DATA,   /* Call Management: вызовами не управляем */
    4, 0x24, 0x02, 0x02,        /* ACM: линия и её состояние (SET/GET_LINE_CODING, SERIAL_STATE) */
    5, 0x24, 0x06, IF_CDC_COMM, IF_CDC_DATA,    /* Union: главный 0, подчинённый 1 */
    7, 0x05, EP_CDC_NOTIFY, EP_TYPE_INTR, CDC_NOTIFY_SIZE, 0x00, 16,   /* уведомления, опрос 16 мс */
    /* интерфейс 1: данные CDC */
    9, 0x04, IF_CDC_DATA, 0, 2, /* две точки */
    0x0A, 0x00, 0x00, 0,        /* CDC Data */
    7, 0x05, EP_CDC_OUT, EP_TYPE_BULK, CDC_DATA_SIZE, 0x00, 0,
    7, 0x05, EP_CDC_IN, EP_TYPE_BULK, CDC_DATA_SIZE, 0x00, 0,
    /* интерфейс 2: HID, загрузочная клавиатура */
    9, 0x04, IF_HID, 0, 1,      /* одна точка */
    0x03, 0x01, 0x01, 0,        /* HID, подкласс 1 (загрузочный), протокол 1 (клавиатура) */
    9, 0x21, 0x11, 0x01, 0x00,  /* HID: bcdHID 1.11, страна 0 */
    1, 0x22, HID_REPORT_DESC_SIZE, 0x00,    /* один дескриптор отчёта, 63 байта */
    7, 0x05, EP_HID_IN, EP_TYPE_INTR, HID_REPORT_SIZE, 0x00, HID_INTERVAL_MS
};

const uint8_t usb_hid_report_desc[HID_REPORT_DESC_SIZE] = {
    0x05, 0x01,                 /* Usage Page (Generic Desktop) */
    0x09, 0x06,                 /* Usage (Keyboard) */
    0xA1, 0x01,                 /* Collection (Application) */
    0x05, 0x07,                 /*   Usage Page (Keyboard) */
    0x19, 0xE0, 0x29, 0xE7,     /*   Usage Min/Max: восемь модификаторов */
    0x15, 0x00, 0x25, 0x01,     /*   Logical Min/Max: 0 … 1 */
    0x75, 0x01, 0x95, 0x08,     /*   Report Size 1, Report Count 8 */
    0x81, 0x02,                 /*   Input (Data, Var, Abs): байт 0 */
    0x95, 0x01, 0x75, 0x08,     /*   Report Count 1, Report Size 8 */
    0x81, 0x01,                 /*   Input (Const): байт 1, резерв */
    0x95, 0x05, 0x75, 0x01,     /*   Report Count 5, Report Size 1 */
    0x05, 0x08,                 /*   Usage Page (LEDs) */
    0x19, 0x01, 0x29, 0x05,     /*   Usage Min/Max: пять светодиодов */
    0x91, 0x02,                 /*   Output (Data, Var, Abs) */
    0x95, 0x01, 0x75, 0x03,     /*   Report Count 1, Report Size 3 */
    0x91, 0x01,                 /*   Output (Const): добивка до байта */
    0x95, 0x06, 0x75, 0x08,     /*   Report Count 6, Report Size 8 */
    0x15, 0x00, 0x25, 0x65,     /*   Logical Min/Max: 0 … 101 */
    0x05, 0x07,                 /*   Usage Page (Keyboard) */
    0x19, 0x00, 0x29, 0x65,     /*   Usage Min/Max: коды клавиш 0 … 101 */
    0x81, 0x00,                 /*   Input (Data, Array): байты 2…7 */
    0xC0                        /* End Collection */
};

_Static_assert(sizeof usb_config_desc == 9 + 8 + (9 + 5 + 5 + 4 + 5 + 7) + (9 + 7 + 7) + (9 + 9 + 7),
               "wTotalLength: конфигурация + IAD + CDC (два интерфейса) + HID");

static char serial[25] = "000000000000000000000000";

void usb_serial_from_uid(const uint32_t uid[3])
{
    static const char hex[] = "0123456789ABCDEF";
    for (int w = 0; w < 3; w++)
        for (int i = 0; i < 8; i++)
            serial[w * 8 + i] = hex[(uid[w] >> (28 - 4 * i)) & 0xFu];
}

/* Строка ASCII -> дескриптор STRING в UTF-16LE: 2 байта заголовка и по 2 байта на знак */
uint16_t usb_string_desc(uint8_t index, uint8_t *buf)
{
    static const char *const text[] = { "STM32 Handbook", "U585 COM + keyboard", serial };
    if (index == 0) {                       /* список языков: 0x0409, английский (США) */
        buf[0] = 4; buf[1] = 0x03; buf[2] = 0x09; buf[3] = 0x04;
        return 4;
    }
    if (index > 3) return 0;
    const char *s = text[index - 1];
    uint16_t n = 2;
    while (*s && n + 2u <= EP0_SIZE) {
        buf[n++] = (uint8_t)*s++;
        buf[n++] = 0;
    }
    buf[0] = (uint8_t)n;
    buf[1] = 0x03;
    return n;
}

usbcore.c — точка 0: стандартные запросы и раздача запросов классам.

/* usbcore.c — управляющие передачи точки 0: стандартные запросы и раздача запросов классам.
   Не знает регистров: пакеты отдаёт и получает через drv_*(). Раздел 198ш. */
#include "usbdev.h"
#include <string.h>

static struct {
    const uint8_t *tx;          /* что ещё осталось отдать в стадии данных IN */
    uint16_t tx_left;
    uint8_t tx_zlp;             /* после данных нужен пустой пакет */
    uint8_t setup[8];           /* запрос, у которого идёт стадия данных OUT */
    uint16_t rx_len, rx_want;
    uint8_t buf[EP0_SIZE];      /* короткие ответы и принятые данные */
    uint8_t config;             /* 0 — не настроено, 1 — наша конфигурация */
    uint8_t halt_cdc_out, halt_cdc_in, halt_hid_in, halt_notify;
} c;

uint8_t usb_configured(void) { return c.config; }

static void ep0_next(void)
{
    uint16_t n = c.tx_left < EP0_SIZE ? c.tx_left : EP0_SIZE;
    drv_ep0_tx(c.tx, n);
    c.tx += n;
    c.tx_left -= n;
}

/* Стадия данных IN: не больше wLength; пустой пакет в конце, если отдали меньше, чем просили,
   а последний пакет вышел полным — иначе хост не узнает, что данные кончились */
static void ep0_send(const uint8_t *p, uint16_t len, uint16_t wlength)
{
    if (len > wlength) len = wlength;
    c.tx = p;
    c.tx_left = len;
    c.tx_zlp = len < wlength && len != 0 && len % EP0_SIZE == 0;
    ep0_next();
}

static void ep0_status(void) { c.tx_left = 0; c.tx_zlp = 0; drv_ep0_tx(0, 0); }

void usb_ep0_in_done(void)
{
    if (c.tx_left) ep0_next();
    else if (c.tx_zlp) { c.tx_zlp = 0; drv_ep0_tx(0, 0); }
}

static uint8_t *halt_flag(uint8_t ep)
{
    switch (ep) {
    case EP_CDC_OUT: return &c.halt_cdc_out;
    case EP_CDC_IN: return &c.halt_cdc_in;
    case EP_HID_IN: return &c.halt_hid_in;
    case EP_CDC_NOTIFY: return &c.halt_notify;
    default: return 0;
    }
}

static void set_config(uint8_t v)
{
    if (c.config) {
        drv_ep_close(EP_CDC_NOTIFY);
        drv_ep_close(EP_CDC_OUT);
        drv_ep_close(EP_CDC_IN);
        drv_ep_close(EP_HID_IN);
    }
    c.config = v;
    c.halt_cdc_out = c.halt_cdc_in = c.halt_hid_in = c.halt_notify = 0;
    cdc_reset();
    hid_reset();
    if (v) {
        drv_ep_open(EP_CDC_NOTIFY, EP_TYPE_INTR, CDC_NOTIFY_SIZE);
        drv_ep_open(EP_CDC_OUT, EP_TYPE_BULK, CDC_DATA_SIZE);
        drv_ep_open(EP_CDC_IN, EP_TYPE_BULK, CDC_DATA_SIZE);
        drv_ep_open(EP_HID_IN, EP_TYPE_INTR, HID_REPORT_SIZE);
        drv_ep_rx_arm(EP_CDC_OUT);
    }
}

void usb_reset(void)
{
    set_config(0);
    c.tx_left = 0;
    c.tx_zlp = 0;
    c.rx_want = 0;
}

static int get_descriptor(uint8_t type, uint8_t index, uint16_t windex, const uint8_t **p, uint16_t *len)
{
    switch (type) {
    case 0x01: *p = usb_device_desc; *len = sizeof usb_device_desc; return 1;
    case 0x02: *p = usb_config_desc; *len = sizeof usb_config_desc; return index == 0;
    case 0x03: *p = c.buf; *len = usb_string_desc(index, c.buf); return *len != 0;
    case 0x21: *p = usb_config_desc + HID_DESC_OFFSET; *len = 9; return windex == IF_HID;    /* HID внутри конфигурации */
    case 0x22: *p = usb_hid_report_desc; *len = sizeof usb_hid_report_desc; return windex == IF_HID;
    default: return 0;          /* 0x06 DEVICE_QUALIFIER: устройство только полной скорости — отказ */
    }
}

static int standard_request(const uint8_t s[8], const uint8_t **p, uint16_t *len)
{
    uint8_t rcpt = s[0] & 0x1Fu, req = s[1];
    uint16_t wvalue = (uint16_t)(s[2] | s[3] << 8), windex = (uint16_t)(s[4] | s[5] << 8);
    uint8_t *h;
    switch (req) {
    case 0x00:                  /* GET_STATUS */
        if (rcpt == 2 && (h = halt_flag((uint8_t)windex)) != 0) c.buf[0] = *h;
        else if (rcpt == 2 && (windex & 0x7Fu) != 0) return REQ_STALL;
        else c.buf[0] = 0;      /* питание от шины, будить хост не умеем */
        c.buf[1] = 0;
        *p = c.buf; *len = 2;
        return REQ_IN;
    case 0x01: case 0x03:       /* CLEAR_FEATURE, SET_FEATURE: только ENDPOINT_HALT */
        if (rcpt != 2 || wvalue != 0 || (h = halt_flag((uint8_t)windex)) == 0) return REQ_STALL;
        *h = req == 0x03;
        drv_ep_stall((uint8_t)windex, *h);
        return REQ_NODATA;
    case 0x05:                  /* SET_ADDRESS: у OTG адрес пишут сразу, до стадии состояния */
        drv_set_address((uint8_t)(wvalue & 0x7Fu));
        return REQ_NODATA;
    case 0x06:                  /* GET_DESCRIPTOR */
        return get_descriptor(s[3], s[2], windex, p, len) ? REQ_IN : REQ_STALL;
    case 0x08:                  /* GET_CONFIGURATION */
        c.buf[0] = c.config;
        *p = c.buf; *len = 1;
        return REQ_IN;
    case 0x09:                  /* SET_CONFIGURATION */
        if (wvalue > 1) return REQ_STALL;
        set_config((uint8_t)wvalue);
        return REQ_NODATA;
    case 0x0A:                  /* GET_INTERFACE: запасных настроек нет */
        if (!c.config || windex > IF_HID) return REQ_STALL;
        c.buf[0] = 0;
        *p = c.buf; *len = 1;
        return REQ_IN;
    case 0x0B:                  /* SET_INTERFACE */
        return (c.config && windex <= IF_HID && wvalue == 0) ? REQ_NODATA : REQ_STALL;
    default:
        return REQ_STALL;
    }
}

void usb_setup(const uint8_t s[8])
{
    const uint8_t *p = 0;
    uint16_t len = 0, wlength = (uint16_t)(s[6] | s[7] << 8);
    uint8_t type = (s[0] >> 5) & 3u, iface = s[4];
    int r = REQ_STALL;

    c.rx_want = 0;
    if (type == 0) {
        r = standard_request(s, &p, &len);
    } else if (type == 1 && (s[0] & 0x1Fu) == 1 && c.config) {  /* запрос класса к интерфейсу */
        if (iface == IF_CDC_COMM) r = cdc_setup(s, &p, &len);
        else if (iface == IF_HID) r = hid_setup(s, &p, &len);
    }
    if (r == REQ_IN && (s[0] & 0x80u)) {
        ep0_send(p, len, wlength);
    } else if (r == REQ_OUT && !(s[0] & 0x80u) && wlength && wlength <= sizeof c.buf) {
        memcpy(c.setup, s, 8);              /* данные придут следующими пакетами OUT */
        c.rx_len = 0;
        c.rx_want = wlength;
    } else if (r == REQ_NODATA) {
        ep0_status();
    } else {
        drv_ep0_stall();
    }
}

void usb_ep0_out(const uint8_t *p, uint16_t n)
{
    if (!c.rx_want) return;                 /* пустой пакет — стадия состояния после IN */
    if (n > c.rx_want - c.rx_len) n = (uint16_t)(c.rx_want - c.rx_len);
    memcpy(c.buf + c.rx_len, p, n);
    c.rx_len = (uint16_t)(c.rx_len + n);
    if (c.rx_len < c.rx_want) return;
    c.rx_want = 0;
    int r = c.setup[4] == IF_CDC_COMM ? cdc_setup_data(c.setup, c.buf, c.rx_len)
                                      : hid_setup_data(c.setup, c.buf, c.rx_len);
    if (r == REQ_NODATA) ep0_status();
    else drv_ep0_stall();
}

int usb_ep_out(uint8_t addr, const uint8_t *p, uint16_t n)
{
    return addr == EP_CDC_OUT ? cdc_rx(p, n) : 1;
}

void usb_ep_in_done(uint8_t addr)
{
    if (addr == EP_HID_IN) hid_in_done();
    else cdc_in_done(addr);
}

cdc.c — класс CDC ACM: запросы линии, SERIAL_STATE, кольца.

/* cdc.c — класс CDC ACM: запросы линии, уведомление SERIAL_STATE, кольца приёма и передачи.
   Раздел 198ш. */
#include "usbdev.h"
#include <string.h>

#define RX_RING 128u            /* степени двойки */
#define TX_RING 1024u

static uint8_t line_coding[7];  /* dwDTERate (4, младшим вперёд), bCharFormat, bParityType, bDataBits */
static uint8_t dtr, rts;
static uint8_t rx_ring[RX_RING], tx_ring[TX_RING];
static volatile uint32_t rx_head, rx_tail, tx_head, tx_tail;
static uint8_t tx_pkt[CDC_DATA_SIZE], notify_pkt[10];
static uint8_t tx_busy, tx_zlp, rx_paused, notify_pending, banner_pending;
static uint32_t dropped;

uint8_t cdc_dtr(void) { return dtr; }
uint8_t cdc_rts(void) { return rts; }
uint32_t cdc_dropped(void) { return dropped; }
uint32_t cdc_baud(void)
{
    return line_coding[0] | line_coding[1] << 8 | line_coding[2] << 16 | (uint32_t)line_coding[3] << 24;
}

void cdc_reset(void)
{
    static const uint8_t dflt[7] = { 0x00, 0xC2, 0x01, 0x00, 0, 0, 8 };    /* 115200, 1 стоп, без чётности, 8 бит */
    memcpy(line_coding, dflt, 7);
    dtr = rts = 0;
    rx_head = rx_tail = tx_head = tx_tail = 0;
    tx_busy = tx_zlp = rx_paused = notify_pending = banner_pending = 0;
}

/* Запросы класса к интерфейсу 0. Скорость и формат мы только помним: провода UART за портом нет */
int cdc_setup(const uint8_t s[8], const uint8_t **data, uint16_t *len)
{
    uint16_t wvalue = (uint16_t)(s[2] | s[3] << 8), wlength = (uint16_t)(s[6] | s[7] << 8);
    switch (s[1]) {
    case 0x20:                              /* SET_LINE_CODING: 7 байт придут стадией данных */
        return (s[0] == 0x21 && wlength == 7) ? REQ_OUT : REQ_STALL;
    case 0x21:                              /* GET_LINE_CODING */
        if (s[0] != 0xA1) return REQ_STALL;
        *data = line_coding;
        *len = 7;
        return REQ_IN;
    case 0x22: {                            /* SET_CONTROL_LINE_STATE: бит 0 — DTR, бит 1 — RTS */
        uint8_t was = dtr;
        if (s[0] != 0x21) return REQ_STALL;
        dtr = wvalue & 1u;
        rts = (wvalue >> 1) & 1u;
        if (dtr && !was)                    /* терминал открыл порт: уведомление и приветствие */
            notify_pending = banner_pending = 1;    /* сами тексты уйдут из главного цикла */
        if (!dtr && was) {                  /* порт закрыт: недосланное никому не нужно */
            dropped += tx_head - tx_tail;
            tx_tail = tx_head;
            tx_zlp = 0;
        }
        return REQ_NODATA;
    }
    default:                                /* SEND_BREAK и прочее не объявлено в bmCapabilities */
        return REQ_STALL;
    }
}

int cdc_setup_data(const uint8_t s[8], const uint8_t *p, uint16_t n)
{
    if (s[1] != 0x20 || n != 7) return REQ_STALL;
    memcpy(line_coding, p, 7);
    return REQ_NODATA;
}

/* Пакет из точки OUT (прерывание): только сложить в кольцо. 0 — следующий пакет пока брать некуда */
int cdc_rx(const uint8_t *p, uint16_t n)
{
    for (uint16_t i = 0; i < n; i++) {
        rx_ring[rx_head % RX_RING] = p[i];
        rx_head++;
    }
    if (RX_RING - (rx_head - rx_tail) >= CDC_DATA_SIZE) return 1;
    rx_paused = 1;                          /* точку не взводим: хост получит NAK и подождёт */
    return 0;
}

int cdc_getc(void)
{
    if (rx_tail == rx_head) return -1;
    return rx_ring[rx_tail++ % RX_RING];
}

/* Вывод консоли. Пока порт не открыт (DTR = 0), текст некому читать — он считается и выбрасывается */
uint32_t cdc_write(const char *s, uint32_t n)
{
    uint32_t i = 0;
    if (!dtr) {
        dropped += n;
        return 0;
    }
    while (i < n && tx_head - tx_tail < TX_RING) {
        tx_ring[tx_head % TX_RING] = (uint8_t)s[i++];
        tx_head++;
    }
    dropped += n - i;
    return i;
}

void cdc_in_done(uint8_t addr)
{
    if (addr == EP_CDC_IN) tx_busy = 0;
}

/* Главный цикл: взвести приём, отдать уведомление и очередной пакет данных */
void cdc_poll(void)
{
    if (!usb_configured()) return;
    if (rx_paused && RX_RING - (rx_head - rx_tail) >= CDC_DATA_SIZE) {
        rx_paused = 0;
        drv_ep_rx_arm(EP_CDC_OUT);
    }
    if (banner_pending) {
        banner_pending = 0;
        cdc_write("U585 console ready, type help\r\n", 31);
    }
    if (notify_pending) {                   /* SERIAL_STATE: «несущая есть, готов» */
        static const uint8_t n[10] = { 0xA1, 0x20, 0, 0, IF_CDC_COMM, 0, 2, 0, 0x03, 0x00 };
        memcpy(notify_pkt, n, sizeof n);
        if (drv_ep_tx(EP_CDC_NOTIFY, notify_pkt, sizeof n) == 0) notify_pending = 0;
    }
    if (tx_busy || !dtr) return;
    uint16_t n = 0;
    while (n < CDC_DATA_SIZE && tx_tail != tx_head) tx_pkt[n++] = tx_ring[tx_tail++ % TX_RING];
    if (n == 0 && !tx_zlp) return;
    if (drv_ep_tx(EP_CDC_IN, tx_pkt, n) != 0) {     /* точка занята: вернуть байты в кольцо */
        tx_tail -= n;
        return;
    }
    tx_busy = 1;
    tx_zlp = n == CDC_DATA_SIZE && tx_tail == tx_head;     /* полный пакет был последним: нужен пустой */
}

console.c — командная консоль и журнал.

/* console.c — командная консоль в виртуальном COM-порту и кольцо журнала. Раздел 198ш. */
#include "usbdev.h"
#include <string.h>

#define LOG_CAP 8u              /* последних записей журнала */

static struct { uint32_t t_s; int16_t temp_c100; uint16_t rh_c100; } log_ring[LOG_CAP];
static uint32_t log_count;

void log_add(uint32_t t_s, int16_t temp_c100, uint16_t rh_c100)
{
    log_ring[log_count % LOG_CAP].t_s = t_s;
    log_ring[log_count % LOG_CAP].temp_c100 = temp_c100;
    log_ring[log_count % LOG_CAP].rh_c100 = rh_c100;
    log_count++;
}

static void put(const char *s) { cdc_write(s, (uint32_t)strlen(s)); }

static void put_u(uint32_t v)
{
    char b[11];
    int i = 10;
    b[10] = 0;
    do { b[--i] = (char)('0' + v % 10u); v /= 10u; } while (v);
    put(b + i);
}

static void put_c100(int32_t v)             /* сотые доли: -1234 -> «-12.34» */
{
    char f[4] = { '.', 0, 0, 0 };
    if (v < 0) { put("-"); v = -v; }
    put_u((uint32_t)v / 100u);
    f[1] = (char)('0' + v / 10 % 10);
    f[2] = (char)('0' + v % 10);
    put(f);
}

static void cmd_status(void)
{
    put("cfg="); put_u(usb_configured());
    put(" dtr="); put_u(cdc_dtr());
    put(" rts="); put_u(cdc_rts());
    put(" baud="); put_u(cdc_baud());
    put(" leds="); put_u(kbd_leds());
    put(" reports="); put_u(kbd_reports_sent());
    put(" log="); put_u(log_count);
    put("\r\n");
}

static void cmd_log(void)
{
    uint32_t n = log_count < LOG_CAP ? log_count : LOG_CAP;
    put("seq,time_s,temp_C,rh_pct\r\n");
    for (uint32_t seq = log_count - n; seq != log_count; seq++) {
        put_u(seq); put(",");
        put_u(log_ring[seq % LOG_CAP].t_s); put(",");
        put_c100(log_ring[seq % LOG_CAP].temp_c100); put(",");
        put_c100(log_ring[seq % LOG_CAP].rh_c100); put("\r\n");
    }
}

static void run(char *line)
{
    if (strcmp(line, "status") == 0) {
        cmd_status();
    } else if (strcmp(line, "log") == 0) {
        cmd_log();
    } else if (strncmp(line, "macro ", 6) == 0) {    /* что набирает кнопка */
        button_macro_set(line + 6);
        put("OK macro\r\n");
    } else if (strncmp(line, "type ", 5) == 0) {     /* набрать текст клавиатурой сейчас */
        kbd_type(line + 5);
        put("OK type\r\n");
    } else if (strcmp(line, "help") == 0) {
        put("status | log | macro <text> | type <text> | help\r\n");
    } else if (line[0]) {
        put("ERR unknown command\r\n");
    }
}

/* Главный цикл: собрать строку из кольца приёма по '\r' или '\n' и выполнить */
void console_poll(void)
{
    static char line[64];
    static uint8_t pos;
    int ch;
    while ((ch = cdc_getc()) >= 0) {
        if (ch == '\r' || ch == '\n') {
            line[pos] = 0;
            pos = 0;
            run(line);
        } else if (pos + 1u < sizeof line) {
            line[pos++] = (char)ch;         /* длинная строка обрезается */
        }
    }
}

hidkbd.c — класс HID: загрузочная клавиатура и кнопка.

/* hidkbd.c — класс HID, загрузочная клавиатура: запросы класса, набор текста отчётами,
   кнопка с антидребезгом. Раздел 198ш. */
#include "usbdev.h"
#include <string.h>

#define TYPE_RING 128u          /* очередь символов на набор; степень двойки */
#define DEBOUNCE_MS 20u
#define MACRO_MAX 48u

/* Назначения 0x04…0x38 подряд, раскладка США: без Shift и с Shift (те же строки, что в 198ч) */
static const char plain[0x39 - 0x04 + 1] =
    "abcdefghijklmnopqrstuvwxyz1234567890\r\x1b\b\t -=[]\\#;'`,./";
static const char shifted[0x39 - 0x04 + 1] =
    "ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()\r\x1b\b\t _+{}|~:\"~<>?";

static char type_ring[TYPE_RING];
static uint32_t type_head, type_tail;
static uint8_t held;            /* код клавиши, которая сейчас «нажата» в глазах хоста */
static uint8_t roll;            /* 1 — отпускать только когда без этого нельзя */
static uint8_t report[8], last[8];
static uint8_t in_busy, idle_rate, protocol = 1, leds;
static uint32_t sent;
static char macro[MACRO_MAX + 1] = "Hello, U585!\n";
static uint8_t btn_state, btn_count;

uint8_t kbd_leds(void) { return leds; }
uint32_t kbd_reports_sent(void) { return sent; }
void kbd_set_roll(uint8_t on) { roll = on; }
const char *button_macro(void) { return macro; }

void button_macro_set(const char *text)
{
    strncpy(macro, text, MACRO_MAX);
    macro[MACRO_MAX] = 0;
}

void hid_reset(void)
{
    type_head = type_tail = 0;
    held = 0;
    in_busy = 0;
    idle_rate = 0;
    protocol = 1;               /* после сброса — протокол отчётов; у нас он совпадает с загрузочным */
    memset(last, 0, sizeof last);
}

/* Символ -> код клавиши и модификатор. '\n' набирается клавишей Enter */
int kbd_lookup(char ch, uint8_t *usage, uint8_t *mods)
{
    if (ch == '\n') ch = '\r';
    for (uint8_t i = 0; i < sizeof plain - 1; i++) {
        if (i == 0x32u - 0x04u) continue;   /* клавиши 0x32 на клавиатуре США нет */
        if (plain[i] == ch) { *usage = (uint8_t)(0x04u + i); *mods = 0; return 1; }
        if (shifted[i] == ch) { *usage = (uint8_t)(0x04u + i); *mods = 0x02; return 1; }  /* левый Shift */
    }
    return 0;
}

int kbd_type(const char *text)
{
    int n = 0;
    while (*text && type_head - type_tail < TYPE_RING) {
        type_ring[type_head++ % TYPE_RING] = *text++;
        n++;
    }
    return n;
}

/* Следующий отчёт набора. 1 — отчёт готов, 0 — слать нечего.
   Хост считает символом код, которого не было в прошлом отчёте. Поэтому между двумя
   одинаковыми кодами («ll», «5» и «%») обязательно отпускание; между разными оно не нужно,
   но без режима roll мы отпускаем после каждого символа — так делает обычная клавиатура. */
int kbd_next_report(uint8_t r[8])
{
    uint8_t usage, mods;
    memset(r, 0, 8);
    while (type_tail != type_head) {
        if (!kbd_lookup(type_ring[type_tail % TYPE_RING], &usage, &mods)) {
            type_tail++;                    /* такого символа на клавиатуре нет — пропуск */
            continue;
        }
        if (held && (!roll || held == usage)) {
            held = 0;                       /* отчёт «всё отпущено», символ остаётся в очереди */
            return 1;
        }
        r[0] = mods;
        r[2] = usage;
        held = usage;
        type_tail++;
        return 1;
    }
    if (held) {                             /* текст кончился: отпустить последнюю клавишу */
        held = 0;
        return 1;
    }
    return 0;
}

void hid_in_done(void) { in_busy = 0; }

/* Главный цикл: точка свободна — отдать следующий отчёт */
void hid_poll(void)
{
    if (!usb_configured() || in_busy) return;
    if (!kbd_next_report(report)) return;
    if (drv_ep_tx(EP_HID_IN, report, sizeof report) == 0) {
        in_busy = 1;
        sent++;
        memcpy(last, report, sizeof last);
    }
}

/* Кнопка: вызывать раз в 1 мс. Нажатие принято, когда уровень держится DEBOUNCE_MS подряд */
void button_tick(uint8_t pressed)
{
    if (pressed == btn_state) {
        btn_count = 0;
        return;
    }
    if (++btn_count < DEBOUNCE_MS) return;
    btn_state = pressed;
    btn_count = 0;
    if (pressed) kbd_type(macro);
}

/* Запросы класса HID к интерфейсу 2 */
int hid_setup(const uint8_t s[8], const uint8_t **data, uint16_t *len)
{
    uint16_t wlength = (uint16_t)(s[6] | s[7] << 8);
    switch (s[1]) {
    case 0x01:                              /* GET_REPORT: входной отчёт (тип 1) — текущее состояние */
        if (s[0] != 0xA1 || s[3] != 1) return REQ_STALL;
        *data = last;
        *len = sizeof last;
        return REQ_IN;
    case 0x02:                              /* GET_IDLE */
        if (s[0] != 0xA1) return REQ_STALL;
        *data = &idle_rate;
        *len = 1;
        return REQ_IN;
    case 0x03:                              /* GET_PROTOCOL */
        if (s[0] != 0xA1) return REQ_STALL;
        *data = &protocol;
        *len = 1;
        return REQ_IN;
    case 0x09:                              /* SET_REPORT: выходной отчёт (тип 2) — светодиоды, 1 байт */
        return (s[0] == 0x21 && s[3] == 2 && wlength == 1) ? REQ_OUT : REQ_STALL;
    case 0x0A:                              /* SET_IDLE: старший байт wValue — период в единицах 4 мс */
        if (s[0] != 0x21) return REQ_STALL;
        idle_rate = s[3];
        return REQ_NODATA;
    case 0x0B:                              /* SET_PROTOCOL: 0 — загрузочный, 1 — отчётов */
        if (s[0] != 0x21 || s[2] > 1) return REQ_STALL;
        protocol = s[2];
        return REQ_NODATA;
    default:
        return REQ_STALL;
    }
}

int hid_setup_data(const uint8_t s[8], const uint8_t *p, uint16_t n)
{
    if (s[1] != 0x09 || n != 1) return REQ_STALL;
    leds = p[0];                            /* бит 0 — Num Lock, бит 1 — Caps Lock, бит 2 — Scroll Lock */
    return REQ_NODATA;
}

otg_u585.c — драйвер OTG_FS STM32U585 регистрами.

/* otg_u585.c — драйвер OTG_FS STM32U585 в режиме устройства, регистрами. Раздел 198ш.
   Каждая передача IN — один пакет не больше 64 байт: он целиком входит в свой FIFO. */
#include "usbdev.h"
#include "otg_regs.h"

static uint16_t out_mps[4];

_Static_assert(FIFO_RX_WORDS + FIFO_TX0_WORDS + FIFO_TX1_WORDS + FIFO_TX2_WORDS + FIFO_TX3_WORDS
               <= FIFO_RAM_WORDS, "FIFO не помещаются в 320 слов");
_Static_assert(FIFO_RX_WORDS >= (5u * 1u + 8u) + 2u * (64u / 4u + 1u) + 2u * 2u + 1u,
               "RX FIFO: меньше формулы RM0456 72.11.3 с запасом на два пакета");

/* Раздел памяти FIFO: приёмный с адреса 0, за ним передающие подряд */
void otg_fifo_setup(void)
{
    static const uint16_t tx[4] = { FIFO_TX0_WORDS, FIFO_TX1_WORDS, FIFO_TX2_WORDS, FIFO_TX3_WORDS };
    uint32_t start = FIFO_RX_WORDS;
    REG(R_GRXFSIZ) = FIFO_RX_WORDS;
    REG(R_DIEPTXF0) = (uint32_t)tx[0] << 16 | start;      /* глубина в старшей половине, начало в младшей */
    start += tx[0];
    for (uint32_t i = 1; i < 4; i++) {
        REG(R_DIEPTXF(i)) = (uint32_t)tx[i] << 16 | start;
        start += tx[i];
    }
}

static void ep0_out_arm(void)
{
    REG(R_DOEPTSIZ(0)) = 3u << TSIZ_STUPCNT_Pos | 1u << TSIZ_PKTCNT_Pos | EP0_SIZE;
    REG(R_DOEPCTL(0)) |= EPCTL_EPENA | EPCTL_CNAK;
}

void drv_ep_open(uint8_t addr, uint8_t type, uint16_t mps)
{
    uint8_t n = addr & 0x0Fu;
    if (addr & 0x80u) {                 /* IN: свой FIFO с тем же номером, что у точки */
        REG(R_DIEPCTL(n)) = mps | (uint32_t)type << EPCTL_EPTYP_Pos | (uint32_t)n << EPCTL_TXFNUM_Pos
                          | EPCTL_SD0PID | EPCTL_USBAEP | EPCTL_SNAK;
        REG(R_DAINTMSK) |= 1u << n;
    } else {
        out_mps[n] = mps;
        REG(R_DOEPCTL(n)) = mps | (uint32_t)type << EPCTL_EPTYP_Pos | EPCTL_SD0PID | EPCTL_USBAEP | EPCTL_SNAK;
        REG(R_DAINTMSK) |= 1u << (16u + n);
    }
}

void drv_ep_close(uint8_t addr)
{
    uint8_t n = addr & 0x0Fu;
    if (addr & 0x80u) {
        REG(R_DAINTMSK) &= ~(1u << n);
        REG(R_DIEPCTL(n)) = (REG(R_DIEPCTL(n)) & EPCTL_EPENA) ? EPCTL_EPDIS | EPCTL_SNAK : 0;
    } else {
        REG(R_DAINTMSK) &= ~(1u << (16u + n));
        REG(R_DOEPCTL(n)) = (REG(R_DOEPCTL(n)) & EPCTL_EPENA) ? EPCTL_EPDIS | EPCTL_SNAK : 0;
    }
}

void drv_ep_rx_arm(uint8_t addr)
{
    uint8_t n = addr & 0x0Fu;
    REG(R_DOEPTSIZ(n)) = 1u << TSIZ_PKTCNT_Pos | out_mps[n];
    REG(R_DOEPCTL(n)) |= EPCTL_EPENA | EPCTL_CNAK;
}

/* Один пакет IN: размер и число пакетов, разрешить точку, затем слова в FIFO */
int drv_ep_tx(uint8_t addr, const uint8_t *p, uint16_t len)
{
    uint8_t n = addr & 0x0Fu;
    if (REG(R_DIEPCTL(n)) & EPCTL_EPENA) return -1;       /* прошлый пакет ещё не ушёл */
    REG(R_DIEPTSIZ(n)) = 1u << TSIZ_PKTCNT_Pos | len;
    REG(R_DIEPCTL(n)) |= EPCTL_EPENA | EPCTL_CNAK;
    for (uint16_t i = 0; i < len; i += 4) {
        uint32_t w = p[i];
        if (i + 1u < len) w |= (uint32_t)p[i + 1] << 8;
        if (i + 2u < len) w |= (uint32_t)p[i + 2] << 16;
        if (i + 3u < len) w |= (uint32_t)p[i + 3] << 24;
        REG(R_DFIFO(n)) = w;
    }
    return 0;
}

void drv_ep0_tx(const uint8_t *p, uint16_t len) { (void)drv_ep_tx(0x80, p, len); }

void drv_ep0_stall(void)
{
    REG(R_DIEPCTL(0)) |= EPCTL_STALL;     /* ядро само снимет остановку со следующим SETUP */
    REG(R_DOEPCTL(0)) |= EPCTL_STALL;
    ep0_out_arm();
}

void drv_ep_stall(uint8_t addr, int on)
{
    uint32_t off = (addr & 0x80u) ? R_DIEPCTL(addr & 0x0Fu) : R_DOEPCTL(addr & 0x0Fu);
    if (on) REG(off) |= EPCTL_STALL;
    else REG(off) = (REG(off) & ~EPCTL_STALL) | EPCTL_SD0PID;   /* после снятия — снова DATA0 */
}

void drv_set_address(uint8_t addr)
{
    REG(R_DCFG) = (REG(R_DCFG) & ~DCFG_DAD_Msk) | (uint32_t)addr << DCFG_DAD_Pos;
}

#ifndef HOST_MODEL
static uint8_t setup_pkt[8];
static uint8_t rx_buf[2][64];           /* принятые пакеты: [0] — точка 0, [1] — данные CDC */
static uint16_t rx_len[2];

static void flush_fifos(void)
{
    REG(R_GRSTCTL) = GRSTCTL_TXFFLSH | 0x10u << GRSTCTL_TXFNUM_Pos;   /* 0x10 — все передающие */
    while (REG(R_GRSTCTL) & GRSTCTL_TXFFLSH) { }
    REG(R_GRSTCTL) = GRSTCTL_RXFFLSH;
    while (REG(R_GRSTCTL) & GRSTCTL_RXFFLSH) { }
}

/* Ядро OTG_FS: тактирование, выводы и VDDUSB к этому моменту уже включены (main_u585.c) */
void otg_init(void)
{
    REG(R_GAHBCFG) &= ~GAHBCFG_GINT;
    REG(R_GUSBCFG) |= GUSBCFG_PHYSEL;                     /* встроенный приёмопередатчик FS */
    while (!(REG(R_GRSTCTL) & GRSTCTL_AHBIDL)) { }
    REG(R_GRSTCTL) |= GRSTCTL_CSRST;                      /* сброс ядра */
    while (REG(R_GRSTCTL) & GRSTCTL_CSRST) { }
    REG(R_GCCFG) |= GCCFG_PWRDWN;                         /* 1 — приёмопередатчик включён */
    REG(R_GUSBCFG) = (REG(R_GUSBCFG) & ~GUSBCFG_TRDT_Msk) | GUSBCFG_FDMOD | 6u << GUSBCFG_TRDT_Pos;
    REG(R_DCTL) |= DCTL_SDIS;                             /* пока не готовы — подтяжка D+ снята */
    REG(R_GCCFG) &= ~GCCFG_VBDEN;                         /* VBUS не следим: считаем, что он есть */
    REG(R_GOTGCTL) |= GOTGCTL_BVALOEN | GOTGCTL_BVALOVAL;
    REG(R_PCGCCTL) = 0;
    REG(R_DCFG) |= DCFG_DSPD_FS;
    otg_fifo_setup();
    flush_fifos();
    REG(R_DIEPMSK) = EPINT_XFRC;
    REG(R_DOEPMSK) = EPINT_XFRC | EPINT_STUP;
    REG(R_DAINTMSK) = 0;
    REG(R_GINTSTS) = 0xBFFFFFFFu;
    REG(R_GINTMSK) = GINT_USBRST | GINT_ENUMDNE | GINT_RXFLVL | GINT_IEPINT | GINT_OEPINT;
    REG(R_GAHBCFG) |= GAHBCFG_GINT;
    REG(R_DCTL) &= ~DCTL_SDIS;                            /* подтяжка D+: хост видит устройство */
}

/* Пакет из приёмного FIFO: слово состояния, за ним данные словами */
static void rx_pop(void)
{
    uint32_t s = REG(R_GRXSTSP);
    uint32_t n = RXSTS_EPNUM(s), cnt = RXSTS_BCNT(s), sts = RXSTS_PKTSTS(s);
    if (sts != PKTSTS_OUT_DATA && sts != PKTSTS_SETUP_DATA) return;    /* «передача окончена» — без данных */
    uint8_t *dst = sts == PKTSTS_SETUP_DATA ? setup_pkt : rx_buf[n ? 1 : 0];
    for (uint32_t i = 0; i < cnt; i += 4) {
        uint32_t w = REG(R_DFIFO(0));
        for (uint32_t k = 0; k < 4 && i + k < cnt; k++)
            if (i + k < 64u) dst[i + k] = (uint8_t)(w >> (8 * k));
    }
    if (sts == PKTSTS_OUT_DATA) rx_len[n ? 1 : 0] = (uint16_t)cnt;
}

void otg_irq(void)
{
    uint32_t st = REG(R_GINTSTS) & REG(R_GINTMSK);
    if (st & GINT_USBRST) {
        REG(R_GINTSTS) = GINT_USBRST;
        REG(R_DCFG) &= ~DCFG_DAD_Msk;
        REG(R_DAINTMSK) = 1u | 1u << 16;                  /* точка 0 в обе стороны */
        usb_reset();
        ep0_out_arm();
    }
    if (st & GINT_ENUMDNE) {
        REG(R_GINTSTS) = GINT_ENUMDNE;
        REG(R_DIEPCTL(0)) &= ~0x3u;                       /* MPSIZ точки 0: 00 — 64 байта */
        REG(R_DCTL) |= DCTL_CGINAK;
    }
    while (REG(R_GINTSTS) & GINT_RXFLVL) rx_pop();
    if (st & GINT_OEPINT) {
        uint32_t eps = REG(R_DAINT) >> 16;
        for (uint8_t n = 0; n < 4; n++) {
            if (!(eps & 1u << n)) continue;
            uint32_t f = REG(R_DOEPINT(n));
            REG(R_DOEPINT(n)) = f;
            if (f & EPINT_STUP) {
                usb_setup(setup_pkt);
                ep0_out_arm();
            } else if (f & EPINT_XFRC) {
                if (n == 0) {
                    usb_ep0_out(rx_buf[0], rx_len[0]);
                    rx_len[0] = 0;
                    ep0_out_arm();
                } else if (usb_ep_out(n, rx_buf[1], rx_len[1])) {
                    drv_ep_rx_arm(n);
                }
            }
        }
    }
    if (st & GINT_IEPINT) {
        uint32_t eps = REG(R_DAINT) & 0xFFFFu;
        for (uint8_t n = 0; n < 4; n++) {
            if (!(eps & 1u << n)) continue;
            uint32_t f = REG(R_DIEPINT(n));
            REG(R_DIEPINT(n)) = f;
            if (!(f & EPINT_XFRC)) continue;
            if (n == 0) usb_ep0_in_done();
            else usb_ep_in_done(0x80u | n);
        }
    }
}
#endif

main_u585.c — 48 МГц от HSI48 с CRS, VDDUSB, выводы и главный цикл.

/* main_u585.c — то, что нужно USB на STM32U585 кроме самого ядра OTG_FS: 48 МГц от HSI48 с
   подстройкой CRS по SOF, питание VDDUSB, выводы PA11/PA12, кнопка PC13, номер кристалла.
   Системную частоту (160 МГц от PLL) настраивает SystemClock_Config примера пакета платы.
   Раздел 198ш. На плате не запускалось. */
#include "stm32u585xx.h"
#include "usbdev.h"
#include "otg_regs.h"

static volatile uint32_t ms;

void SysTick_Handler(void)
{
    ms++;
    button_tick((GPIOC->IDR & GPIO_IDR_ID13) != 0);     /* USER на PC13: нажата — единица */
}

void OTG_FS_IRQHandler(void) { otg_irq(); }

/* Датчики — в разделе 198х; здесь заглушка, чтобы журнал не был пустым */
static void sensor_sample(int16_t *temp_c100, uint16_t *rh_c100)
{
    *temp_c100 = 2345;
    *rh_c100 = 4510;
}

void usb_clock_init(void)
{
    RCC->CR |= RCC_CR_HSI48ON;                          /* свой генератор 48 МГц */
    while (!(RCC->CR & RCC_CR_HSI48RDY)) { }
    RCC->CCIPR1 &= ~RCC_CCIPR1_ICLKSEL;                 /* 00: 48 МГц для USB — от HSI48 */
    RCC->APB1ENR1 |= RCC_APB1ENR1_CRSEN;
    CRS->CFGR = (CRS->CFGR & ~CRS_CFGR_SYNCSRC) | CRS_CFGR_SYNCSRC_1;  /* метка — SOF от USB */
    CRS->CR |= CRS_CR_AUTOTRIMEN | CRS_CR_CEN;          /* подстройка HSI48 по кадрам хоста */

    RCC->AHB3ENR |= RCC_AHB3ENR_PWREN;
    PWR->SVMCR |= PWR_SVMCR_USV;                        /* VDDUSB есть: снять изоляцию выводов USB */

    RCC->AHB2ENR1 |= RCC_AHB2ENR1_GPIOAEN | RCC_AHB2ENR1_GPIOCEN | RCC_AHB2ENR1_OTGEN;
    GPIOA->AFR[1] = (GPIOA->AFR[1] & ~(0xFFu << 12)) | 0xAAu << 12;     /* PA11, PA12 — AF10 */
    GPIOA->OSPEEDR |= 0xFu << 22;
    GPIOA->MODER = (GPIOA->MODER & ~(0xFu << 22)) | 0xAu << 22;         /* альтернативная функция */
    GPIOC->MODER &= ~(3u << 26);                        /* PC13 — вход */
}

int main(void)
{
    uint32_t next_log = 0, t_s = 0;
    /* SystemClock_Config() примера: HCLK 160 МГц */
    usb_clock_init();
    usb_serial_from_uid((const uint32_t *)UID_BASE);    /* 96 бит номера кристалла */
    SysTick_Config(160000000u / 1000u);
    otg_init();
    NVIC_SetPriority(OTG_FS_IRQn, 8);
    NVIC_EnableIRQ(OTG_FS_IRQn);
    for (;;) {
        NVIC_DisableIRQ(OTG_FS_IRQn);                   /* ядро не зовут из двух мест сразу (20.7) */
        cdc_poll();
        hid_poll();
        NVIC_EnableIRQ(OTG_FS_IRQn);
        console_poll();
        if ((int32_t)(ms - next_log) >= 0) {            /* раз в 10 с — запись в журнал */
            int16_t t;
            uint16_t rh;
            next_log += 10000u;
            sensor_sample(&t, &rh);
            log_add(t_s, t, rh);
            t_s += 10u;
        }
    }
}

Дальше: раздел 198щ «Мини-проект: свой загрузчик серийного прибора на STM32F405 — раскладка по секторам, буферный слот, обновление по UART-Bluetooth и защита от обрыва питания» — прибор собран, теперь его надо уметь обновлять у покупателя.

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