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 Что за прибор и что видит хост
Прибор для хоста — три интерфейса и две функции:
- функция CDC — интерфейсы 0 и 1. На компьютере это COM-порт. В нём
консоль:
status,log,macro <текст>,type <текст>,help; - функция HID — интерфейс 2, загрузочная клавиатура (20в.4). Кнопка
USER на PC13 набирает строку-макрос; команда
typeнабирает то, что прислали в COM-порт.
Что есть в примерах пакета. Оба запускают стек двумя вызовами,
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 — сходится
Что здесь важно:
wTotalLength= 100 = 9 + 8 + (9 + 5 + 5 + 4 + 5 + 7) + (9 + 7 + 7) + (9 + 9 + 7). Эту сумму проверяет_Static_assertвusbdesc.c. У betaflight длина та же: 34 − 9 + 67 + 8 = 100, только HID там стоит первым, а CDC занимает интерфейсы 1 и 2;- функциональные дескрипторы CDC (тип 0x24) лежат между интерфейсом 0 и его точкой. Union называет главный интерфейс 0 и подчинённый 1 — то же, что IAD, но на языке CDC. ACM с возможностями 0x02 обещает запросы формата линии и уведомление SERIAL_STATE; пример MDK ставит 0x06 — ещё и Send_Break, которого у нас нет;
- точка уведомлений 0x81 — 16 байт: уведомление SERIAL_STATE занимает
10. В библиотеке ST эта точка — 8 байт (
CDC_CMD_PACKET_SIZE); - HID: подкласс 1 и протокол 1 — загрузочная клавиатура, отчёт 8 байт без номера. Дескриптор HID (тип 0x21) по смещению 84 называет длину дескриптора отчёта — 63 байта.
Дескриптор отчёта клавиатуры — язык из 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
На что смотреть:
- конфигурация в 100 байт идёт двумя пакетами: 64 + 36. Короткий пакет кончает данные, поэтому запрос на 255 байт даёт те же два пакета;
- запрос ровно на 64 байта — один полный пакет и ничего больше: хост получил всё, что просил;
- DEVICE_QUALIFIER — отказ: этот дескриптор описывает работу на другой скорости, а у нас она одна — полная;
- адрес записан в регистр сразу по SETUP, до стадии состояния. Так велит RM0456 (72.6.2): запрос SET_ADDRESS разобран — программа пишет адрес в поле DAD регистра OTG_DCFG;
- запрос класса до SET_CONFIGURATION — отказ: интерфейсов ещё нет.
Строки. Дескриптор строки — 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. Для прибора это
единственный признак, что его текст кто-то читает:
- DTR = 0 — вывод консоли считается и выбрасывается, очередь не копится;
- DTR стал 1 — уходит уведомление и приветствие;
- 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
Три вещи из этого прогона:
- пустой пакет. Ответ ровно в 64 байта — это пакеты 64 и 0. Без пустого пакета хост не знает, что данные кончились. 130 байт — 64, 64 и 2: короткий пакет сам кончает передачу;
- NAK вместо потерь. Главный цикл «завис», хост шлёт три пакета. Два легли в кольцо на 128 байт, третий получил NAK и прошёл после того, как цикл разобрал кольцо;
- закрытый порт. 100 байт в очереди при падении DTR выброшены, новый DTR начинает с приветствия.
Консоль собирает строки по \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 Итоги
- Составное устройство: класс EF/02/01, IAD (8 байт) перед функцией CDC, 3 интерфейса, 4 точки, дескриптор конфигурации 100 байт — столько же, сколько у CDC + HID в betaflight.
- Точки: 0x81 — уведомления CDC, 16 байт; 0x02 и 0x82 — данные, по 64 байта; 0x83 — отчёты клавиатуры, 8 байт, опрос 10 мс.
- CDC: SET_LINE_CODING 0x20 (7 байт), GET_LINE_CODING 0x21, SET_CONTROL_LINE_STATE 0x22; DTR — «порт открыт»; ответ ровно в 64 байта кончается пустым пакетом.
- Клавиатура: отчёт 8 байт, дескриптор отчёта 63 байта. «Hello, U585!» и Enter — 26 отчётов с отпусканием после каждого символа, 15 — с отпусканием только между одинаковыми кодами; без отпусканий «ll» теряется.
- OTG_FS у U585: 6 точек с нулевой, память FIFO 320 слов. Наш раздел: приёмный 64 (по формуле не меньше 35), передающие 16, 16, 64 и 16 — занято 176.
- 48 МГц — HSI48 с подстройкой CRS по SOF; без CRS генератор не укладывается в ±0,25 %.
- Серийный номер — 24 знака из 96 бит номера кристалла, дескриптор 50 байт.
- Прошивка — 6896 байт флеш-памяти и 1879 байт ОЗУ.
Полные исходники
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 и защита от обрыва питания» — прибор собран, теперь его надо уметь обновлять у покупателя.