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

20д. Своё USB-устройство без драйвера: дескрипторы по байтам, MS OS 2.0 и WinUSB

В разделе 20 прибор стал COM-портом, в 20в — клавиатурой, в 198ш — тем и другим сразу. Это готовые классы: система берёт для них свой драйвер. Но бывает, что ни COM-порт, ни HID не подходят. Нужны свои команды, своя скорость, массовые точки без обёрток. Тогда класс у прибора свой — 0xFF, «у производителя». Своего драйвера для него в Windows нет. Его либо пишут и подписывают, либо прибор сам говорит системе: «возьми стандартный WinUSB». Для этого служат дескрипторы Microsoft OS 2.0.

Здесь собрано такое устройство: интерфейс класса 0xFF, две массовые точки, BOS с платформенной возможностью MS OS 2.0 и набор дескрипторов с совместимым ID «WINUSB» и GUID интерфейса. Устройство работает эхом: что пришло в точку OUT, то, «посоленное» ключом, уходит обратно в точку IN. Всё описано по байтам, и каждый байт проверен. Хост на компьютере опознаёт устройство через модель блока USB FS STM32F103 и разбирает каждый дескриптор своим разборщиком. Набор MS OS 2.0 и BOS побайтно сверены со структурами Zephyr. Программа на ПК прогнана поверх той же модели. Прошивки под F103 и F407 собраны arm-none-eabi-gcc. На плате и на Windows прибор не запускался.

Опоры: RM0008 Rev 17 (STM32F10x, глава 23 «USB full-speed device interface»: 23.4.2, 23.5.2, 23.5.3, таблица 177; 30.2 — номер кристалла); DocID13587 Rev 17 (STM32F103x8/B, таблица 43 — время запуска приёмопередатчика); RM0090 Rev 19 (STM32F4, глава 34 «OTG_FS»: 34.2, 34.13.1); заголовки stm32f10x.h (STM32F1xx_DFP 2.4.1) и stm32f407xx.h (STM32F4xx_DFP 2.17.1); Zephyr 4.4.2 (github.com/zephyrproject-rtos/zephyr, коммит dccb095: include/zephyr/usb/msos_desc.h, include/zephyr/usb/bos.h, пример samples/subsys/usb/webusb, subsys/usb/device_next/usbd_ch9.c; лицензия Apache-2.0); candleLight (github.com/candle-usb/candleLight_fw, коммит 85e5b33, src/usbd_gs_can.c; лицензия MIT); MDK-Middleware 7.17.0 и 8.1.0 (usb_ms.h, USBD_Config_CustomClass.h, описание примера WinUSB_Echo; лицензия Arm — только пересказ). Устройство, точка 0 и драйвер F407 продолжают код раздела 198ш. Тексты спецификаций USB и Microsoft OS 2.0 не использовались: форматы взяты из этих исходников. Лицензия: свой код раздела — свободно.

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

Оригинал Перевод
vendor-specific class (0xFF) класс «у производителя»: смысл задаёт сам прибор
vendor request запрос производителя: bmRequestType 0x40 или 0xC0
Binary device Object Store (BOS) хранилище возможностей устройства, дескриптор типа 0x0F
Device Capability, Platform Capability возможность устройства; платформенная — с UUID платформы
Microsoft OS 2.0 descriptor set набор дескрипторов Microsoft OS 2.0
bMS_VendorCode код запроса производителя, которым хост просит набор
Compatible ID «WINUSB» совместимый ID: какой стандартный драйвер подходит
DeviceInterfaceGUIDs, REG_MULTI_SZ GUID интерфейса устройства; тип реестра «список строк»
Microsoft OS String Descriptor (0xEE) строка 0xEE — вход в старые дескрипторы MS OS 1.0
packet memory area (PMA), buffer descriptor table память пакетов F103 и таблица буферов в ней
toggle bit, rc_w0 бит-переключатель (меняется записью 1); флаг, сбрасываемый записью 0

20д.1 Зачем своё устройство и что хост спросит

Три пути к прибору с компьютера:

Класс Драйвер на ПК Что получает программа Когда брать
CDC ACM (раздел 20, 198ш) у системы свой: в Windows — usbser.sys (198ш.6) COM-порт: поток байт консоль, замена UART
HID (раздел 20в) у системы свой, во всех системах отчёты фиксированной длины по прерывающей точке клавиатура, кнопки, малые данные
свой, 0xFF + WinUSB winusb.sys, если прибор отдал MS OS-дескрипторы точки как есть: управляющие запросы, массовые пакеты по 64 байта свой протокол, поток данных, без обёрток COM-порта

У COM-порта нет своих команд, кроме скорости и линий: команду придётся прятать в поток. У HID отчёт фиксированной длины и прерывающая точка: не больше одного пакета за период опроса. Своё устройство даёт всё сразу: управляющие запросы для команд и массовые точки для данных. Цена — драйвер на ПК. Его и снимают дескрипторы MS OS: в описании примера WinUSB_Echo из MDK-Middleware сказано, что на Windows 8 и новее драйвер ставится сам, а на Windows 7 нужен файл установки.

Что хост спрашивает у такого прибора — на рисунке. Новое по сравнению с 198ш — шаги 2 и 3: дескриптор BOS и запрос производителя.

Опознание своего устройства: хост читает дескриптор устройства, видит bcdUSB 2.01 и просит BOS сначала на 5 байт, потом на все 33; в BOS платформенная возможность MS OS 2.0 с UUID D8DD60DF, кодом производителя 0x20 и длиной набора 162 байта; запрос производителя C0 20 с wIndex 7 возвращает набор пакетами 64, 64 и 34 байта: заголовок 10, совместимый ID WINUSB 20 и свойство с GUID 132; дальше конфигурация 32 байта с интерфейсом класса FF и массовыми точками 0x01 и 0x81, затем эхо данных и свои запросы на точке 0; внизу — при bcdUSB 2.00 BOS не запрашивается, а MS OS 1.0 у candleLight — это строка 0xEE и два запроса, всего 228 байт

Модель хоста повторяет этот порядок. Сначала она смотрит bcdUSB. Стек Zephyr на стороне устройства отказывает в BOS, если bcdUSB меньше 0x0201 (sreq_get_desc_bos в usbd_ch9.c). Поэтому и модель просит BOS только у устройства с версией 2.01 и выше. Платформенную возможность она узнаёт по UUID и берёт из неё код запроса и длину набора. Набор просит запросом производителя к устройству с wIndex = 7: это MS_OS_20_DESCRIPTOR_INDEX в msos_desc.h Zephyr.


20д.2 Дескрипторы устройства и конфигурации по байтам

Общий заголовок устройства (vend.h) — номера точек, коды запросов и длины всех дескрипторов:

#ifndef USB_BCD_USB
#define USB_BCD_USB    0x0201u      /* 2.01: только у такого устройства хост спрашивает BOS */
#endif
#define USB_VID        0x1209u      /* заглушки для макета: в изделии — свои номера */
#define USB_PID        0x0002u
#define EP0_SIZE       64u
#define EP_OUT         0x01u        /* массовая OUT, 64 байта */
#define EP_IN          0x81u        /* массовая IN, 64 байта */
#define BULK_SIZE      64u
#define EP_TYPE_BULK   2u

#define MS_VENDOR_CODE 0x20u        /* bRequest, которым хост просит набор MS OS 2.0 */
#define MS_OS_20_INDEX 7u           /* wIndex того же запроса */
#define VREQ_GET_INFO  0x01u        /* свои запросы: IN, 12 байт состояния */
#define VREQ_SET_LED   0x02u        /* без данных: wValue = 0 или 1 */
#define VREQ_SET_XOR   0x03u        /* OUT, 1 байт: им «солится» эхо */
#define INFO_SIZE      12u

#define DEVICE_DESC_SIZE 18u
#define CONFIG_DESC_SIZE 32u
#define BOS_DESC_SIZE    33u
#define MSOS20_SET_SIZE  162u

#define LE16(x) ((x) & 0xFFu), (((x) >> 8) & 0xFFu)

Сравните с 198ш.2: там класс устройства EF/02/01 и IAD, потому что функций две. Здесь одна функция, и класс стоит в интерфейсе. Главное отличие — два байта версии: 2.01 вместо 2.00.

const uint8_t usb_device_desc[] = {
    18, 0x01,                   /* bLength, DEVICE */
    LE16(USB_BCD_USB),          /* bcdUSB 2.01: у устройства есть BOS */
    0x00, 0x00, 0x00,           /* класс, подкласс, протокол — смотри интерфейс */
    EP0_SIZE,                   /* bMaxPacketSize0 */
    LE16(USB_VID),
    LE16(USB_PID),
    0x00, 0x01,                 /* bcdDevice 1.00 */
    1, 2, 3,                    /* строки: изготовитель, изделие, серийный номер */
    1                           /* одна конфигурация */
};

Конфигурация — 32 байта: сама конфигурация, один интерфейс и две точки.

const uint8_t usb_config_desc[] = {
    /* конфигурация */
    9, 0x02, LE16(CONFIG_DESC_SIZE),    /* wTotalLength = 32 */
    1,                          /* bNumInterfaces */
    1,                          /* bConfigurationValue */
    0,                          /* iConfiguration */
    0x80,                       /* bmAttributes: питание от шины */
    50,                         /* bMaxPower: 50 x 2 мА = 100 мА */
    /* интерфейс 0: свой класс */
    9, 0x04, 0, 0, 2,           /* номер 0, настройка 0, две точки */
    0xFF, 0x00, 0x00,           /* класс 0xFF «у производителя», подкласс и протокол — свои */
    0,                          /* iInterface */
    /* точки */
    7, 0x05, EP_OUT, EP_TYPE_BULK, LE16(BULK_SIZE), 0,
    7, 0x05, EP_IN, EP_TYPE_BULK, LE16(BULK_SIZE), 0
};

Хост модели прочитал оба дескриптора и разобрал их своим разборщиком:

SETUP 80 06 00 01 00 00 12 00  GET_DESCRIPTOR(DEVICE 0, 18): IN 18 = 18 байт
  DEVICE: bcdUSB 2.01, класс 00/00/00, пакет точки 0 — 64, VID 1209, PID 0002, строки 1 2 3, конфигураций 1 — сходится
SETUP 80 06 00 02 00 00 09 00  GET_DESCRIPTOR(CONFIGURATION 0, 9): IN 9 = 9 байт
  wTotalLength = 32
SETUP 80 06 00 02 00 00 FF 00  GET_DESCRIPTOR(CONFIGURATION 0, 255): IN 32 = 32 байт
    0: 09 02 20 00 01 01 00 80 32
       CONFIGURATION: wTotalLength 32, интерфейсов 1, номер 1, bmAttributes 0x80, 100 мА
    9: 09 04 00 00 02 FF 00 00 00
       INTERFACE 0: настройка 0, точек 2, класс FF/00/00
   18: 07 05 01 02 40 00 00
       ENDPOINT 0x01: массовая OUT, пакет 64, bInterval 0
   25: 07 05 81 02 40 00 00
       ENDPOINT 0x81: массовая IN, пакет 64, bInterval 0
  итог разбора конфигурации: пройдено 32 байт из 32, интерфейсов 1 (заявлено 1), точек 2 — сходится

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

Длины, записанные внутри дескрипторов, берутся из макросов, а размер массивов сверяет компилятор:

_Static_assert(sizeof usb_device_desc == DEVICE_DESC_SIZE, "дескриптор устройства: не 18 байт");
_Static_assert(sizeof usb_config_desc == CONFIG_DESC_SIZE, "конфигурация: wTotalLength не сходится");
_Static_assert(sizeof usb_bos_desc == BOS_DESC_SIZE, "BOS: wTotalLength не сходится");
_Static_assert(sizeof usb_msos20_set == MSOS20_SET_SIZE, "набор MS OS 2.0: wTotalLength не сходится");

Без этих строк массив можно объявить в заголовке с длиной: extern const uint8_t usb_msos20_set[162]. Тогда недостающий байт компилятор молча заполнит нулём, а лишний даст только предупреждение. С проверками оба случая — ошибка сборки (error: static assertion failed, раздел 9 прогона).


20д.3 BOS и набор MS OS 2.0

BOS — корень на 5 байт и за ним возможности. У нас одна, платформенная: UUID платформы и четыре поля Microsoft.

const uint8_t usb_bos_desc[] = {
    5, 0x0F,                    /* bLength, BOS */
    LE16(BOS_DESC_SIZE),        /* wTotalLength = 33: корень и все возможности */
    1,                          /* bNumDeviceCaps */
    /* платформенная возможность MS OS 2.0 */
    28, 0x10, 0x05,             /* bLength, DEVICE_CAPABILITY, PLATFORM */
    0x00,                       /* bReserved */
    0xDF, 0x60, 0xDD, 0xD8, 0x89, 0x45, 0xC7, 0x4C,     /* UUID D8DD60DF-4589-4CC7- */
    0x9C, 0xD2, 0x65, 0x9D, 0x9E, 0x64, 0x8A, 0x9F,     /*      9CD2-659D9E648A9F */
    0x00, 0x00, 0x00, 0x0A,     /* dwWindowsVersion 0x0A000000: Windows 10 */
    LE16(MSOS20_SET_SIZE),      /* wMSOSDescriptorSetTotalLength = 162 */
    MS_VENDOR_CODE,             /* bMS_VendorCode: этим bRequest набор и попросят */
    0x00                        /* bAltEnumCode: второго варианта нет */
};

UUID в дескрипторе записан как GUID Windows: первые три поля — младшим байтом вперёд. Поэтому D8DD60DF начинается с байта DF. Код производителя 0x20 выбран свободно: тот же код стоит у candleLight, у примеров Zephyr — 0x02, у MDK по умолчанию — 0x01.

Набор MS OS 2.0 — заголовок и записи. У каждой записи первые 4 байта — длина и тип, по 2 байта.

const uint8_t usb_msos20_set[] = {
    /* заголовок набора */
    0x0A, 0x00,                 /* wLength = 10 */
    0x00, 0x00,                 /* MS_OS_20_SET_HEADER_DESCRIPTOR */
    0x00, 0x00, 0x00, 0x0A,     /* dwWindowsVersion 0x0A000000 */
    LE16(MSOS20_SET_SIZE),      /* wTotalLength = 162: весь набор */
    /* совместимый ID: какой драйвер ставить */
    0x14, 0x00,                 /* wLength = 20 */
    0x03, 0x00,                 /* MS_OS_20_FEATURE_COMPATIBLE_ID */
    'W', 'I', 'N', 'U', 'S', 'B', 0x00, 0x00,           /* CompatibleID */
    0, 0, 0, 0, 0, 0, 0, 0,     /* SubCompatibleID: нет */
    /* свойство реестра: GUID, по которому программа найдёт устройство */
    0x84, 0x00,                 /* wLength = 132 */
    0x04, 0x00,                 /* MS_OS_20_FEATURE_REG_PROPERTY */
    0x07, 0x00,                 /* wPropertyDataType: REG_MULTI_SZ, список строк */
    0x2A, 0x00,                 /* wPropertyNameLength = 42 */
    U16('D'), U16('e'), U16('v'), U16('i'), U16('c'), U16('e'),
    U16('I'), U16('n'), U16('t'), U16('e'), U16('r'), U16('f'), U16('a'), U16('c'), U16('e'),
    U16('G'), U16('U'), U16('I'), U16('D'), U16('s'), 0x00, 0x00,
    0x50, 0x00,                 /* wPropertyDataLength = 80 */
    U16('{'), U16('9'), U16('D'), U16('E'), U16('E'), U16('3'), U16('3'), U16('1'), U16('4'),
    U16('-'), U16('0'), U16('6'), U16('2'), U16('6'),
    U16('-'), U16('4'), U16('A'), U16('2'), U16('1'),
    U16('-'), U16('B'), U16('C'), U16('E'), U16('4'),
    U16('-'), U16('9'), U16('4'), U16('5'), U16('6'), U16('A'), U16('E'), U16('2'), U16('2'),
    U16('7'), U16('C'), U16('2'), U16('8'), U16('}'),
    0x00, 0x00, 0x00, 0x00      /* конец строки и конец списка */
};

Хост модели прочитал BOS, нашёл в нём MS OS 2.0 и попросил набор:

SETUP 80 06 00 0F 00 00 05 00  GET_DESCRIPTOR(BOS 0, 5): IN 5 = 5 байт
  BOS: wTotalLength = 33
SETUP 80 06 00 0F 00 00 21 00  GET_DESCRIPTOR(BOS 0, 33): IN 33 = 33 байт
    0: 05 0F 21 00 01
       BOS: wTotalLength 33, возможностей 1
    5: 1C 10 05 00 DF 60 DD D8 89 45 C7 4C 9C D2 65 9D
   21: 9E 64 8A 9F 00 00 00 0A A2 00 20 00
       DEVICE_CAPABILITY 28 байт: PLATFORM, UUID D8DD60DF-4589-4CC7-9CD2-659D9E648A9F — MS OS 2.0
       dwWindowsVersion 0x0A000000, wMSOSDescriptorSetTotalLength 162, bMS_VendorCode 0x20, bAltEnumCode 0
  итог разбора BOS: пройдено 33 байт из 33, возможностей 1 (заявлено 1) — сходится
SETUP C0 20 00 00 07 00 A2 00  производитель IN: bRequest 0x20, wValue 0, wIndex 7, wLength 162: IN 64+64+34 = 162 байт
    0: 0A 00 00 00 00 00 00 0A A2 00
       заголовок: wLength 10, тип 0, dwWindowsVersion 0x0A000000, wTotalLength 162
   10: 14 00 03 00 57 49 4E 55 53 42 00 00 00 00 00 00
   26: 00 00 00 00
       COMPATIBLE_ID 20 байт: CompatibleID «WINUSB», SubCompatibleID «»
   30: 84 00 04 00 07 00 2A 00 44 00 65 00 76 00 69 00
   46: 63 00 65 00 49 00 6E 00 74 00 65 00 72 00 66 00
   62: 61 00 63 00 65 00 47 00 55 00 49 00 44 00 73 00
   78: 00 00 50 00 7B 00 39 00 44 00 45 00 45 00 33 00
   94: 33 00 31 00 34 00 2D 00 30 00 36 00 32 00 36 00
  110: 2D 00 34 00 41 00 32 00 31 00 2D 00 42 00 43 00
  126: 45 00 34 00 2D 00 39 00 34 00 35 00 36 00 41 00
  142: 45 00 32 00 32 00 37 00 43 00 32 00 38 00 7D 00
  158: 00 00 00 00
       REG_PROPERTY 132 байт: тип данных 7, имя 42 байт «DeviceInterfaceGUIDs», данные 80 байт
       список строк: {9DEE3314-0626-4A21-BCE4-9456AE227C28}; оканчивается двумя нулями: да
  итог разбора набора: пройдено 162 байт из 162 — сходится

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

Тот же прибор, собранный с bcdUSB 2.00, модель хоста опознаёт без BOS — и без набора:

  DEVICE: bcdUSB 2.00, класс 00/00/00, пакет точки 0 — 64, VID 1209, PID 0002, строки 1 2 3, конфигураций 1 — сходится
  bcdUSB 2.00 < 2.01: BOS не запрашивается, MS OS 2.0 хост не увидит
SETUP 80 06 00 06 00 00 0A 00  GET_DESCRIPTOR(DEVICE_QUALIFIER 0, 10): STALL
SETUP 80 06 00 02 00 00 09 00  GET_DESCRIPTOR(CONFIGURATION 0, 9): IN 9 = 9 байт
  wTotalLength = 32
SETUP 80 06 00 02 00 00 FF 00  GET_DESCRIPTOR(CONFIGURATION 0, 255): IN 32 = 32 байт
    0: 09 02 20 00 01 01 00 80 32
       CONFIGURATION: wTotalLength 32, интерфейсов 1, номер 1, bmAttributes 0x80, 100 мА
    9: 09 04 00 00 02 FF 00 00 00
       INTERFACE 0: настройка 0, точек 2, класс FF/00/00
   18: 07 05 01 02 40 00 00
       ENDPOINT 0x01: массовая OUT, пакет 64, bInterval 0
   25: 07 05 81 02 40 00 00
       ENDPOINT 0x81: массовая IN, пакет 64, bInterval 0
  итог разбора конфигурации: пройдено 32 байт из 32, интерфейсов 1 (заявлено 1), точек 2 — сходится
SETUP 80 06 00 03 00 00 FF 00  GET_DESCRIPTOR(STRING 0, 255): IN 4 = 4 байт
  длина 4: язык 0x0409
SETUP 80 06 01 03 09 04 FF 00  GET_DESCRIPTOR(STRING 1, 255): IN 30 = 30 байт
  длина 30: «STM32 Handbook»
SETUP 80 06 02 03 09 04 FF 00  GET_DESCRIPTOR(STRING 2, 255): IN 32 = 32 байт
  длина 32: «WinUSB echo 20d»
SETUP 80 06 03 03 09 04 FF 00  GET_DESCRIPTOR(STRING 3, 255): IN 50 = 50 байт
  длина 50: «000000000000000000000000»
SETUP 80 06 EE 03 00 00 12 00  GET_DESCRIPTOR(STRING 238, 18): STALL
итог опознания: дескрипторы сходятся; MS OS 2.0: не найден

Сверка с Zephyr. Набор и BOS собраны тем же инициализатором структур, что в примере webusb Zephyr (ветка без CDC), только с нашим GUID и кодом, и сравнены с нашими массивами побайтно:

размеры структур Zephyr: заголовок набора 10, подмножество конфигурации 8, подмножество функции 8, совместимый ID 20, свойство GUID 132
BOS у Zephyr: корень 5, платформенная возможность 20 + MS OS 8 = 28
набор MS OS 2.0: Zephyr 162 байт, наш 162 байт, побайтно одинаковы
BOS: Zephyr 33 байт, наш 33 байт, побайтно одинаковы

MS OS 1.0 — старый путь. До BOS было так: строка с индексом 0xEE и подписью «MSFT100» несёт код производителя. По нему хост делает два запроса: wIndex 4 — совместимый ID, wIndex 5 — расширенные свойства. Так сделан candleLight (прошивка адаптеров USB–CAN на STM32, раздел 19ж). MDK-Middleware тоже умеет только этот путь (usb_ms.h: строка 0xEE, индексы 4 и 5). Сравнение по прогону:

строка 0xEE: 18 байт, bLength 0x12, подпись «MSFT100», код 0x20
совместимый ID (wIndex 4): 64 байт, dwLength 0x40, секций 2
расширенные свойства (wIndex 5): 146 байт, dwLength 0x92, имя «DeviceInterfaceGUIDs» — есть
всего MS OS 1.0: 228 байт в трёх ответах
MS OS 1.0 (candleLight) MS OS 2.0 (этот раздел)
как хост узнаёт строка 0xEE с подписью «MSFT100» BOS, платформенная возможность с UUID D8DD60DF-…
условие — bcdUSB не ниже 2.01
запросов производителя два: wIndex 4 и 5 один: wIndex 7
байт в ответах 18 + 64 + 146 = 228 33 + 162 = 195
имя свойства GUID DeviceInterfaceGUIDs DeviceInterfaceGUIDs

Имя свойства пишут по-разному. У candleLight и Zephyr — DeviceInterfaceGUIDs (список, тип 7). В настройках MDK по умолчанию — DeviceInterfaceGUID (одна строка, тип 1), а тип 7 там прямо не поддерживается. Какое имя читает Windows, в разделе не проверялось; у нас — как у Zephyr и candleLight.


20д.4 Точка 0: запросы производителя

Стадии передачи, строки, адрес и конфигурация — как в 198ш.3. Новое — ветка запросов производителя в разборе SETUP и сам обработчик:

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, rcpt = s[0] & 0x1Fu;
    int r = REQ_STALL;

    c.rx_want = 0;
    if (type == 0) r = standard_request(s, &p, &len);
    else if (type == 2 && rcpt == 0) r = vendor_request(s, &p, &len);    /* производитель, устройству */
    if (r == REQ_IN && (s[0] & 0x80u) && wlength) {
        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 && !wlength) {
        ep0_status();
    } else {
        drv_ep0_stall();
    }
}
/* Запросы производителя к устройству. Набор MS OS 2.0 отдаётся только на свой
 * bRequest и wIndex = 7 и в любом состоянии — хост просит его до SET_CONFIGURATION.
 * Свои команды — только в настроенном устройстве. */
static int vendor_request(const uint8_t s[8], const uint8_t **p, uint16_t *len)
{
    uint8_t in = s[0] & 0x80u;
    uint16_t wvalue = (uint16_t)(s[2] | s[3] << 8), windex = (uint16_t)(s[4] | s[5] << 8);
    if (s[1] == MS_VENDOR_CODE) {
        if (!in || windex != MS_OS_20_INDEX) return REQ_STALL;
        *p = usb_msos20_set; *len = MSOS20_SET_SIZE;
        return REQ_IN;
    }
    if (!c.config) return REQ_STALL;
    switch (s[1]) {
    case VREQ_GET_INFO:         /* версия, светодиод, ключ, счётчики байт */
        if (!in) return REQ_STALL;
        c.buf[0] = 0x00; c.buf[1] = 0x01;           /* версия протокола 1.00 */
        c.buf[2] = app.led;
        c.buf[3] = app.xor_key;
        put32(c.buf + 4, app.rx_bytes);
        put32(c.buf + 8, app.tx_bytes);
        *p = c.buf; *len = INFO_SIZE;
        return REQ_IN;
    case VREQ_SET_LED:
        if (in || wvalue > 1) return REQ_STALL;
        app.led = (uint8_t)wvalue;
        board_led(app.led);
        return REQ_NODATA;
    case VREQ_SET_XOR:          /* байт ключа придёт в стадии данных */
        return in ? REQ_STALL : REQ_OUT;
    default:
        return REQ_STALL;
    }
}
static int vendor_data(const uint8_t s[8], const uint8_t *d, uint16_t n)
{
    if (s[1] != VREQ_SET_XOR || n != 1) return REQ_STALL;
    app.xor_key = d[0];
    return REQ_NODATA;
}

Три правила, которые здесь зашиты:

GET_DESCRIPTOR тоже знает новый тип: 0x0F — BOS.

static int get_descriptor(uint8_t type, uint8_t index, const uint8_t **p, uint16_t *len)
{
    switch (type) {
    case 0x01: *p = usb_device_desc; *len = DEVICE_DESC_SIZE; return 1;
    case 0x02: *p = usb_config_desc; *len = CONFIG_DESC_SIZE; return index == 0;
    case 0x03: *p = c.buf; *len = usb_string_desc(index, c.buf); return *len != 0;
    case 0x0F: *p = usb_bos_desc; *len = BOS_DESC_SIZE; return index == 0;
    default: return 0;          /* 0x06 DEVICE_QUALIFIER: только полная скорость — отказ */
    }
}

Прогон отказов и краёв:

SETUP C0 20 00 00 08 00 FF 00  производитель IN: bRequest 0x20, wValue 0, wIndex 8, wLength 255: STALL
SETUP C0 21 00 00 07 00 FF 00  производитель IN: bRequest 0x21, wValue 0, wIndex 7, wLength 255: STALL
SETUP 40 20 00 00 07 00 00 00  производитель OUT: bRequest 0x20, wValue 0, wIndex 7, wLength 0: STALL
SETUP C0 20 00 00 07 00 0A 00  производитель IN: bRequest 0x20, wValue 0, wIndex 7, wLength 10: IN 10 = 10 байт
  0A 00 00 00 00 00 00 0A A2 00
SETUP C0 20 00 00 07 00 FF 00  производитель IN: bRequest 0x20, wValue 0, wIndex 7, wLength 255: IN 64+64+34 = 162 байт
SETUP C1 20 00 00 07 00 FF 00  производитель IN: bRequest 0x20, wValue 0, wIndex 7, wLength 255: STALL
SETUP C0 01 00 00 00 00 0C 00  производитель IN: bRequest 0x01, wValue 0, wIndex 0, wLength 12: STALL
SETUP 00 09 01 00 00 00 00 00  SET_CONFIGURATION(1): состояние принято
  EP1R 0x3021: точка 1 открыта
SETUP C0 7F 00 00 00 00 01 00  производитель IN: bRequest 0x7F, wValue 0, wIndex 0, wLength 1: STALL
SETUP 40 02 02 00 00 00 00 00  производитель OUT: bRequest 0x02, wValue 2, wIndex 0, wLength 0: STALL

Запрос с wIndex 8 (MS_OS_20_SET_ALT_ENUMERATION в Zephyr) — отказ: bAltEnumCode = 0, второго варианта нет. Набор, запрошенный на 10 байт, — ровно заголовок. Запрос на 255 байт даёт все 162: короткий третий пакет (34 байта) сам говорит хосту «конец». GET_INFO до настройки — отказ, после SET_CONFIGURATION — 12 байт (20д.6). Строку 0xEE прибор не отдаёт: MS OS 1.0 ему не нужен.


20д.5 Драйвер F103: память пакетов и регистр EPnR

У F103 не OTG, как у F407 и U585, а свой блок USB FS (RM0008, глава 23). Буферы точек лежат в памяти пакетов на 512 байт (раздел 20.3). Процессор видит её 16-битными словами с шагом 4 байта: локальный адрес умножается на два (23.5.3). В начале памяти — таблица буферов, по 8 байт на точку: адрес и счётчик передачи, адрес и счётчик приёма. Дальше у нас — буферы по 64 байта: 0x40 и 0x80 у точки 0, 0xC0 и 0x100 у точки 1.

#define BT_ADDR_TX(n)  PMA16(8u * (n))
#define BT_COUNT_TX(n) PMA16(8u * (n) + 2u)
#define BT_ADDR_RX(n)  PMA16(8u * (n) + 4u)
#define BT_COUNT_RX(n) PMA16(8u * (n) + 6u)
#define BUF_TX(n)      (0x40u + 0x80u * (n))
#define BUF_RX(n)      (0x80u + 0x80u * (n))
static void pma_write(uint16_t a, const uint8_t *p, uint16_t n)
{
    for (uint16_t i = 0; i < n; i += 2) {
        uint16_t w = p[i];
        if (i + 1u < n) w |= (uint16_t)(p[i + 1] << 8);
        PMA16(a + i) = w;
    }
}

Главная ловушка — регистр точки EPnR. Его поля трёх видов (23.5.2): STAT_RX, STAT_TX, DTOG_RX, DTOG_TX меняются записью единицы (запись нуля их не трогает); флаги CTR_RX и CTR_TX сбрасываются записью нуля; тип, EP_KIND и номер — обычные. Чтение-изменение-запись здесь ломает всё: прочитанная единица в STAT при записи перевернёт бит. Поэтому нужное значение получают исключающим ИЛИ, а флаги CTR пишут единицами:

/* Запись в EPnR без чтения-изменения-записи: STAT и DTOG меняются записью 1,
 * флаги CTR сбрасываются записью 0. «Ничего не менять» — 0 в переключатели
 * и 1 во флаги CTR. Здесь в поля mask ставятся значения want. */
static void ep_set(uint32_t n, uint16_t mask, uint16_t want)
{
    uint16_t r = (uint16_t)USB_RD(R_EPR(n));
    USB_WR(R_EPR(n), (r & EP_KEEP) | EP_CTR_RX | EP_CTR_TX | ((r ^ want) & mask));
}
static void ep_clear_ctr(uint32_t n, uint16_t flag)
{
    uint16_t r = (uint16_t)USB_RD(R_EPR(n));
    USB_WR(R_EPR(n), ((r & EP_KEEP) | EP_CTR_RX | EP_CTR_TX) & ~flag);
}

Размер приёмного буфера пишется в счётчик приёма блоками: до 62 байт — по 2 байта, дальше — по 32 (таблица 177; 64 байта — BL_SIZE = 1, NUM_BLOCK = 1):

/* Размер приёмного буфера в COUNTn_RX: до 62 байт — блоки по 2, дальше — по 32 (табл. 177) */
static uint16_t rx_count_field(uint16_t mps)
{
    return mps > 62u ? (uint16_t)(0x8000u | (mps / 32u - 1u) << 10) : (uint16_t)((mps / 2u) << 10);
}

Открытие точки: тип и номер — прямой записью, остальное — через ep_set. Первый пакет массовой точки — DATA0, поэтому DTOG сбрасывается вместе с состоянием:

void drv_ep_open(uint8_t addr, uint8_t type, uint16_t mps)
{
    static const uint16_t kind[4] = { 0x0200u, 0x0400u, 0x0000u, 0x0600u };   /* упр., изохр., масс., прер. */
    uint32_t n = addr & 0x0Fu;
    USB_WR(R_EPR(n), EP_CTR_RX | EP_CTR_TX | kind[type & 3u] | n);           /* тип и номер */
    if (addr & 0x80u) {
        BT_ADDR_TX(n) = BUF_TX(n);
        BT_COUNT_TX(n) = 0;
        ep_set(n, EP_STAT_TX | EP_DTOG_TX, TX_NAK);     /* первый пакет — DATA0 */
    } else {
        BT_ADDR_RX(n) = BUF_RX(n);
        BT_COUNT_RX(n) = rx_count_field(mps);
        ep_set(n, EP_STAT_RX | EP_DTOG_RX, RX_NAK);
    }
}

Адрес — после стадии состояния. У OTG адрес пишут в регистр сразу по SETUP: так велит RM0090 (34.17.5, «Endpoint initialization on SetAddress command»), так сделано в 198ш. У F103 наоборот: пока хост не забрал пустой пакет состояния, устройство должно отвечать по адресу 0. Драйвер запоминает адрес и пишет его в DADDR по CTR_TX точки 0:

void drv_set_address(uint8_t addr)
{
    dev_addr = addr;                    /* в DADDR — только после стадии состояния */
    addr_pending = 1;
}
void usb_f103_irq(void)
{
    uint16_t istr;
    while ((istr = (uint16_t)USB_RD(R_ISTR)) & (ISTR_CTR | ISTR_RESET)) {
        if (istr & ISTR_RESET) {
            USB_WR(R_ISTR, (uint16_t)~ISTR_RESET);              /* флаги ISTR сбрасываются нулём */
            bus_reset();
            continue;
        }
        uint32_t n = istr & ISTR_EP_ID;
        uint16_t r = (uint16_t)USB_RD(R_EPR(n));
        if (r & EP_CTR_RX) {
            uint16_t cnt = BT_COUNT_RX(n) & 0x3FFu;
            if (cnt > sizeof rxbuf) cnt = sizeof rxbuf;
            pma_read((uint16_t)BUF_RX(n), rxbuf, cnt);
            ep_clear_ctr(n, EP_CTR_RX);
            if (n == 0) {
                if (r & EP_SETUP) {
                    ep0_stalled = 0;
                    usb_setup(rxbuf);
                } else {
                    usb_ep0_out(rxbuf, cnt);
                }
                if (!ep0_stalled) ep_set(0, EP_STAT_RX, RX_VALID);
            } else if (usb_ep_out((uint8_t)n, rxbuf, cnt)) {
                ep_set(n, EP_STAT_RX, RX_VALID);
            }
        }
        if (r & EP_CTR_TX) {
            ep_clear_ctr(n, EP_CTR_TX);
            if (n == 0) {
                if (addr_pending) {                             /* стадия состояния SET_ADDRESS прошла */
                    addr_pending = 0;
                    USB_WR(R_DADDR, DADDR_EF | dev_addr);
                }
                usb_ep0_in_done();
            } else {
                usb_ep_in_done((uint8_t)(0x80u | n));
            }
        }
    }
}

Включение — по 23.4.2: снять PDWN, подождать запуска приёмопередатчика (tSTARTUP — до 1 мкс, таблица 43 DocID13587), снять FRES, сбросить флаги.

/* Включение: PDWN снять, подождать запуска приёмопередатчика (tSTARTUP до 1 мкс),
 * снять FRES, сбросить ложные флаги. Такт USB и APB1 к этому моменту включены. */
void usb_f103_init(void)
{
    USB_WR(R_CNTR, CNTR_FRES);
    for (volatile uint32_t i = 0; i < 100u; i++) { }
    USB_WR(R_CNTR, 0);
    USB_WR(R_ISTR, 0);
    USB_WR(R_CNTR, CNTR_CTRM | CNTR_RESETM);
}

Модель блока. На компьютере драйвер собран вместе с моделью блока USB FS: регистры, память пакетов и транзакции SETUP/IN/OUT ведут себя так, как их описывает RM0008. Переключатели меняются записью 1, CTR — сбрасывается нулём. SETUP принимается при STAT_RX = NAK и STALL, но не при поднятом CTR_RX. Пакет с чужим DATA0/1 подтверждается и выбрасывается. Устройство с чужим адресом молчит. Хост (host_model.py) ходит к модели как по шине и следит за DATA0/1 сам. Прогон начинается со сброса шины:

сброс шины: адрес 0, DADDR 0x80, EP0R 0x3220
SETUP 80 06 00 01 00 00 40 00  GET_DESCRIPTOR(DEVICE 0, 64): IN 18 = 18 байт
  12 01 01 02 00 00 00 40 09 12 02 00 00 01 01 02 03 01
SETUP 00 05 05 00 00 00 00 00  SET_ADDRESS(5): состояние принято
  адрес устройства: 5 (DADDR 0x85)

EP0R = 0x3220: приём разрешён (STAT_RX = 11), передача — NAK (STAT_TX = 10), тип CONTROL (01). Адрес 5 появился в DADDR только после стадии состояния. Если записать его сразу, как у OTG, хост теряет устройство:

== 6. Ошибка: адрес записан в DADDR сразу, в стадии SETUP
SETUP 80 06 00 01 00 00 40 00  GET_DESCRIPTOR(DEVICE 0, 64): IN 18 = 18 байт
  SET_ADDRESS(5): состояние IN: нет ответа; DADDR 0x85 — устройство уже слушает адрес 5

За весь прогон драйвер записал в EPnR 192 раза, и прогон прошёл без расхождений: перевёрнутый бит развёл бы DATA0/1 или состояния точек.

F407. Драйвер OTG_FS — из 198ш.6 с тремя отличиями: адрес блока 0x50000000; VBUS выключается битом NOVBUSSENS (бит 21 GCCFG), а у U585 тот же бит — VBDEN с обратным смыслом (раздел 20.5); подмены сигнала сессии в GOTGCTL у F407 нет. Память FIFO — 1,25 КБ, 320 слов (34.2); приёмному FIFO отдано 64 слова, по расчёту 34.13.1 надо не меньше 47: 10 на SETUP, 1 на Global OUT NAK, два пакета по 64/4 + 1 и по слову на каждую точку OUT.

/* Ядро OTG_FS: такт AHB2 и выводы PA11/PA12 к этому моменту включены (main_f407.c) */
void otg_f407_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 | GCCFG_NOVBUSSENS;       /* приёмопередатчик включён, VBUS не следим */
    REG(R_GUSBCFG) = (REG(R_GUSBCFG) & ~GUSBCFG_TRDT_Msk) | GUSBCFG_FDMOD | 6u << GUSBCFG_TRDT_Pos;
    REG(R_DCTL) |= DCTL_SDIS;                             /* пока не готовы — подтяжка D+ снята */
    REG(R_PCGCCTL) = 0;
    REG(R_DCFG) |= DCFG_DSPD_FS;
    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+: хост видит устройство */
}

Смещения и биты обоих драйверов сверены с заголовками CMSIS при сборке: selfcheck_f103.c и selfcheck_f407.c состоят из _Static_assert.


20д.6 Программа на ПК, эхо и размеры

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

/* Эхо: принятый пакет, «посоленный» ключом, уходит обратно в точку IN.
 * Пока ответ не забран хостом, новых пакетов OUT не принимаем — хост получит NAK. */
int usb_ep_out(uint8_t addr, const uint8_t *p, uint16_t n)
{
    if (addr != EP_OUT || app.in_busy) return 0;
    for (uint16_t i = 0; i < n; i++)
        app.pkt[i] = p[i] ^ app.xor_key;
    app.rx_bytes += n;
    app.in_len = n;
    app.in_busy = 1;
    (void)drv_ep_tx(EP_IN, app.pkt, n);    /* точка IN свободна: за этим следит in_busy */
    return 0;                               /* приём откроет usb_ep_in_done */
}
void usb_ep_in_done(uint8_t addr)
{
    if (addr != EP_IN || !app.in_busy) return;
    app.tx_bytes += app.in_len;
    app.in_busy = 0;
    drv_ep_rx_arm(EP_OUT);
}

Программа на ПК (vend_pc.py) обращается к прибору тремя вызовами в духе pyusb: ctrl_transfer, write, read. На настоящем приборе объект устройства даёт pyusb поверх libusb; в проверке — модель хоста с теми же тремя методами.

def get_info(dev):
    b = bytes(dev.ctrl_transfer(REQ_VENDOR_IN, VREQ_GET_INFO, 0, 0, 12))
    return {"version": "%d.%02d" % (b[1], b[0]), "led": b[2], "xor": b[3],
            "rx": int.from_bytes(b[4:8], "little"), "tx": int.from_bytes(b[8:12], "little")}


def set_xor(dev, key):
    dev.ctrl_transfer(REQ_VENDOR_OUT, VREQ_SET_XOR, 0, 0, bytes([key]))


def echo(dev, data):
    """Отправить данные пакетами по 64 байта; на каждый пакет устройство отвечает своим."""
    back = b""
    for i in range(0, len(data), 64):
        chunk = data[i:i + 64]
        dev.write(EP_OUT, chunk)
        back += bytes(dev.read(EP_IN, 64))
    return back

Прогон программы поверх модели F103:

SETUP C0 01 00 00 00 00 0C 00  производитель IN: bRequest 0x01, wValue 0, wIndex 0, wLength 12: IN 12 = 12 байт
состояние: {'version': '1.00', 'led': 0, 'xor': 0, 'rx': 0, 'tx': 0}
SETUP 40 02 01 00 00 00 00 00  производитель OUT: bRequest 0x02, wValue 1, wIndex 0, wLength 0: состояние принято
SETUP 40 03 00 00 00 00 01 00  производитель OUT: bRequest 0x03, wValue 0, wIndex 0, wLength 1: OUT 1 байт, состояние принято
эхо: отправлено 200, принято 200, совпало с ожидаемым: да
SETUP C0 01 00 00 00 00 0C 00  производитель IN: bRequest 0x01, wValue 0, wIndex 0, wLength 12: IN 12 = 12 байт
состояние: {'version': '1.00', 'led': 1, 'xor': 90, 'rx': 200, 'tx': 200}
светодиод: 1, вызовов board_led: 1
программа на ПК: работает

200 байт ушли четырьмя пакетами (64 + 64 + 64 + 8) и вернулись теми же байтами, сложенными по XOR с ключом 0x5A. Светодиод включён одним вызовом board_led.

Поток и остановка точки:

два пакета OUT без чтения: первый ACK, второй NAK
IN: 64 байт, первый байт 0x5B
повтор второго: ACK, IN 64 байт, первый байт 0x58
IN без запроса: NAK
SETUP 02 03 00 00 01 00 00 00  SET_FEATURE(ENDPOINT_HALT, точка 0x01): состояние принято
OUT в остановленную точку: STALL
SETUP 82 00 00 00 01 00 02 00  GET_STATUS: IN 2 = 2 байт
  GET_STATUS точки 0x01: 01 00
SETUP 02 01 00 00 01 00 00 00  CLEAR_FEATURE(ENDPOINT_HALT, точка 0x01): состояние принято
после CLEAR_FEATURE: OUT ACK, IN 1 байт 0x5E, EP1R 0x7061
SETUP C0 01 00 00 00 00 0C 00  производитель IN: bRequest 0x01, wValue 0, wIndex 0, wLength 12: IN 12 = 12 байт
состояние: {'version': '1.00', 'led': 1, 'xor': 90, 'rx': 329, 'tx': 329}
записей в EPnR драйвером за прогон: 192

В Windows по GUID прибор находят функциями SetupAPI: SetupDiGetClassDevsW с флагами DIGCF_PRESENT | DIGCF_DEVICEINTERFACE, затем SetupDiEnumDeviceInterfaces и SetupDiGetDeviceInterfaceDetailW — путь устройства; файл по этому пути открывают и передают в WinUsb_Initialize. Дальше — WinUsb_ControlTransfer, WinUsb_WritePipe, WinUsb_ReadPipe, WinUsb_ResetPipe (SetupAPI.h, winusb.h Windows SDK 10.0.19041). Такая программа в разделе не писалась и не запускалась.

Размеры — arm-none-eabi-gcc 12.2, -Os, без библиотеки HAL:

Файл Cortex-M3 (F103): код / данные / ОЗУ Cortex-M4 (F407): код / данные / ОЗУ
vend_desc.c — дескрипторы и строки 441 / 25 / 0 441 / 25 / 0
vend_core.c — точка 0, запросы, эхо 948 / 0 / 168 948 / 0 / 168
драйвер: usb_f103.c / otg_f407.c 974 / 0 / 67 1090 / 0 / 148
main_f103.c / main_f407.c — такты, выводы, таблица векторов 406 / 0 / 0 718 / 0 / 0
образ целиком 3160 / 28 / 236 3664 / 28 / 316

Все четыре дескриптора в образе F103 — 18 + 32 + 33 + 162 = 245 байт Flash. Прибор на F103 целиком меньше 3,5 КБ, на F407 — около 3,6 КБ: без библиотеки ST и без RTOS.


20д.7 Грабли

WinUSB не встаёт, хотя набор в прошивке есть. Проверьте bcdUSB. С версией 2.00 хост BOS не спрашивает (20д.3): набор не запрошен ни разу.

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

Вместо набора — ответ своей команды. Свой запрос производителя взял тот же bRequest, что bMS_VendorCode, и не смотрит на wIndex. Разведите коды; набор отдавайте только на wIndex 7.

Набор не отдаётся при опознании. Обработчик требует SET_CONFIGURATION, а хост просит набор раньше. MS OS — в любом состоянии, свои команды — после.

GUID не находится программой. Строка без фигурных скобок, не в UTF-16LE или без второго нуля в конце списка REG_MULTI_SZ. У нас 38 знаков в скобках и 4 нулевых байта в конце.

F103 перестаёт отвечать после SET_ADDRESS. Адрес записан в DADDR в стадии SETUP: пустой пакет состояния хост шлёт ещё на адрес 0, а прибор слушает уже 5 (20д.5, «нет ответа»). У OTG — наоборот, адрес пишут сразу.

Точки F103 «сходят с ума» после записи в EPnR. Чтение-изменение-запись переворачивает биты STAT и DTOG и сбрасывает чужие флаги CTR. Только запись через исключающее ИЛИ с маской и с единицами в CTR.

После снятия остановки пропал первый пакет. Хост или прибор не вернулся к DATA0. После CLEAR_FEATURE(ENDPOINT_HALT) — DATA0 у обоих.

Прибор на F407 не виден, хотя код с U585 работал. Бит 21 GCCFG: у U585 это VBDEN (0 — не следить за VBUS), у F407 — NOVBUSSENS (1 — не следить). Перенос строки «как есть» включает контроль VBUS.

Подписи и кэш Windows. Как Windows запоминает ответ на MS OS-дескрипторы и нужно ли менять bcdDevice или PID после правки набора, в разделе не проверялось. На макете надёжнее не трогать набор после первого подключения к ПК без смены PID.


20д.8 Итоги

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

vend.h — общий заголовок устройства: точки, запросы, длины дескрипторов.

/* vend.h — своё USB-устройство без драйвера: класс 0xFF, две массовые точки,
 * BOS с возможностью MS OS 2.0 и совместимый ID «WINUSB». Раздел 20д.
 * Общая часть без железа: дескрипторы, точка 0, эхо. Драйверы — usb_f103.c, otg_f407.c. */
#ifndef VEND_H
#define VEND_H
#include <stdint.h>

#ifndef USB_BCD_USB
#define USB_BCD_USB    0x0201u      /* 2.01: только у такого устройства хост спрашивает BOS */
#endif
#define USB_VID        0x1209u      /* заглушки для макета: в изделии — свои номера */
#define USB_PID        0x0002u
#define EP0_SIZE       64u
#define EP_OUT         0x01u        /* массовая OUT, 64 байта */
#define EP_IN          0x81u        /* массовая IN, 64 байта */
#define BULK_SIZE      64u
#define EP_TYPE_BULK   2u

#define MS_VENDOR_CODE 0x20u        /* bRequest, которым хост просит набор MS OS 2.0 */
#define MS_OS_20_INDEX 7u           /* wIndex того же запроса */
#define VREQ_GET_INFO  0x01u        /* свои запросы: IN, 12 байт состояния */
#define VREQ_SET_LED   0x02u        /* без данных: wValue = 0 или 1 */
#define VREQ_SET_XOR   0x03u        /* OUT, 1 байт: им «солится» эхо */
#define INFO_SIZE      12u

#define DEVICE_DESC_SIZE 18u
#define CONFIG_DESC_SIZE 32u
#define BOS_DESC_SIZE    33u
#define MSOS20_SET_SIZE  162u

#define LE16(x) ((x) & 0xFFu), (((x) >> 8) & 0xFFu)

/* длины массивов — в макросах выше; совпадение проверяет _Static_assert в vend_desc.c */
extern const uint8_t usb_device_desc[];
extern const uint8_t usb_config_desc[];
extern const uint8_t usb_bos_desc[];
extern const uint8_t usb_msos20_set[];
uint16_t usb_string_desc(uint8_t index, uint8_t *buf);  /* длина или 0, если строки нет */
void usb_serial_from_uid(const uint32_t uid[3]);        /* 96 бит номера кристалла -> 24 знака */

/* --- что ядро просит у драйвера (usb_f103.c, otg_f407.c; в модели — usb_f103.c + usbfs_model.c) --- */
void drv_ep0_tx(const uint8_t *p, uint16_t n);          /* один пакет IN точки 0, n <= 64 */
void drv_ep0_stall(void);
void drv_set_address(uint8_t addr);                     /* вызывается в стадии SETUP */
void drv_ep_open(uint8_t addr, uint8_t type, uint16_t mps);
void drv_ep_close(uint8_t addr);
int  drv_ep_tx(uint8_t addr, const uint8_t *p, uint16_t n);  /* 0 — пакет взят, -1 — точка занята */
void drv_ep_rx_arm(uint8_t addr);                       /* снова принимать OUT */
void drv_ep_stall(uint8_t addr, int on);

/* --- что драйвер сообщает ядру (vend_core.c) --- */
void usb_reset(void);
void usb_setup(const uint8_t s[8]);
void usb_ep0_out(const uint8_t *p, uint16_t n);
void usb_ep0_in_done(void);
int  usb_ep_out(uint8_t addr, const uint8_t *p, uint16_t n); /* 1 — можно сразу принимать дальше */
void usb_ep_in_done(uint8_t addr);
uint8_t usb_configured(void);

void board_led(uint8_t on);                             /* светодиод платы */
#endif

vend_desc.c — дескрипторы по байтам: устройство, конфигурация, BOS, MS OS 2.0, строки.

/* vend_desc.c — дескрипторы устройства по байтам: устройство, конфигурация, BOS,
 * набор MS OS 2.0 и строки. Раздел 20д. Длины, записанные внутри дескрипторов,
 * берутся из макросов vend.h, а _Static_assert в конце сверяет их с числом байт. */
#include "vend.h"

#define U16(ch) (ch), 0x00          /* знак UTF-16LE */

const uint8_t usb_device_desc[] = {
    18, 0x01,                   /* bLength, DEVICE */
    LE16(USB_BCD_USB),          /* bcdUSB 2.01: у устройства есть BOS */
    0x00, 0x00, 0x00,           /* класс, подкласс, протокол — смотри интерфейс */
    EP0_SIZE,                   /* bMaxPacketSize0 */
    LE16(USB_VID),
    LE16(USB_PID),
    0x00, 0x01,                 /* bcdDevice 1.00 */
    1, 2, 3,                    /* строки: изготовитель, изделие, серийный номер */
    1                           /* одна конфигурация */
};

const uint8_t usb_config_desc[] = {
    /* конфигурация */
    9, 0x02, LE16(CONFIG_DESC_SIZE),    /* wTotalLength = 32 */
    1,                          /* bNumInterfaces */
    1,                          /* bConfigurationValue */
    0,                          /* iConfiguration */
    0x80,                       /* bmAttributes: питание от шины */
    50,                         /* bMaxPower: 50 x 2 мА = 100 мА */
    /* интерфейс 0: свой класс */
    9, 0x04, 0, 0, 2,           /* номер 0, настройка 0, две точки */
    0xFF, 0x00, 0x00,           /* класс 0xFF «у производителя», подкласс и протокол — свои */
    0,                          /* iInterface */
    /* точки */
    7, 0x05, EP_OUT, EP_TYPE_BULK, LE16(BULK_SIZE), 0,
    7, 0x05, EP_IN, EP_TYPE_BULK, LE16(BULK_SIZE), 0
};

const uint8_t usb_bos_desc[] = {
    5, 0x0F,                    /* bLength, BOS */
    LE16(BOS_DESC_SIZE),        /* wTotalLength = 33: корень и все возможности */
    1,                          /* bNumDeviceCaps */
    /* платформенная возможность MS OS 2.0 */
    28, 0x10, 0x05,             /* bLength, DEVICE_CAPABILITY, PLATFORM */
    0x00,                       /* bReserved */
    0xDF, 0x60, 0xDD, 0xD8, 0x89, 0x45, 0xC7, 0x4C,     /* UUID D8DD60DF-4589-4CC7- */
    0x9C, 0xD2, 0x65, 0x9D, 0x9E, 0x64, 0x8A, 0x9F,     /*      9CD2-659D9E648A9F */
    0x00, 0x00, 0x00, 0x0A,     /* dwWindowsVersion 0x0A000000: Windows 10 */
    LE16(MSOS20_SET_SIZE),      /* wMSOSDescriptorSetTotalLength = 162 */
    MS_VENDOR_CODE,             /* bMS_VendorCode: этим bRequest набор и попросят */
    0x00                        /* bAltEnumCode: второго варианта нет */
};

const uint8_t usb_msos20_set[] = {
    /* заголовок набора */
    0x0A, 0x00,                 /* wLength = 10 */
    0x00, 0x00,                 /* MS_OS_20_SET_HEADER_DESCRIPTOR */
    0x00, 0x00, 0x00, 0x0A,     /* dwWindowsVersion 0x0A000000 */
    LE16(MSOS20_SET_SIZE),      /* wTotalLength = 162: весь набор */
    /* совместимый ID: какой драйвер ставить */
    0x14, 0x00,                 /* wLength = 20 */
    0x03, 0x00,                 /* MS_OS_20_FEATURE_COMPATIBLE_ID */
    'W', 'I', 'N', 'U', 'S', 'B', 0x00, 0x00,           /* CompatibleID */
    0, 0, 0, 0, 0, 0, 0, 0,     /* SubCompatibleID: нет */
    /* свойство реестра: GUID, по которому программа найдёт устройство */
    0x84, 0x00,                 /* wLength = 132 */
    0x04, 0x00,                 /* MS_OS_20_FEATURE_REG_PROPERTY */
    0x07, 0x00,                 /* wPropertyDataType: REG_MULTI_SZ, список строк */
    0x2A, 0x00,                 /* wPropertyNameLength = 42 */
    U16('D'), U16('e'), U16('v'), U16('i'), U16('c'), U16('e'),
    U16('I'), U16('n'), U16('t'), U16('e'), U16('r'), U16('f'), U16('a'), U16('c'), U16('e'),
    U16('G'), U16('U'), U16('I'), U16('D'), U16('s'), 0x00, 0x00,
    0x50, 0x00,                 /* wPropertyDataLength = 80 */
    U16('{'), U16('9'), U16('D'), U16('E'), U16('E'), U16('3'), U16('3'), U16('1'), U16('4'),
    U16('-'), U16('0'), U16('6'), U16('2'), U16('6'),
    U16('-'), U16('4'), U16('A'), U16('2'), U16('1'),
    U16('-'), U16('B'), U16('C'), U16('E'), U16('4'),
    U16('-'), U16('9'), U16('4'), U16('5'), U16('6'), U16('A'), U16('E'), U16('2'), U16('2'),
    U16('7'), U16('C'), U16('2'), U16('8'), U16('}'),
    0x00, 0x00, 0x00, 0x00      /* конец строки и конец списка */
};

_Static_assert(sizeof usb_device_desc == DEVICE_DESC_SIZE, "дескриптор устройства: не 18 байт");
_Static_assert(sizeof usb_config_desc == CONFIG_DESC_SIZE, "конфигурация: wTotalLength не сходится");
_Static_assert(sizeof usb_bos_desc == BOS_DESC_SIZE, "BOS: wTotalLength не сходится");
_Static_assert(sizeof usb_msos20_set == MSOS20_SET_SIZE, "набор MS OS 2.0: wTotalLength не сходится");

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];
}

uint16_t usb_string_desc(uint8_t index, uint8_t *buf)
{
    static const char *const text[] = { "STM32 Handbook", "WinUSB echo 20d", serial };
    if (index == 0) {                       /* список языков: 0x0409, английский (США) */
        buf[0] = 4; buf[1] = 0x03; buf[2] = 0x09; buf[3] = 0x04;
        return 4;
    }
    if (index > 3) return 0;                /* в том числе 0xEE: MS OS 1.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;
}

vend_core.c — точка 0, запросы производителя и эхо.

/* vend_core.c — точка 0 (стандартные запросы, BOS, запросы производителя)
 * и эхо по массовым точкам. Раздел 20д. Железа не касается: всё через drv_*. */
#include <string.h>
#include "vend.h"

enum { REQ_STALL = -1, REQ_NODATA = 0, REQ_IN = 1, REQ_OUT = 2 };

static struct {
    const uint8_t *tx;          /* стадия данных IN: что осталось отдать */
    uint16_t tx_left;
    uint8_t tx_zlp;             /* после полных пакетов нужен пустой */
    uint8_t setup[8];           /* запрос, чьи данные OUT ещё идут */
    uint16_t rx_want, rx_len;
    uint8_t buf[64];
    uint8_t config;
    uint8_t halt_in, halt_out;
} c;

static struct {
    uint8_t led, xor_key, in_busy;
    uint16_t in_len;
    uint32_t rx_bytes, tx_bytes;
    uint8_t pkt[BULK_SIZE];
} app;

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); }
}

static void ep0_status(void) { drv_ep0_tx(0, 0); }

static int get_descriptor(uint8_t type, uint8_t index, const uint8_t **p, uint16_t *len)
{
    switch (type) {
    case 0x01: *p = usb_device_desc; *len = DEVICE_DESC_SIZE; return 1;
    case 0x02: *p = usb_config_desc; *len = CONFIG_DESC_SIZE; return index == 0;
    case 0x03: *p = c.buf; *len = usb_string_desc(index, c.buf); return *len != 0;
    case 0x0F: *p = usb_bos_desc; *len = BOS_DESC_SIZE; return index == 0;
    default: return 0;          /* 0x06 DEVICE_QUALIFIER: только полная скорость — отказ */
    }
}

static uint8_t *halt_flag(uint8_t ep)
{
    if (!c.config) return 0;
    if (ep == EP_IN) return &c.halt_in;
    if (ep == EP_OUT) return &c.halt_out;
    return 0;
}

static void set_config(uint8_t v)
{
    if (c.config) {
        drv_ep_close(EP_OUT);
        drv_ep_close(EP_IN);
    }
    c.config = v;
    c.halt_in = c.halt_out = 0;
    app.in_busy = 0;
    if (v) {
        drv_ep_open(EP_OUT, EP_TYPE_BULK, BULK_SIZE);
        drv_ep_open(EP_IN, EP_TYPE_BULK, BULK_SIZE);
        drv_ep_rx_arm(EP_OUT);
    }
}

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);
        if (!*h) {              /* остановку сняли: эхо начинается с чистого листа */
            app.in_busy = 0;
            if (!c.halt_out) drv_ep_rx_arm(EP_OUT);
        }
        return REQ_NODATA;
    case 0x05:                  /* SET_ADDRESS: когда писать в регистр, решает драйвер */
        drv_set_address((uint8_t)(wvalue & 0x7Fu));
        return REQ_NODATA;
    case 0x06:                  /* GET_DESCRIPTOR */
        return get_descriptor(s[3], s[2], 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 != 0) return REQ_STALL;
        c.buf[0] = 0;
        *p = c.buf; *len = 1;
        return REQ_IN;
    case 0x0B:                  /* SET_INTERFACE */
        return (c.config && windex == 0 && wvalue == 0) ? REQ_NODATA : REQ_STALL;
    default:
        return REQ_STALL;
    }
}

static void put32(uint8_t *d, uint32_t v)
{
    d[0] = (uint8_t)v; d[1] = (uint8_t)(v >> 8); d[2] = (uint8_t)(v >> 16); d[3] = (uint8_t)(v >> 24);
}

/* Запросы производителя к устройству. Набор MS OS 2.0 отдаётся только на свой
 * bRequest и wIndex = 7 и в любом состоянии — хост просит его до SET_CONFIGURATION.
 * Свои команды — только в настроенном устройстве. */
static int vendor_request(const uint8_t s[8], const uint8_t **p, uint16_t *len)
{
    uint8_t in = s[0] & 0x80u;
    uint16_t wvalue = (uint16_t)(s[2] | s[3] << 8), windex = (uint16_t)(s[4] | s[5] << 8);
    if (s[1] == MS_VENDOR_CODE) {
        if (!in || windex != MS_OS_20_INDEX) return REQ_STALL;
        *p = usb_msos20_set; *len = MSOS20_SET_SIZE;
        return REQ_IN;
    }
    if (!c.config) return REQ_STALL;
    switch (s[1]) {
    case VREQ_GET_INFO:         /* версия, светодиод, ключ, счётчики байт */
        if (!in) return REQ_STALL;
        c.buf[0] = 0x00; c.buf[1] = 0x01;           /* версия протокола 1.00 */
        c.buf[2] = app.led;
        c.buf[3] = app.xor_key;
        put32(c.buf + 4, app.rx_bytes);
        put32(c.buf + 8, app.tx_bytes);
        *p = c.buf; *len = INFO_SIZE;
        return REQ_IN;
    case VREQ_SET_LED:
        if (in || wvalue > 1) return REQ_STALL;
        app.led = (uint8_t)wvalue;
        board_led(app.led);
        return REQ_NODATA;
    case VREQ_SET_XOR:          /* байт ключа придёт в стадии данных */
        return in ? REQ_STALL : REQ_OUT;
    default:
        return REQ_STALL;
    }
}

static int vendor_data(const uint8_t s[8], const uint8_t *d, uint16_t n)
{
    if (s[1] != VREQ_SET_XOR || n != 1) return REQ_STALL;
    app.xor_key = d[0];
    return REQ_NODATA;
}

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, rcpt = s[0] & 0x1Fu;
    int r = REQ_STALL;

    c.rx_want = 0;
    if (type == 0) r = standard_request(s, &p, &len);
    else if (type == 2 && rcpt == 0) r = vendor_request(s, &p, &len);    /* производитель, устройству */
    if (r == REQ_IN && (s[0] & 0x80u) && wlength) {
        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 && !wlength) {
        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;
    if (vendor_data(c.setup, c.buf, c.rx_len) == REQ_NODATA) ep0_status();
    else drv_ep0_stall();
}

void usb_reset(void)
{
    memset(&c, 0, sizeof c);
    app.in_busy = 0;
}

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

/* Эхо: принятый пакет, «посоленный» ключом, уходит обратно в точку IN.
 * Пока ответ не забран хостом, новых пакетов OUT не принимаем — хост получит NAK. */
int usb_ep_out(uint8_t addr, const uint8_t *p, uint16_t n)
{
    if (addr != EP_OUT || app.in_busy) return 0;
    for (uint16_t i = 0; i < n; i++)
        app.pkt[i] = p[i] ^ app.xor_key;
    app.rx_bytes += n;
    app.in_len = n;
    app.in_busy = 1;
    (void)drv_ep_tx(EP_IN, app.pkt, n);    /* точка IN свободна: за этим следит in_busy */
    return 0;                               /* приём откроет usb_ep_in_done */
}

void usb_ep_in_done(uint8_t addr)
{
    if (addr != EP_IN || !app.in_busy) return;
    app.tx_bytes += app.in_len;
    app.in_busy = 0;
    drv_ep_rx_arm(EP_OUT);
}

usb_f103.c — драйвер USB FS STM32F103 регистрами.

/* usb_f103.c — драйвер USB FS STM32F103 в режиме устройства, регистрами (RM0008, глава 23).
 * Раздел 20д. Буферы точек лежат в памяти пакетов (512 байт): процессор видит её
 * 16-битными словами через 32 бита, поэтому локальный адрес умножается на два.
 * Точка n: передача с 0x40 + 0x80·n, приём с 0x80 + 0x80·n, по 64 байта; таблица — с 0. */
#include "vend.h"

#ifdef HOST_MODEL                       /* модель на компьютере: регистры — функции модели */
uint16_t usbm_read(uint32_t off);
void usbm_write(uint32_t off, uint16_t v);
extern uint8_t usbm_pma[1024];
#define USB_RD(off)     usbm_read(off)
#define USB_WR(off, v)  usbm_write((off), (uint16_t)(v))
#define PMA_BASE        ((uintptr_t)usbm_pma)
#else
#define USB_RD(off)     (*(volatile uint32_t *)(0x40005C00u + (off)))
#define USB_WR(off, v)  (*(volatile uint32_t *)(0x40005C00u + (off)) = (uint16_t)(v))
#define PMA_BASE        0x40006000u
#endif
#define PMA16(a)        (*(volatile uint16_t *)(PMA_BASE + 2u * (a)))

#define R_EPR(n)    (4u * (n))
#define R_CNTR      0x40u
#define R_ISTR      0x44u
#define R_DADDR     0x4Cu
#define R_BTABLE    0x50u

#define CNTR_FRES   0x0001u
#define CNTR_RESETM 0x0400u
#define CNTR_CTRM   0x8000u
#define ISTR_EP_ID  0x000Fu
#define ISTR_RESET  0x0400u
#define ISTR_CTR    0x8000u
#define DADDR_EF    0x0080u

#define EP_CTR_RX   0x8000u             /* сбрасывается записью 0 */
#define EP_DTOG_RX  0x4000u             /* переключается записью 1 */
#define EP_STAT_RX  0x3000u             /* переключается записью 1 */
#define EP_SETUP    0x0800u             /* только чтение */
#define EP_TYPE     0x0600u
#define EP_KIND     0x0100u
#define EP_CTR_TX   0x0080u
#define EP_DTOG_TX  0x0040u
#define EP_STAT_TX  0x0030u
#define EP_EA       0x000Fu
#define EP_KEEP     (EP_TYPE | EP_KIND | EP_EA)
#define RX_STALL 0x1000u
#define RX_NAK   0x2000u
#define RX_VALID 0x3000u
#define TX_STALL 0x0010u
#define TX_NAK   0x0020u
#define TX_VALID 0x0030u

#define BT_ADDR_TX(n)  PMA16(8u * (n))
#define BT_COUNT_TX(n) PMA16(8u * (n) + 2u)
#define BT_ADDR_RX(n)  PMA16(8u * (n) + 4u)
#define BT_COUNT_RX(n) PMA16(8u * (n) + 6u)
#define BUF_TX(n)      (0x40u + 0x80u * (n))
#define BUF_RX(n)      (0x80u + 0x80u * (n))

static uint8_t rxbuf[64];
static uint8_t addr_pending, dev_addr, ep0_stalled;

/* Запись в EPnR без чтения-изменения-записи: STAT и DTOG меняются записью 1,
 * флаги CTR сбрасываются записью 0. «Ничего не менять» — 0 в переключатели
 * и 1 во флаги CTR. Здесь в поля mask ставятся значения want. */
static void ep_set(uint32_t n, uint16_t mask, uint16_t want)
{
    uint16_t r = (uint16_t)USB_RD(R_EPR(n));
    USB_WR(R_EPR(n), (r & EP_KEEP) | EP_CTR_RX | EP_CTR_TX | ((r ^ want) & mask));
}

static void ep_clear_ctr(uint32_t n, uint16_t flag)
{
    uint16_t r = (uint16_t)USB_RD(R_EPR(n));
    USB_WR(R_EPR(n), ((r & EP_KEEP) | EP_CTR_RX | EP_CTR_TX) & ~flag);
}

/* Размер приёмного буфера в COUNTn_RX: до 62 байт — блоки по 2, дальше — по 32 (табл. 177) */
static uint16_t rx_count_field(uint16_t mps)
{
    return mps > 62u ? (uint16_t)(0x8000u | (mps / 32u - 1u) << 10) : (uint16_t)((mps / 2u) << 10);
}

static void pma_write(uint16_t a, const uint8_t *p, uint16_t n)
{
    for (uint16_t i = 0; i < n; i += 2) {
        uint16_t w = p[i];
        if (i + 1u < n) w |= (uint16_t)(p[i + 1] << 8);
        PMA16(a + i) = w;
    }
}

static void pma_read(uint16_t a, uint8_t *p, uint16_t n)
{
    for (uint16_t i = 0; i < n; i += 2) {
        uint16_t w = PMA16(a + i);
        p[i] = (uint8_t)w;
        if (i + 1u < n) p[i + 1] = (uint8_t)(w >> 8);
    }
}

void drv_ep0_tx(const uint8_t *p, uint16_t n)
{
    pma_write(BUF_TX(0), p, n);
    BT_COUNT_TX(0) = n;
    ep_set(0, EP_STAT_TX, TX_VALID);
}

void drv_ep0_stall(void)
{
    ep_set(0, EP_STAT_TX | EP_STAT_RX, TX_STALL | RX_STALL);   /* SETUP примется и так */
    ep0_stalled = 1;
}

void drv_set_address(uint8_t addr)
{
    dev_addr = addr;                    /* в DADDR — только после стадии состояния */
    addr_pending = 1;
}

void drv_ep_open(uint8_t addr, uint8_t type, uint16_t mps)
{
    static const uint16_t kind[4] = { 0x0200u, 0x0400u, 0x0000u, 0x0600u };   /* упр., изохр., масс., прер. */
    uint32_t n = addr & 0x0Fu;
    USB_WR(R_EPR(n), EP_CTR_RX | EP_CTR_TX | kind[type & 3u] | n);           /* тип и номер */
    if (addr & 0x80u) {
        BT_ADDR_TX(n) = BUF_TX(n);
        BT_COUNT_TX(n) = 0;
        ep_set(n, EP_STAT_TX | EP_DTOG_TX, TX_NAK);     /* первый пакет — DATA0 */
    } else {
        BT_ADDR_RX(n) = BUF_RX(n);
        BT_COUNT_RX(n) = rx_count_field(mps);
        ep_set(n, EP_STAT_RX | EP_DTOG_RX, RX_NAK);
    }
}

void drv_ep_close(uint8_t addr)
{
    uint32_t n = addr & 0x0Fu;
    if (addr & 0x80u) ep_set(n, EP_STAT_TX, 0);
    else ep_set(n, EP_STAT_RX, 0);
}

int drv_ep_tx(uint8_t addr, const uint8_t *p, uint16_t n)
{
    uint32_t e = addr & 0x0Fu;
    if ((USB_RD(R_EPR(e)) & EP_STAT_TX) == TX_VALID) return -1;    /* прошлый пакет не забран */
    pma_write((uint16_t)BUF_TX(e), p, n);
    BT_COUNT_TX(e) = n;
    ep_set(e, EP_STAT_TX, TX_VALID);
    return 0;
}

void drv_ep_rx_arm(uint8_t addr)
{
    ep_set(addr & 0x0Fu, EP_STAT_RX, RX_VALID);
}

void drv_ep_stall(uint8_t addr, int on)
{
    uint32_t n = addr & 0x0Fu;
    if (addr & 0x80u) ep_set(n, EP_STAT_TX | (on ? 0u : EP_DTOG_TX), on ? TX_STALL : TX_NAK);
    else ep_set(n, EP_STAT_RX | (on ? 0u : EP_DTOG_RX), on ? RX_STALL : RX_NAK);
}

static void bus_reset(void)
{
    USB_WR(R_BTABLE, 0);
    BT_ADDR_TX(0) = BUF_TX(0);
    BT_COUNT_TX(0) = 0;
    BT_ADDR_RX(0) = BUF_RX(0);
    BT_COUNT_RX(0) = rx_count_field(EP0_SIZE);
    USB_WR(R_EPR(0), EP_CTR_RX | EP_CTR_TX | 0x0200u);         /* точка 0, управляющая */
    ep_set(0, EP_STAT_RX | EP_STAT_TX | EP_DTOG_RX | EP_DTOG_TX, RX_VALID | TX_NAK);
    USB_WR(R_DADDR, DADDR_EF);                                 /* адрес 0, функция включена */
    addr_pending = ep0_stalled = 0;
    usb_reset();
}

/* Включение: PDWN снять, подождать запуска приёмопередатчика (tSTARTUP до 1 мкс),
 * снять FRES, сбросить ложные флаги. Такт USB и APB1 к этому моменту включены. */
void usb_f103_init(void)
{
    USB_WR(R_CNTR, CNTR_FRES);
    for (volatile uint32_t i = 0; i < 100u; i++) { }
    USB_WR(R_CNTR, 0);
    USB_WR(R_ISTR, 0);
    USB_WR(R_CNTR, CNTR_CTRM | CNTR_RESETM);
}

void usb_f103_irq(void)
{
    uint16_t istr;
    while ((istr = (uint16_t)USB_RD(R_ISTR)) & (ISTR_CTR | ISTR_RESET)) {
        if (istr & ISTR_RESET) {
            USB_WR(R_ISTR, (uint16_t)~ISTR_RESET);              /* флаги ISTR сбрасываются нулём */
            bus_reset();
            continue;
        }
        uint32_t n = istr & ISTR_EP_ID;
        uint16_t r = (uint16_t)USB_RD(R_EPR(n));
        if (r & EP_CTR_RX) {
            uint16_t cnt = BT_COUNT_RX(n) & 0x3FFu;
            if (cnt > sizeof rxbuf) cnt = sizeof rxbuf;
            pma_read((uint16_t)BUF_RX(n), rxbuf, cnt);
            ep_clear_ctr(n, EP_CTR_RX);
            if (n == 0) {
                if (r & EP_SETUP) {
                    ep0_stalled = 0;
                    usb_setup(rxbuf);
                } else {
                    usb_ep0_out(rxbuf, cnt);
                }
                if (!ep0_stalled) ep_set(0, EP_STAT_RX, RX_VALID);
            } else if (usb_ep_out((uint8_t)n, rxbuf, cnt)) {
                ep_set(n, EP_STAT_RX, RX_VALID);
            }
        }
        if (r & EP_CTR_TX) {
            ep_clear_ctr(n, EP_CTR_TX);
            if (n == 0) {
                if (addr_pending) {                             /* стадия состояния SET_ADDRESS прошла */
                    addr_pending = 0;
                    USB_WR(R_DADDR, DADDR_EF | dev_addr);
                }
                usb_ep0_in_done();
            } else {
                usb_ep_in_done((uint8_t)(0x80u | n));
            }
        }
    }
}

otg_f407.c — драйвер OTG_FS STM32F407 регистрами.

/* otg_f407.c — драйвер OTG_FS STM32F407 в режиме устройства, регистрами (RM0090, глава 34).
 * Раздел 20д. Тот же драйвер, что для U585 в разделе 198ш, с тремя отличиями F407:
 * адрес блока 0x50000000; VBUS выключается битом NOVBUSSENS (бит 21 GCCFG, а у U585
 * этот бит — VBDEN с обратным смыслом); подмены сигнала сессии в GOTGCTL нет.
 * Каждая передача IN — один пакет не больше 64 байт: он целиком входит в свой FIFO. */
#include "vend.h"

#define OTG_BASE      0x50000000u               /* USB_OTG_FS_PERIPH_BASE */
#define REG(off)      (*(volatile uint32_t *)(OTG_BASE + (off)))
#define R_GAHBCFG     0x008u
#define R_GUSBCFG     0x00Cu
#define R_GRSTCTL     0x010u
#define R_GINTSTS     0x014u
#define R_GINTMSK     0x018u
#define R_GRXSTSP     0x020u
#define R_GRXFSIZ     0x024u
#define R_DIEPTXF0    0x028u
#define R_GCCFG       0x038u
#define R_DIEPTXF(n)  (0x104u + 4u * ((n) - 1u))
#define R_DCFG        0x800u
#define R_DCTL        0x804u
#define R_DIEPMSK     0x810u
#define R_DOEPMSK     0x814u
#define R_DAINT       0x818u
#define R_DAINTMSK    0x81Cu
#define R_DIEPCTL(n)  (0x900u + 0x20u * (n))
#define R_DIEPINT(n)  (0x908u + 0x20u * (n))
#define R_DIEPTSIZ(n) (0x910u + 0x20u * (n))
#define R_DOEPCTL(n)  (0xB00u + 0x20u * (n))
#define R_DOEPINT(n)  (0xB08u + 0x20u * (n))
#define R_DOEPTSIZ(n) (0xB10u + 0x20u * (n))
#define R_PCGCCTL     0xE00u
#define R_DFIFO(n)    (0x1000u + 0x1000u * (n))

#define GAHBCFG_GINT      (1u << 0)
#define GUSBCFG_PHYSEL    (1u << 6)
#define GUSBCFG_TRDT_Pos  10
#define GUSBCFG_TRDT_Msk  (0xFu << 10)
#define GUSBCFG_FDMOD     (1u << 30)
#define GRSTCTL_CSRST     (1u << 0)
#define GRSTCTL_RXFFLSH   (1u << 4)
#define GRSTCTL_TXFFLSH   (1u << 5)
#define GRSTCTL_TXFNUM_Pos 6
#define GRSTCTL_AHBIDL    (1u << 31)
#define GINT_RXFLVL       (1u << 4)
#define GINT_USBRST       (1u << 12)
#define GINT_ENUMDNE      (1u << 13)
#define GINT_IEPINT       (1u << 18)
#define GINT_OEPINT       (1u << 19)
#define GCCFG_PWRDWN      (1u << 16)
#define GCCFG_VBUSASEN    (1u << 18)
#define GCCFG_VBUSBSEN    (1u << 19)
#define GCCFG_NOVBUSSENS  (1u << 21)
#define DCFG_DSPD_FS      3u
#define DCFG_DAD_Pos      4
#define DCFG_DAD_Msk      (0x7Fu << 4)
#define DCTL_SDIS         (1u << 1)
#define DCTL_CGINAK       (1u << 8)
#define EPCTL_USBAEP      (1u << 15)
#define EPCTL_EPTYP_Pos   18
#define EPCTL_STALL       (1u << 21)
#define EPCTL_TXFNUM_Pos  22
#define EPCTL_CNAK        (1u << 26)
#define EPCTL_SNAK        (1u << 27)
#define EPCTL_SD0PID      (1u << 28)
#define EPCTL_EPDIS       (1u << 30)
#define EPCTL_EPENA       (1u << 31)
#define EPINT_XFRC        (1u << 0)
#define EPINT_STUP        (1u << 3)
#define TSIZ_PKTCNT_Pos   19
#define TSIZ_STUPCNT_Pos  29
#define RXSTS_EPNUM(s)    ((s) & 0xFu)
#define RXSTS_BCNT(s)     (((s) >> 4) & 0x7FFu)
#define RXSTS_PKTSTS(s)   (((s) >> 17) & 0xFu)
#define PKTSTS_OUT_DATA   2u
#define PKTSTS_SETUP_DATA 6u

#define FIFO_RAM_WORDS 320u     /* 1,25 КБ (RM0090, 34.2) */
#define FIFO_RX_WORDS  64u
#define FIFO_TX0_WORDS 16u      /* точка 0: пакет 64 байта */
#define FIFO_TX1_WORDS 16u      /* массовая IN: пакет 64 байта */

_Static_assert(FIFO_RX_WORDS + FIFO_TX0_WORDS + FIFO_TX1_WORDS <= FIFO_RAM_WORDS, "FIFO не помещаются");
_Static_assert(FIFO_RX_WORDS >= 10u + 1u + 2u * (64u / 4u + 1u) + 2u,
               "RX FIFO меньше расчёта RM0090 34.13.1: SETUP, Global OUT NAK, два пакета, по слову на точку OUT");

static uint16_t out_mps[4];
static uint8_t setup_pkt[8];
static uint8_t rx_buf[2][64];           /* принятые пакеты: [0] — точка 0, [1] — массовая OUT */
static uint16_t rx_len[2];

static void fifo_setup(void)
{
    REG(R_GRXFSIZ) = FIFO_RX_WORDS;
    REG(R_DIEPTXF0) = FIFO_TX0_WORDS << 16 | FIFO_RX_WORDS;              /* глубина | начало */
    REG(R_DIEPTXF(1)) = FIFO_TX1_WORDS << 16 | (FIFO_RX_WORDS + FIFO_TX0_WORDS);
}

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;
}

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)      /* у OTG адрес пишут сразу, до стадии состояния (RM0090) */
{
    REG(R_DCFG) = (REG(R_DCFG) & ~DCFG_DAD_Msk) | (uint32_t)addr << DCFG_DAD_Pos;
}

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: такт AHB2 и выводы PA11/PA12 к этому моменту включены (main_f407.c) */
void otg_f407_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 | GCCFG_NOVBUSSENS;       /* приёмопередатчик включён, VBUS не следим */
    REG(R_GUSBCFG) = (REG(R_GUSBCFG) & ~GUSBCFG_TRDT_Msk) | GUSBCFG_FDMOD | 6u << GUSBCFG_TRDT_Pos;
    REG(R_DCTL) |= DCTL_SDIS;                             /* пока не готовы — подтяжка D+ снята */
    REG(R_PCGCCTL) = 0;
    REG(R_DCFG) |= DCFG_DSPD_FS;
    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_f407_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);
        }
    }
}

vend_pc.py — программа на ПК в духе pyusb.

# -*- coding: utf-8 -*-
"""Программа на ПК для устройства раздела 20д. Обращается к устройству тремя
вызовами в духе pyusb: ctrl_transfer, write, read. На настоящем устройстве dev даёт
open_device() (нужны pyusb и libusb); в проверке — модель хоста (host_model.py)."""
VID, PID = 0x1209, 0x0002
EP_OUT, EP_IN = 0x01, 0x81
VREQ_GET_INFO, VREQ_SET_LED, VREQ_SET_XOR = 0x01, 0x02, 0x03
REQ_VENDOR_IN, REQ_VENDOR_OUT = 0xC0, 0x40      # производитель, устройству


def open_device():
    import usb.core                             # pyusb
    dev = usb.core.find(idVendor=VID, idProduct=PID)
    if dev is None:
        raise SystemExit("устройство %04X:%04X не найдено" % (VID, PID))
    dev.set_configuration()
    return dev


def get_info(dev):
    b = bytes(dev.ctrl_transfer(REQ_VENDOR_IN, VREQ_GET_INFO, 0, 0, 12))
    return {"version": "%d.%02d" % (b[1], b[0]), "led": b[2], "xor": b[3],
            "rx": int.from_bytes(b[4:8], "little"), "tx": int.from_bytes(b[8:12], "little")}


def set_led(dev, on):
    dev.ctrl_transfer(REQ_VENDOR_OUT, VREQ_SET_LED, 1 if on else 0, 0)


def set_xor(dev, key):
    dev.ctrl_transfer(REQ_VENDOR_OUT, VREQ_SET_XOR, 0, 0, bytes([key]))


def echo(dev, data):
    """Отправить данные пакетами по 64 байта; на каждый пакет устройство отвечает своим."""
    back = b""
    for i in range(0, len(data), 64):
        chunk = data[i:i + 64]
        dev.write(EP_OUT, chunk)
        back += bytes(dev.read(EP_IN, 64))
    return back


def main(dev):
    print("состояние:", get_info(dev))
    set_led(dev, True)
    set_xor(dev, 0x5A)
    data = bytes(range(200))
    back = echo(dev, data)
    ok = back == bytes(b ^ 0x5A for b in data)
    print("эхо: отправлено %d, принято %d, совпало с ожидаемым: %s" % (len(data), len(back), "да" if ok else "нет"))
    print("состояние:", get_info(dev))
    return ok


if __name__ == "__main__":
    main(open_device())

Дальше: раздел 21 «SWO и ITM» — отладочный вывод, не занимающий ни UART, ни единого вывода сверх тех, что уже заняты отладчиком.

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