198ш. Mini-project: a USB COM port and a keyboard in one device on the B-U585I-IOT02A — a composite CDC + HID device
Section 20 called a device with several classes at once composite, but showed only a virtual COM port. Section 20в built a keyboard and a mouse on one HID interface. In 198ф the B-U585I-IOT02A board was a flash drive, in 198ч a host. Here the same board, over one USB-C cable, shows the computer two devices at once: a COM port with a command console (status, sensor log) and a keyboard whose only key is the USER button. Press it, and the board "types" a prepared string into whatever window has the cursor.
The board pack B-U585I-IOT02A_BSP 1.1.0 (Keil) has two separate device
examples on the MDK-Middleware stack — VirtualCOM (a USB-to-USART1 bridge) and
HID (a button and LEDs driven by its own PC program). There is no composite
example. We take from the examples what belongs to the board — pins, clocks,
OTG_FS setup — and write the rest ourselves: descriptors, endpoint 0, both classes
and a register-level OTG_FS core driver. That is 973 lines in seven files; they
will not replace a library, but every byte is visible in them.
Tested without a board. The descriptors, endpoint 0, CDC, HID and the console were built with gcc on a PC and run against a host model: it enumerates the device request by request, parses the configuration descriptor with its own parser, opens the port, sends commands and reads the keyboard reports. The OTG_FS driver was only partly tested on the PC — a register model shows what it writes into the FIFO and endpoint registers. The whole firmware was built with armclang for Cortex-M33; it has not been run on the board.
References: RM0456 Rev 4 (STM32U5, chapter 72 "OTG_FS": 72.2.3, table 740,
72.6.2, 72.11.3, registers 72.15); datasheet DS13086 (STM32U585xx, table
84 — HSI48); the header stm32u585xx.h and HAL (stm32u5xx_ll_usb.c,
stm32u5xx_hal_pcd_ex.c) from the STM32U5xx_DFP 2.2.1 pack; the board pack
B-U585I-IOT02A_BSP 1.1.0: the examples USB/Device/VirtualCOM and HID (license
Apache-2.0), the driver 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 license — paraphrase and short quotes only); the driver
USBD_STM32.c from the CMSIS-Driver_STM32 1.3.0 pack; the composite
CDC + HID device from github.com/betaflight/betaflight, commit 744f95f (file
usbd_cdc_hid.c, GPL-3.0 license, cross-check only); Zephyr 4.4.2
(include/zephyr/usb/class/hid.h, the sample samples/subsys/usb/cdc_acm).
The texts of the USB 2.0, CDC and HID specifications were not used: codes and formats are taken
from these sources. License: the section's own code is free to use.
Terms used in the documentation
Term Meaning Interface Association Descriptor (IAD) a descriptor that groups interfaces into one function function (in a composite device) one or more interfaces served by one driver Communications Class interface, Data Class interface the CDC control interface and the CDC data interface Abstract Control Model (ACM) the CDC subclass used for a COM port functional descriptor: Header, Call Management, Union CDC functional descriptors: header, call management, union line coding (dwDTERate, bCharFormat, bParityType, bDataBits) line format: baud rate, stop bits, parity, data bits SERIAL_STATE notification a line-state notification sent over the interrupt endpoint zero-length packet (ZLP) an empty packet that ends data whose size is a multiple of the packet size wTotalLength the full length of the configuration descriptor with everything nested in it soft disconnect (SDIS) software disconnect: the D+ pull-up is removed B-session valid override (BVALOEN, BVALOVAL) software override of the "VBUS present" signal USB turnaround time (TRDT) bus turnaround time: the margin for the device to respond
198ш.1 What the device is and what the host sees
To the host the device is three interfaces and two functions:
- the CDC function — interfaces 0 and 1. On the PC this is a COM port. It carries
the console:
status,log,macro <text>,type <text>,help; - the HID function — interface 2, a boot keyboard (20в.4). The
USER button on PC13 types a macro string; the
typecommand types what was sent to the COM port.
What the pack examples have. Both start the stack with two calls,
USBD_Initialize(0U) and USBD_Connect(0U); the stack builds the descriptors itself from
configuration files. In VirtualCOM: VID 0xC251, PID 0x5905, interrupt endpoint
1 of 16 bytes with a 2 ms polling interval, bulk endpoints 2 of 64 bytes. In HID: endpoint 1,
a 4-byte packet, 16 ms polling. The description of both: a 160 MHz processor, USB at 48 MHz,
OTG_FS on connector CN1 (USB Type-C).
The MDK stack can build a composite device: the macro USBD0_USE_IAD
is enabled when there is another class next to CDC, and then the device descriptor
gets the class USB_DEVICE_CLASS_MISCELLANEOUS (0xEF) with
subclass 2 and protocol 1 — "look at the association descriptors". A ready-made
CDC + HID device exists in betaflight (usbd_cdc_hid.c, the ST library):
the flight controller shows up as a COM port and an HID device. We will check the
descriptor's length and contents against it.
Project files:
| File | Contents |
|---|---|
usbdesc.c |
descriptors: device, configuration, HID report, strings |
usbcore.c |
endpoint 0: standard requests, dispatching requests to the classes |
cdc.c |
CDC requests, notification, receive and transmit rings |
hidkbd.c |
HID requests, typing text with reports, the button |
console.c |
commands and the log |
otg_u585.c, otg_regs.h |
register-level OTG_FS driver |
main_u585.c |
48 MHz, CRS, VDDUSB, pins, main loop |
The core does not know the registers: it sends and receives packets through eight
drv_* functions. On the board they come from otg_u585.c, in the model from host.c.
198ш.2 Descriptors: device, IAD and configuration byte by byte
Device descriptor. The difference from a single CDC is three class bytes:
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 and PID here are placeholders for a prototype (20.6): a product has its own.
Why IAD. The CDC function takes two interfaces — control and data. The host needs to know that interfaces 0 and 1 are one device under one driver, and interface 2 is another. That is what the IAD says: 8 bytes before the first interface of the function — the first interface number, the number of interfaces and the function class. A single HID interface needs no association. The device class EF/02/01 warns the host that the configuration contains an IAD.
Configuration descriptor. All the nested descriptors go in one
array, and the host reads it whole, by 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
};
The same array through the host's eyes — the parse from the model: the offset on the left, then the bytes and their meaning.
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 — сходится
What matters here:
wTotalLength= 100 = 9 + 8 + (9 + 5 + 5 + 4 + 5 + 7) + (9 + 7 + 7) + (9 + 9 + 7). A_Static_assertinusbdesc.cchecks this sum. betaflight has the same length: 34 − 9 + 67 + 8 = 100, only there HID comes first, and CDC takes interfaces 1 and 2;- the CDC functional descriptors (type 0x24) sit between interface 0 and its endpoint. Union names master interface 0 and subordinate 1 — the same as the IAD, but in CDC terms. ACM with capabilities 0x02 promises the line-coding requests and the SERIAL_STATE notification; the MDK example sets 0x06 — also Send_Break, which we do not have;
- the notification endpoint 0x81 — 16 bytes: the SERIAL_STATE notification takes
10. In the ST library this endpoint is 8 bytes (
CDC_CMD_PACKET_SIZE); - HID: subclass 1 and protocol 1 — a boot keyboard, an 8-byte report without a report ID. The HID descriptor (type 0x21) at offset 84 gives the length of the report descriptor — 63 bytes.
The keyboard report descriptor is in the language of 20в.2, so no breakdown:
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 bytes, 32 items, an 8-byte input, a 1-byte output (the LEDs). Zephyr's
HID_KEYBOARD_REPORT_DESC() macro has the same length, number of items and reports;
the differences are the order of Report Size and Report Count and the padding flag (Zephyr has
0x03, we have 0x01).
198ш.3 Endpoint 0: enumeration, strings and the serial number
A control transfer is an 8-byte SETUP, then data in one direction in packets of up to 64 bytes, then an empty packet in the opposite direction (the "status"). The IN data stage is handled by three functions:
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); }
}
Request parsing. The core handles standard requests; class requests
go to cdc.c or hidkbd.c by the interface number in 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 opens four endpoints and arms reception:
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 data (line coding, LEDs) is accumulated until wLength
bytes have arrived, and only then goes to the class:
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();
}
Enumeration in the model. The host does what operating systems do: the device descriptor, the address, the configuration first for 9 bytes, then in full, the strings:
сброс шины: конфигурация 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
What to look at:
- the 100-byte configuration goes in two packets: 64 + 36. A short packet ends the data, so a request for 255 bytes gives the same two packets;
- a request for exactly 64 bytes is one full packet and nothing more: the host got everything it asked for;
- DEVICE_QUALIFIER is refused: this descriptor describes operation at another speed, and we have only one — full speed;
- the address is written to the register right at SETUP, before the status stage. That is what RM0456 (72.6.2) prescribes: once the SET_ADDRESS request is decoded, the program writes the address into the DAD field of the OTG_DCFG register;
- a class request before SET_CONFIGURATION is refused: there are no interfaces yet.
Strings. A string descriptor is a 2-byte header and the text in UTF-16LE.
String 0 is the list of languages; we have one: 0x0409. The serial number is the 96-bit
chip ID (on the U585 it is at address 0x0BFA0700, UID_BASE) as
24 hexadecimal digits: 2 + 24 × 2 = 50 bytes. In the model the number is made up.
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: line coding, DTR and the console
ACM has three class requests and one notification. The codes come from MDK's usb_cdc.h:
SET_LINE_CODING 0x20, GET_LINE_CODING 0x21, SET_CONTROL_LINE_STATE 0x22,
SEND_BREAK 0x23; the SERIAL_STATE notification is also 0x20, but it goes from the
device over the interrupt endpoint.
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;
}
Line coding is 7 bytes: the baud rate (4 bytes, least significant first), stop bits,
parity, data bits. 00 C2 01 00 is 0x0001C200 = 115200. There is no UART behind our
port, so we only remember the baud rate and return it; it has no effect on
the exchange. We still accept the request and return the same value in
GET_LINE_CODING.
DTR means "port open". SET_CONTROL_LINE_STATE carries two bits in wValue:
bit 0 is DTR, bit 1 is RTS (about them from the PC program's side — 115б).
Terminals usually set DTR when they open the port. For the device it is
the only sign that someone is reading its text:
- DTR = 0 — console output is counted and discarded, no queue builds up;
- DTR became 1 — a notification and a greeting are sent;
- DTR dropped — whatever was not sent is discarded.
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
The notification A1 20 00 00 00 00 02 00 03 00 is a header like a SETUP one
(type 0xA1, code 0x20, interface 0, length 2) and two state bytes: 0x03 —
the "receive carrier" and "transmit carrier" bits (CDC_SERIAL_STATE_RX_CARRIER,
CDC_SERIAL_STATE_TX_CARRIER).
Receive and transmit. Reception is as in 20.7: the interrupt only puts data into the ring. If the ring has no room left for a whole packet, the endpoint is not armed — the host gets NAK and waits:
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; /* полный пакет был последним: нужен пустой */
}
Commands arrive in arbitrary pieces — the model cuts status and log into three packets,
and the 136-byte reply goes out in three:
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
Three things from this run:
- the empty packet. A reply of exactly 64 bytes is the packets 64 and 0. Without the empty packet the host does not know that the data has ended. 130 bytes is 64, 64 and 2: a short packet ends the transfer by itself;
- NAK instead of losses. The main loop has "hung", the host sends three packets. Two went into the 128-byte ring, the third got NAK and went through after the loop drained the ring;
- the closed port. 100 bytes in the queue are discarded when DTR drops, and a new DTR starts with the greeting.
The console assembles lines on \r or \n and executes them:
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: the button types a string
A boot keyboard report is 8 bytes: modifiers, a reserved byte, six key codes (20в.4). To "type" a character, you send a report with its code, then a report without it. A US layout table converts a character to a code — the same one as in 198ч, only in the opposite direction:
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;
}
When a release is needed. The host counts as a keystroke a code that was not in the previous report (198ч.3). Two identical codes in a row with no release between them are one long key press to the host. An identical code is not only "ll", but also "5" and "%": that is one key, the only difference is 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;
}
A report goes out when the endpoint is free; the host takes one report per
poll, once every bInterval = 10 ms:
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);
}
}
The button. USER on PC13 gives a one when pressed (that is how the board pack's
I/O driver reads it: BSP_PB_GetState(BUTTON_USER) == 1U).
Debouncing is 20 ms of the same level in a row; typing starts on the press edge:
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);
}
The run: 6 ms of bounce, the button is held for 200 ms. The default macro is "Hello, U585!" and Enter, 13 characters:
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
...and so on, 26 reports. The first is 02 00 0B: left Shift and code 0x0B,
"H".
кнопка, отпускание после каждого символа: отчётов 26, хост набрал «Hello, U585!<Enter>»
кнопка: дребезг 6 мс, удержание 200 мс — макрос набран 1 раз, отчётов отправлено 26
без отпусканий (13 отчётов): хост набрал «Helo, U585!<Enter>»
The last line shows what would happen if the firmware sent only presses: "ll"
merged into one "l". Another way is the kbd_set_roll(1) mode: release
only where it cannot be avoided:
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
— this is how "ll" looks in both modes, and in total there are 15 reports instead of 26: 13 presses, one release between "l" and "l" and one at the end.
тот же текст, отпускание только где нужно: отчётов 15, хост набрал «Hello, U585!<Enter>»
With polling every 10 ms that is 150 ms versus 260 ms per string. By default the mode is off: in it Shift changes in the same report as the key, and how each operating system will take that cannot be checked on the model.
A macro that needs every release is "55%": three characters on one key. It was set from the COM port and typed with the button; the mode is the same, the economical one:
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%»
The two functions work together: the type command arrived over the COM port, and the text
went out through the keyboard.
OUT 11 байт "type Ab1!\r\n": ACK
ответ: пакеты IN 9, всего 9 байт
> OK type
команда type из COM-порта: отчётов 8, хост набрал «Ab1!»
HID class requests. A keyboard host sends SET_IDLE with zero ("report only on change"), may ask for the current report and light the LEDs:
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 is the Caps Lock bit. On a keyboard without an OUT endpoint the LEDs arrive
as a SET_REPORT request over endpoint 0 (20в.1). reports=55 is all the reports of the run:
26 + 15 + 6 + 8.
Invalid requests are refused, and the host sets and clears an endpoint halt with standard requests:
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 on the U585: FIFO memory, endpoints, 48 MHz and the board
How many endpoints. The OTG_FS core of the STM32U575/585 has endpoint 0 and five IN and five OUT endpoints (RM0456, 72.2.3; table 740 lists 6 bidirectional endpoints including endpoint zero). We need three numbers: 1 (notifications), 2 (data in both directions), 3 (HID).
FIFO memory. The core has its own packet memory — 1.25 KB (72.11; in table 740 it is rounded to 1.2 KB), that is 320 words of 32 bits. The program divides it itself: one shared receive FIFO and one transmit FIFO for each IN endpoint. The reset values of the registers are no good for this: GRXFSIZ holds 0x200 — 512 words, more than the whole memory.
The receive FIFO by the formula from 72.11.3: (5 × number of control endpoints + 8) + (largest packet / 4 + 1) + 2 × number of OUT endpoints + 1. We have two OUT endpoints — endpoint zero and CDC data: 13 + 17 + 4 + 1 = 35 words. With a margin for a second packet, as RM0456 advises, it is 52. We take 64. A transmit FIFO is no smaller than the packet of its endpoint and no smaller than 16 words (the INEPTXFD field: "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];
}
}
The register model: otg_u585.c is built on the PC, the OTG block is ordinary
memory, the functions are the same.
после сброса: 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 words of 320 are used. The CDC data endpoint gets 64 words — four packets; the remaining 144 words can also go to it if more speed is needed.
Endpoints. To open an endpoint, write its type, packet size, FIFO number and the "active" bit into its register, and enable its interrupt:
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 reads as: packet 0x10, bit 15 — endpoint active,
type 3 (interrupt) in bits 19:18, FIFO number 1 in bits 25:22. The SNAK
and SD0PID bits (both write-only) read as zero in the real register; in the model this is memory, and they
are visible. Transmission is one packet:
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;
}
Until the host has taken the packet, the EPENA bit is set, and the next call returns −1 —
that is what tx_busy in cdc.c and in_busy in hidkbd.c rely on.
Starting the core. The order is the same as in HAL (USB_CoreInit,
USB_SetCurrentMode, USB_DevInit): the embedded transceiver, core
reset, device mode, FIFOs, interrupt masks, and only at the very end — clear
SDIS, that is, turn on the D+ pull-up:
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 is the HAL value for an AHB bus clock from 32 MHz. VBUS sensing
is off, as in the pack example (vbus_sensing_enable = DISABLE): in
that case HAL sets the same BVALOEN and BVALOVAL. The register offsets and bits of
otg_regs.h are checked against stm32u585xx.h at build time by the file otg_selfcheck.c —
fourteen _Static_asserts.
Reception: the core puts a status word and the data into the shared FIFO; the PKTSTS field tells SETUP from OUT data:
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 MHz without a crystal. Both device examples in the pack take 48 MHz from
HSI48 (RCC_CLK48CLKSOURCE_HSI48) and do not enable the CRS block. According to
table 84 of DS13086, HSI48 after factory calibration is 47.5...48.5 MHz at 30 °C,
that is ±1 %, and over temperature from −3 to +2 %; USB requires ±0.25 %
(section 20). CRS helps the device: the host sends a SOF every 1 ms, and the block
trims HSI48 against them (20а.5). The "SOF from USB" sync source is the
default (RCC_CRS_SYNC_SOURCE_USB in HAL); what remains is to enable the counter and
automatic trimming:
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 — вход */
}
The USV bit in PWR_SVMCR tells the chip that the VDDUSB supply is present; HAL
does this with the function HAL_PWREx_EnableVddUSB. The 160 MHz system clock
comes from the example's SystemClock_Config; we do not touch it.
The USB-C connector. The connector's CC lines go to PA15 and PB15 (UCPD1); the example
leaves them analog and does not start UCPD. The port power on the board
is managed by the TCPP03 (198ч.2). The device examples do not call its driver —
the calls sit under #ifdef RTE_Drivers_USBH0. According to the driver's comment,
the TCPP03 state after power-up is Hibernate: "Dead Batteries Enabled",
the mode for a sink. So the Rd pull-downs, by which a USB-C host detects
a device and applies VBUS (20б.2), are provided by the TCPP03 itself. Whether the board is routed this way
the MB1551 schematic will tell; this section was not checked against it.
What the operating system needs. Nothing has to be installed. According to the description of the
VirtualCOM example, recent Windows versions create a "USB Serial Device (COMx)" port on their own;
in the setup file shipped with the example the driver is usbser.sys, and the functions
of the composite device are named by the number of their first interface: MI_00,
MI_02. For us MI_00 is the COM port, MI_02 is the keyboard. In Linux the port
is taken by the cdc_acm driver, which creates a ttyACM device (the line cdc_acm
9-1:1.0: ttyACM1: USB ACM device is from the description of the Zephyr sample).
Both systems take the keyboard with the standard HID driver. Our device has not been tested on
real operating systems.
Sizes — armclang 6.24, -Os, object files (without the startup code
and 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 байт
Of the 1879 bytes of RAM, 1260 are the CDC rings: 128 for receive, 1024 for transmit, and the packet buffers.
198ш.7 Pitfalls
IAD without class EF/02/01, or the class without an IAD. They go in pairs: both the MDK stack and betaflight set the device class EF/02/01 together with an IAD before the first interface of the CDC function. Class 0x02 is fit only for a single CDC.
wTotalLength does not add up. A descriptor was added and the length forgotten. The host
reads exactly wTotalLength bytes: it cuts off the excess and takes what is missing for
garbage. Compute the sum at compile time, like the _Static_assert in
usbdesc.c.
Union and IAD disagree. The interfaces were reordered (HID first, as in betaflight), the IAD was fixed, but Union and Call Management kept the old numbers. Interface numbers live in four places: the IAD, Union, Call Management and the interfaces themselves.
The last packet came out exactly 64 bytes. A transfer is ended by a short packet; after a full last packet an empty one is needed, otherwise the transfer is not finished for the host.
Text piles up and pours out when the port is opened. The device wrote into the queue without looking at DTR. Look at DTR and discard output while the port is closed; but then a program that opens the port and does not set DTR will see nothing — you need to be aware of this.
Double letters disappear. There is no release report between identical codes: "Hello" became "Helo". The same with "5" and "%" — the code is the same.
The FIFOs are not divided. The reset values are 512 words for each with 320 words of memory. Each IN endpoint gets its own FIFO of at least 16 words, all back to back, the sum no more than 320.
A FIFO smaller than 16 words. The USBD_STM32.c driver from CMSIS-Driver_STM32
1.3.0 sets the transmit FIFO to (ep_max_packet_size + 3U) / 4U words: for the
example's HID endpoint with a 4-byte packet that is 1 word. The comment in
HAL_PCDEx_SetTxFiFo itself says "TXn min size = 16 words", and so does RM0456.
We have not checked how the core behaves with such a FIFO; we keep our own sizes no
smaller than 16.
48 MHz from HSI48 without CRS. ±1 % at 30 °C and down to −3 % over temperature against the required ±0.25 %. The pack examples do not enable CRS.
The D+ pull-up too early. Until the firmware is ready to respond, the host
must not see the device. HAL does the same: USB_DevInit sets
SDIS, and USB_DevConnect clears it — as the last step of startup.
The serial number is the same on every device. The string is hard-coded in the firmware
(in the example, "0001A0000000"): two devices will have the same one. Take the number
from UID_BASE.
198ш.8 Summary
- A composite device: class EF/02/01, an IAD (8 bytes) before the CDC function, 3 interfaces, 4 endpoints, a 100-byte configuration descriptor — the same as the CDC + HID in betaflight.
- Endpoints: 0x81 — CDC notifications, 16 bytes; 0x02 and 0x82 — data, 64 bytes each; 0x83 — keyboard reports, 8 bytes, 10 ms polling.
- CDC: SET_LINE_CODING 0x20 (7 bytes), GET_LINE_CODING 0x21, SET_CONTROL_LINE_STATE 0x22; DTR means "port open"; a reply of exactly 64 bytes ends with an empty packet.
- Keyboard: an 8-byte report, a 63-byte report descriptor. "Hello, U585!" and Enter take 26 reports with a release after every character, 15 with a release only between identical codes; without releases "ll" is lost.
- OTG_FS on the U585: 6 endpoints including endpoint zero, 320 words of FIFO memory. In this section: receive 64 (at least 35 by the formula), transmit 16, 16, 64 and 16 — 176 used.
- 48 MHz comes from HSI48 trimmed by CRS against SOF; without CRS the oscillator does not stay within ±0.25 %.
- The serial number is 24 digits from the 96-bit chip ID, a 50-byte descriptor.
- The firmware is 6896 bytes of flash and 1879 bytes of RAM.
Full source code
usbdesc.c — descriptors of the composite CDC ACM + HID device.
/* 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 — endpoint 0: standard requests and dispatching requests to the classes.
/* 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 — the CDC ACM class: line requests, SERIAL_STATE, rings.
/* 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 — the command console and the log.
/* 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 — the HID class: a boot keyboard and the button.
/* 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 — a register-level OTG_FS driver for the 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 MHz from HSI48 with CRS, VDDUSB, pins and the main loop.
/* 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;
}
}
}
Next: section 198щ "Mini-project: your own bootloader for a production device on the STM32F405 — sector layout, staging slot, updates over UART-Bluetooth and protection against power loss" — the device is built, and now you need to be able to update it at the customer's end.