20д. Your own USB device without a driver: descriptors byte by byte, MS OS 2.0 and WinUSB
In section 20 the device became a COM port, in 20в a keyboard, in 198ш both at once. These are ready-made classes: the system takes its own driver for them. But sometimes neither a COM port nor HID fits. You need your own commands, your own speed, bulk endpoints without wrappers. Then the device has a class of its own — 0xFF, "vendor-specific". Windows has no driver for it. You either write and sign one, or the device itself tells the system: "take the standard WinUSB". This is what the Microsoft OS 2.0 descriptors are for.
Here such a device is built: an interface of class 0xFF, two bulk endpoints, a BOS with the MS OS 2.0 platform capability and a descriptor set with the compatible ID "WINUSB" and an interface GUID. The device works as an echo: whatever came into the OUT endpoint goes back, "salted" with a key, through the IN endpoint. Everything is described byte by byte, and every byte is checked. The host on the computer enumerates the device through a model of the STM32F103 USB FS block and parses every descriptor with its own parser. The MS OS 2.0 set and the BOS are compared byte by byte with the Zephyr structures. The PC program was run on top of the same model. The firmware for F103 and F407 is built with arm-none-eabi-gcc. The device was not run on a board or on Windows.
Sources: RM0008 Rev 17 (STM32F10x, chapter 23 "USB full-speed device
interface": 23.4.2, 23.5.2, 23.5.3, table 177; 30.2 — the die ID);
DocID13587 Rev 17 (STM32F103x8/B, table 43 — transceiver start-up
time); RM0090 Rev 19 (STM32F4, chapter 34 "OTG_FS": 34.2, 34.13.1);
the headers stm32f10x.h (STM32F1xx_DFP 2.4.1) and stm32f407xx.h
(STM32F4xx_DFP 2.17.1); Zephyr 4.4.2 (github.com/zephyrproject-rtos/zephyr,
commit dccb095: include/zephyr/usb/msos_desc.h, include/zephyr/usb/bos.h,
the example samples/subsys/usb/webusb, subsys/usb/device_next/usbd_ch9.c;
Apache-2.0 license); candleLight (github.com/candle-usb/candleLight_fw,
commit 85e5b33, src/usbd_gs_can.c; MIT license); MDK-Middleware 7.17.0 and
8.1.0 (usb_ms.h, USBD_Config_CustomClass.h, the description of the
WinUSB_Echo example; Arm license — retold only). The device, endpoint 0 and
the F407 driver continue the code of section 198ш. The texts of the USB and
Microsoft OS 2.0 specifications were not used: the formats are taken from these sources.
License: the section's own code is free to use.
Terms used in the documentation
Term Meaning vendor-specific class (0xFF) a class whose meaning is defined by the device itself vendor request a request with bmRequestType 0x40 or 0xC0 Binary device Object Store (BOS) the store of device capabilities, descriptor type 0x0F Device Capability, Platform Capability a device capability; a platform one carries a platform UUID Microsoft OS 2.0 descriptor set the set of Microsoft OS 2.0 descriptors bMS_VendorCode the vendor request code the host uses to ask for the set Compatible ID "WINUSB" which standard driver fits the device DeviceInterfaceGUIDs, REG_MULTI_SZ the device interface GUID; the "list of strings" registry type Microsoft OS String Descriptor (0xEE) string 0xEE — the entry to the old MS OS 1.0 descriptors packet memory area (PMA), buffer descriptor table the F103 packet memory and the buffer table in it toggle bit, rc_w0 a bit that flips when 1 is written; a flag cleared by writing 0
20д.1 Why a device of your own and what the host will ask
Three ways to reach a device from a computer:
| Class | Driver on the PC | What the program gets | When to choose it |
|---|---|---|---|
| CDC ACM (section 20, 198ш) | the system's own: in Windows usbser.sys (198ш.6) |
a COM port: a byte stream | a console, a UART replacement |
| HID (section 20в) | the system's own, in all systems | fixed-length reports over an interrupt endpoint | a keyboard, buttons, small data |
| your own, 0xFF + WinUSB | winusb.sys, if the device returned MS OS descriptors |
the endpoints as they are: control requests, 64-byte bulk packets | your own protocol, a data stream, no COM port wrappers |
A COM port has no commands of its own other than the baud rate and the lines: a command has to be
hidden in the stream. HID has fixed-length reports and an interrupt endpoint:
no more than one packet per polling interval. A device of your own gives everything
at once: control requests for commands and bulk endpoints for data.
The price is a driver on the PC. The MS OS descriptors remove exactly that: the description of
the WinUSB_Echo example from MDK-Middleware says that on Windows 8 and later
the driver installs itself, while Windows 7 needs a setup file.
What the host asks such a device is shown in the figure. What is new compared with 198ш is steps 2 and 3: the BOS descriptor and the vendor request.
The host model repeats this order. First it looks at bcdUSB.
The Zephyr stack on the device side refuses the BOS if bcdUSB is below
0x0201 (sreq_get_desc_bos in usbd_ch9.c). That is why the model asks for the BOS
only from a device with version 2.01 and above. It recognises the platform capability
by its UUID and takes the request code and the set length from it. It asks for the set
with a vendor request to the device with wIndex = 7: this is
MS_OS_20_DESCRIPTOR_INDEX in Zephyr's msos_desc.h.
20д.2 The device and configuration descriptors byte by byte
The common device header (vend.h) holds the endpoint numbers, request codes and
lengths of all descriptors:
#ifndef USB_BCD_USB
#define USB_BCD_USB 0x0201u /* 2.01: только у такого устройства хост спрашивает BOS */
#endif
#define USB_VID 0x1209u /* заглушки для макета: в изделии — свои номера */
#define USB_PID 0x0002u
#define EP0_SIZE 64u
#define EP_OUT 0x01u /* массовая OUT, 64 байта */
#define EP_IN 0x81u /* массовая IN, 64 байта */
#define BULK_SIZE 64u
#define EP_TYPE_BULK 2u
#define MS_VENDOR_CODE 0x20u /* bRequest, которым хост просит набор MS OS 2.0 */
#define MS_OS_20_INDEX 7u /* wIndex того же запроса */
#define VREQ_GET_INFO 0x01u /* свои запросы: IN, 12 байт состояния */
#define VREQ_SET_LED 0x02u /* без данных: wValue = 0 или 1 */
#define VREQ_SET_XOR 0x03u /* OUT, 1 байт: им «солится» эхо */
#define INFO_SIZE 12u
#define DEVICE_DESC_SIZE 18u
#define CONFIG_DESC_SIZE 32u
#define BOS_DESC_SIZE 33u
#define MSOS20_SET_SIZE 162u
#define LE16(x) ((x) & 0xFFu), (((x) >> 8) & 0xFFu)
Compare with 198ш.2: there the device class is EF/02/01 with an IAD, because there are two functions. Here there is one function, and the class sits in the interface. The main difference is the two version bytes: 2.01 instead of 2.00.
const uint8_t usb_device_desc[] = {
18, 0x01, /* bLength, DEVICE */
LE16(USB_BCD_USB), /* bcdUSB 2.01: у устройства есть BOS */
0x00, 0x00, 0x00, /* класс, подкласс, протокол — смотри интерфейс */
EP0_SIZE, /* bMaxPacketSize0 */
LE16(USB_VID),
LE16(USB_PID),
0x00, 0x01, /* bcdDevice 1.00 */
1, 2, 3, /* строки: изготовитель, изделие, серийный номер */
1 /* одна конфигурация */
};
The configuration is 32 bytes: the configuration itself, one interface and two endpoints.
const uint8_t usb_config_desc[] = {
/* конфигурация */
9, 0x02, LE16(CONFIG_DESC_SIZE), /* wTotalLength = 32 */
1, /* bNumInterfaces */
1, /* bConfigurationValue */
0, /* iConfiguration */
0x80, /* bmAttributes: питание от шины */
50, /* bMaxPower: 50 x 2 мА = 100 мА */
/* интерфейс 0: свой класс */
9, 0x04, 0, 0, 2, /* номер 0, настройка 0, две точки */
0xFF, 0x00, 0x00, /* класс 0xFF «у производителя», подкласс и протокол — свои */
0, /* iInterface */
/* точки */
7, 0x05, EP_OUT, EP_TYPE_BULK, LE16(BULK_SIZE), 0,
7, 0x05, EP_IN, EP_TYPE_BULK, LE16(BULK_SIZE), 0
};
The model host read both descriptors and parsed them with its own parser:
SETUP 80 06 00 01 00 00 12 00 GET_DESCRIPTOR(DEVICE 0, 18): IN 18 = 18 байт
DEVICE: bcdUSB 2.01, класс 00/00/00, пакет точки 0 — 64, VID 1209, PID 0002, строки 1 2 3, конфигураций 1 — сходится
SETUP 80 06 00 02 00 00 09 00 GET_DESCRIPTOR(CONFIGURATION 0, 9): IN 9 = 9 байт
wTotalLength = 32
SETUP 80 06 00 02 00 00 FF 00 GET_DESCRIPTOR(CONFIGURATION 0, 255): IN 32 = 32 байт
0: 09 02 20 00 01 01 00 80 32
CONFIGURATION: wTotalLength 32, интерфейсов 1, номер 1, bmAttributes 0x80, 100 мА
9: 09 04 00 00 02 FF 00 00 00
INTERFACE 0: настройка 0, точек 2, класс FF/00/00
18: 07 05 01 02 40 00 00
ENDPOINT 0x01: массовая OUT, пакет 64, bInterval 0
25: 07 05 81 02 40 00 00
ENDPOINT 0x81: массовая IN, пакет 64, bInterval 0
итог разбора конфигурации: пройдено 32 байт из 32, интерфейсов 1 (заявлено 1), точек 2 — сходится
What matters here:
bcdUSB= 0x0201 is stored least significant byte first:01 02. A host that sees 2.00 does not ask for the BOS — the same device with 2.00 is left without WinUSB (20д.3);- interface class 0xFF tells the host one thing: "there is no ready-made driver". With 0xFF the subclass and protocol are up to the vendor, ours are zeros;
- bulk endpoints do not need
bInterval, so it is 0; the packet is 64 bytes, full speed gives no more for bulk endpoints; - strings 1–3 are the same as in 198ш.3: the manufacturer (30 bytes),
the product "WinUSB echo 20d" (32 bytes) and a serial number from the 96 bits of the die
ID (50 bytes). The ID is at address 0x1FFFF7E8 on the F103 (RM0008,
30.2), at 0x1FFF7A10 on the F407 (
UID_BASE). In the model it is zero.
The lengths written inside the descriptors come from macros, and the array sizes are checked by the compiler:
_Static_assert(sizeof usb_device_desc == DEVICE_DESC_SIZE, "дескриптор устройства: не 18 байт");
_Static_assert(sizeof usb_config_desc == CONFIG_DESC_SIZE, "конфигурация: wTotalLength не сходится");
_Static_assert(sizeof usb_bos_desc == BOS_DESC_SIZE, "BOS: wTotalLength не сходится");
_Static_assert(sizeof usb_msos20_set == MSOS20_SET_SIZE, "набор MS OS 2.0: wTotalLength не сходится");
Without these lines an array can be declared in a header with a length: extern const
uint8_t usb_msos20_set[162]. Then the compiler will silently fill a missing byte
with zero, and an extra one will only give a warning. With the checks both
cases are a build error (error: static assertion failed, section 9
of the run).
20д.3 The BOS and the MS OS 2.0 set
The BOS is a 5-byte root followed by capabilities. We have one, the platform capability: the platform UUID and four Microsoft fields.
const uint8_t usb_bos_desc[] = {
5, 0x0F, /* bLength, BOS */
LE16(BOS_DESC_SIZE), /* wTotalLength = 33: корень и все возможности */
1, /* bNumDeviceCaps */
/* платформенная возможность MS OS 2.0 */
28, 0x10, 0x05, /* bLength, DEVICE_CAPABILITY, PLATFORM */
0x00, /* bReserved */
0xDF, 0x60, 0xDD, 0xD8, 0x89, 0x45, 0xC7, 0x4C, /* UUID D8DD60DF-4589-4CC7- */
0x9C, 0xD2, 0x65, 0x9D, 0x9E, 0x64, 0x8A, 0x9F, /* 9CD2-659D9E648A9F */
0x00, 0x00, 0x00, 0x0A, /* dwWindowsVersion 0x0A000000: Windows 10 */
LE16(MSOS20_SET_SIZE), /* wMSOSDescriptorSetTotalLength = 162 */
MS_VENDOR_CODE, /* bMS_VendorCode: этим bRequest набор и попросят */
0x00 /* bAltEnumCode: второго варианта нет */
};
The UUID in the descriptor is written as a Windows GUID: the first three fields are least significant
byte first. That is why D8DD60DF starts with the byte DF. The vendor
code 0x20 is chosen freely: candleLight uses the same code, the
Zephyr examples use 0x02, MDK uses 0x01 by default.
The MS OS 2.0 set is a header and records. The first 4 bytes of each record are the length and the type, 2 bytes each.
const uint8_t usb_msos20_set[] = {
/* заголовок набора */
0x0A, 0x00, /* wLength = 10 */
0x00, 0x00, /* MS_OS_20_SET_HEADER_DESCRIPTOR */
0x00, 0x00, 0x00, 0x0A, /* dwWindowsVersion 0x0A000000 */
LE16(MSOS20_SET_SIZE), /* wTotalLength = 162: весь набор */
/* совместимый ID: какой драйвер ставить */
0x14, 0x00, /* wLength = 20 */
0x03, 0x00, /* MS_OS_20_FEATURE_COMPATIBLE_ID */
'W', 'I', 'N', 'U', 'S', 'B', 0x00, 0x00, /* CompatibleID */
0, 0, 0, 0, 0, 0, 0, 0, /* SubCompatibleID: нет */
/* свойство реестра: GUID, по которому программа найдёт устройство */
0x84, 0x00, /* wLength = 132 */
0x04, 0x00, /* MS_OS_20_FEATURE_REG_PROPERTY */
0x07, 0x00, /* wPropertyDataType: REG_MULTI_SZ, список строк */
0x2A, 0x00, /* wPropertyNameLength = 42 */
U16('D'), U16('e'), U16('v'), U16('i'), U16('c'), U16('e'),
U16('I'), U16('n'), U16('t'), U16('e'), U16('r'), U16('f'), U16('a'), U16('c'), U16('e'),
U16('G'), U16('U'), U16('I'), U16('D'), U16('s'), 0x00, 0x00,
0x50, 0x00, /* wPropertyDataLength = 80 */
U16('{'), U16('9'), U16('D'), U16('E'), U16('E'), U16('3'), U16('3'), U16('1'), U16('4'),
U16('-'), U16('0'), U16('6'), U16('2'), U16('6'),
U16('-'), U16('4'), U16('A'), U16('2'), U16('1'),
U16('-'), U16('B'), U16('C'), U16('E'), U16('4'),
U16('-'), U16('9'), U16('4'), U16('5'), U16('6'), U16('A'), U16('E'), U16('2'), U16('2'),
U16('7'), U16('C'), U16('2'), U16('8'), U16('}'),
0x00, 0x00, 0x00, 0x00 /* конец строки и конец списка */
};
The model host read the BOS, found MS OS 2.0 in it and asked for the set:
SETUP 80 06 00 0F 00 00 05 00 GET_DESCRIPTOR(BOS 0, 5): IN 5 = 5 байт
BOS: wTotalLength = 33
SETUP 80 06 00 0F 00 00 21 00 GET_DESCRIPTOR(BOS 0, 33): IN 33 = 33 байт
0: 05 0F 21 00 01
BOS: wTotalLength 33, возможностей 1
5: 1C 10 05 00 DF 60 DD D8 89 45 C7 4C 9C D2 65 9D
21: 9E 64 8A 9F 00 00 00 0A A2 00 20 00
DEVICE_CAPABILITY 28 байт: PLATFORM, UUID D8DD60DF-4589-4CC7-9CD2-659D9E648A9F — MS OS 2.0
dwWindowsVersion 0x0A000000, wMSOSDescriptorSetTotalLength 162, bMS_VendorCode 0x20, bAltEnumCode 0
итог разбора BOS: пройдено 33 байт из 33, возможностей 1 (заявлено 1) — сходится
SETUP C0 20 00 00 07 00 A2 00 производитель IN: bRequest 0x20, wValue 0, wIndex 7, wLength 162: IN 64+64+34 = 162 байт
0: 0A 00 00 00 00 00 00 0A A2 00
заголовок: wLength 10, тип 0, dwWindowsVersion 0x0A000000, wTotalLength 162
10: 14 00 03 00 57 49 4E 55 53 42 00 00 00 00 00 00
26: 00 00 00 00
COMPATIBLE_ID 20 байт: CompatibleID «WINUSB», SubCompatibleID «»
30: 84 00 04 00 07 00 2A 00 44 00 65 00 76 00 69 00
46: 63 00 65 00 49 00 6E 00 74 00 65 00 72 00 66 00
62: 61 00 63 00 65 00 47 00 55 00 49 00 44 00 73 00
78: 00 00 50 00 7B 00 39 00 44 00 45 00 45 00 33 00
94: 33 00 31 00 34 00 2D 00 30 00 36 00 32 00 36 00
110: 2D 00 34 00 41 00 32 00 31 00 2D 00 42 00 43 00
126: 45 00 34 00 2D 00 39 00 34 00 35 00 36 00 41 00
142: 45 00 32 00 32 00 37 00 43 00 32 00 38 00 7D 00
158: 00 00 00 00
REG_PROPERTY 132 байт: тип данных 7, имя 42 байт «DeviceInterfaceGUIDs», данные 80 байт
список строк: {9DEE3314-0626-4A21-BCE4-9456AE227C28}; оканчивается двумя нулями: да
итог разбора набора: пройдено 162 байт из 162 — сходится
What to look at:
- three lengths must agree.
wMSOSDescriptorSetTotalLengthin the BOS,wTotalLengthin the set header and the sum of the records: 10 + 20 + 132 = 162. The host asks for exactly as much as the BOS promised — 162 bytes, three packets 64 + 64 + 34; - the compatible ID is 8 ASCII bytes padded with zeros:
WINUSB\0\0. This is what "take WinUSB" means; - the property
DeviceInterfaceGUIDsof type 7 (REG_MULTI_SZ) is a list of UTF-16LE strings. The name is 20 characters plus a null, 42 bytes. The data is a GUID in curly braces (38 characters), the string null and the end-of-list null: 40 characters, 80 bytes. The PC program finds the device by this GUID. Each product has its own GUID; ours was created by a random GUID generator; - there are no subset records (type 1 — configuration, type 2 — function): the device has one function, and the set applies to the whole device. In a composite device the compatible ID goes into the function subset with the number of the first interface — this is how the Zephyr example with CDC ACM is done.
The same device built with bcdUSB 2.00 is enumerated by the host model without the BOS —
and without the set:
DEVICE: bcdUSB 2.00, класс 00/00/00, пакет точки 0 — 64, VID 1209, PID 0002, строки 1 2 3, конфигураций 1 — сходится
bcdUSB 2.00 < 2.01: BOS не запрашивается, MS OS 2.0 хост не увидит
SETUP 80 06 00 06 00 00 0A 00 GET_DESCRIPTOR(DEVICE_QUALIFIER 0, 10): STALL
SETUP 80 06 00 02 00 00 09 00 GET_DESCRIPTOR(CONFIGURATION 0, 9): IN 9 = 9 байт
wTotalLength = 32
SETUP 80 06 00 02 00 00 FF 00 GET_DESCRIPTOR(CONFIGURATION 0, 255): IN 32 = 32 байт
0: 09 02 20 00 01 01 00 80 32
CONFIGURATION: wTotalLength 32, интерфейсов 1, номер 1, bmAttributes 0x80, 100 мА
9: 09 04 00 00 02 FF 00 00 00
INTERFACE 0: настройка 0, точек 2, класс FF/00/00
18: 07 05 01 02 40 00 00
ENDPOINT 0x01: массовая OUT, пакет 64, bInterval 0
25: 07 05 81 02 40 00 00
ENDPOINT 0x81: массовая IN, пакет 64, bInterval 0
итог разбора конфигурации: пройдено 32 байт из 32, интерфейсов 1 (заявлено 1), точек 2 — сходится
SETUP 80 06 00 03 00 00 FF 00 GET_DESCRIPTOR(STRING 0, 255): IN 4 = 4 байт
длина 4: язык 0x0409
SETUP 80 06 01 03 09 04 FF 00 GET_DESCRIPTOR(STRING 1, 255): IN 30 = 30 байт
длина 30: «STM32 Handbook»
SETUP 80 06 02 03 09 04 FF 00 GET_DESCRIPTOR(STRING 2, 255): IN 32 = 32 байт
длина 32: «WinUSB echo 20d»
SETUP 80 06 03 03 09 04 FF 00 GET_DESCRIPTOR(STRING 3, 255): IN 50 = 50 байт
длина 50: «000000000000000000000000»
SETUP 80 06 EE 03 00 00 12 00 GET_DESCRIPTOR(STRING 238, 18): STALL
итог опознания: дескрипторы сходятся; MS OS 2.0: не найден
Comparison with Zephyr. The set and the BOS are built with the same structure initialiser
as in Zephyr's webusb example (the branch without CDC), only with our GUID and
code, and compared with our arrays byte by byte:
размеры структур Zephyr: заголовок набора 10, подмножество конфигурации 8, подмножество функции 8, совместимый ID 20, свойство GUID 132
BOS у Zephyr: корень 5, платформенная возможность 20 + MS OS 8 = 28
набор MS OS 2.0: Zephyr 162 байт, наш 162 байт, побайтно одинаковы
BOS: Zephyr 33 байт, наш 33 байт, побайтно одинаковы
MS OS 1.0 — the old way. Before the BOS it worked like this: a string with index 0xEE
and the signature "MSFT100" carries the vendor code. With it the host makes two
requests: wIndex 4 — the compatible ID, wIndex 5 — the extended properties.
This is how candleLight does it (firmware for USB–CAN adapters on STM32, section 19ж).
MDK-Middleware also supports only this way (usb_ms.h: string 0xEE,
indexes 4 and 5). A comparison from the run:
строка 0xEE: 18 байт, bLength 0x12, подпись «MSFT100», код 0x20
совместимый ID (wIndex 4): 64 байт, dwLength 0x40, секций 2
расширенные свойства (wIndex 5): 146 байт, dwLength 0x92, имя «DeviceInterfaceGUIDs» — есть
всего MS OS 1.0: 228 байт в трёх ответах
| MS OS 1.0 (candleLight) | MS OS 2.0 (this section) | |
|---|---|---|
| how the host finds out | string 0xEE with the signature "MSFT100" | the BOS, a platform capability with UUID D8DD60DF-… |
| condition | — | bcdUSB at least 2.01 |
| vendor requests | two: wIndex 4 and 5 |
one: wIndex 7 |
| bytes in the replies | 18 + 64 + 146 = 228 | 33 + 162 = 195 |
| GUID property name | DeviceInterfaceGUIDs |
DeviceInterfaceGUIDs |
The property name is written in different ways. candleLight and Zephyr use DeviceInterfaceGUIDs
(a list, type 7). The MDK default settings use DeviceInterfaceGUID (a single
string, type 1), and type 7 is not directly supported there. Which name Windows
reads was not tested in this section; ours is as in Zephyr and candleLight.
20д.4 Endpoint 0: vendor requests
Transfer stages, strings, the address and the configuration are as in 198ш.3. What is new is the vendor request branch in the SETUP parsing and the handler itself:
void usb_setup(const uint8_t s[8])
{
const uint8_t *p = 0;
uint16_t len = 0, wlength = (uint16_t)(s[6] | s[7] << 8);
uint8_t type = (s[0] >> 5) & 3u, rcpt = s[0] & 0x1Fu;
int r = REQ_STALL;
c.rx_want = 0;
if (type == 0) r = standard_request(s, &p, &len);
else if (type == 2 && rcpt == 0) r = vendor_request(s, &p, &len); /* производитель, устройству */
if (r == REQ_IN && (s[0] & 0x80u) && wlength) {
ep0_send(p, len, wlength);
} else if (r == REQ_OUT && !(s[0] & 0x80u) && wlength && wlength <= sizeof c.buf) {
memcpy(c.setup, s, 8); /* данные придут следующими пакетами OUT */
c.rx_len = 0;
c.rx_want = wlength;
} else if (r == REQ_NODATA && !wlength) {
ep0_status();
} else {
drv_ep0_stall();
}
}
/* Запросы производителя к устройству. Набор MS OS 2.0 отдаётся только на свой
* bRequest и wIndex = 7 и в любом состоянии — хост просит его до SET_CONFIGURATION.
* Свои команды — только в настроенном устройстве. */
static int vendor_request(const uint8_t s[8], const uint8_t **p, uint16_t *len)
{
uint8_t in = s[0] & 0x80u;
uint16_t wvalue = (uint16_t)(s[2] | s[3] << 8), windex = (uint16_t)(s[4] | s[5] << 8);
if (s[1] == MS_VENDOR_CODE) {
if (!in || windex != MS_OS_20_INDEX) return REQ_STALL;
*p = usb_msos20_set; *len = MSOS20_SET_SIZE;
return REQ_IN;
}
if (!c.config) return REQ_STALL;
switch (s[1]) {
case VREQ_GET_INFO: /* версия, светодиод, ключ, счётчики байт */
if (!in) return REQ_STALL;
c.buf[0] = 0x00; c.buf[1] = 0x01; /* версия протокола 1.00 */
c.buf[2] = app.led;
c.buf[3] = app.xor_key;
put32(c.buf + 4, app.rx_bytes);
put32(c.buf + 8, app.tx_bytes);
*p = c.buf; *len = INFO_SIZE;
return REQ_IN;
case VREQ_SET_LED:
if (in || wvalue > 1) return REQ_STALL;
app.led = (uint8_t)wvalue;
board_led(app.led);
return REQ_NODATA;
case VREQ_SET_XOR: /* байт ключа придёт в стадии данных */
return in ? REQ_STALL : REQ_OUT;
default:
return REQ_STALL;
}
}
static int vendor_data(const uint8_t s[8], const uint8_t *d, uint16_t n)
{
if (s[1] != VREQ_SET_XOR || n != 1) return REQ_STALL;
app.xor_key = d[0];
return REQ_NODATA;
}
Three rules built in here:
- the MS OS 2.0 set is returned before SET_CONFIGURATION. The host asks for it during enumeration, when there is no configuration yet. Custom commands, on the contrary, work only on a configured device;
- the MS code and the custom codes do not overlap. The set is returned only for
bRequest0x20 andwIndex7. Custom commands are 0x01…0x03. If a custom command takes the same code and does not look atwIndex, the host will get its reply instead of the set; - the recipient is the device (the low bits of
bmRequestTypeare 0). A request to the interface (C1) is refused.
GET_DESCRIPTOR also knows the new type: 0x0F — BOS.
static int get_descriptor(uint8_t type, uint8_t index, const uint8_t **p, uint16_t *len)
{
switch (type) {
case 0x01: *p = usb_device_desc; *len = DEVICE_DESC_SIZE; return 1;
case 0x02: *p = usb_config_desc; *len = CONFIG_DESC_SIZE; return index == 0;
case 0x03: *p = c.buf; *len = usb_string_desc(index, c.buf); return *len != 0;
case 0x0F: *p = usb_bos_desc; *len = BOS_DESC_SIZE; return index == 0;
default: return 0; /* 0x06 DEVICE_QUALIFIER: только полная скорость — отказ */
}
}
A run of refusals and edge cases:
SETUP C0 20 00 00 08 00 FF 00 производитель IN: bRequest 0x20, wValue 0, wIndex 8, wLength 255: STALL
SETUP C0 21 00 00 07 00 FF 00 производитель IN: bRequest 0x21, wValue 0, wIndex 7, wLength 255: STALL
SETUP 40 20 00 00 07 00 00 00 производитель OUT: bRequest 0x20, wValue 0, wIndex 7, wLength 0: STALL
SETUP C0 20 00 00 07 00 0A 00 производитель IN: bRequest 0x20, wValue 0, wIndex 7, wLength 10: IN 10 = 10 байт
0A 00 00 00 00 00 00 0A A2 00
SETUP C0 20 00 00 07 00 FF 00 производитель IN: bRequest 0x20, wValue 0, wIndex 7, wLength 255: IN 64+64+34 = 162 байт
SETUP C1 20 00 00 07 00 FF 00 производитель IN: bRequest 0x20, wValue 0, wIndex 7, wLength 255: STALL
SETUP C0 01 00 00 00 00 0C 00 производитель IN: bRequest 0x01, wValue 0, wIndex 0, wLength 12: STALL
SETUP 00 09 01 00 00 00 00 00 SET_CONFIGURATION(1): состояние принято
EP1R 0x3021: точка 1 открыта
SETUP C0 7F 00 00 00 00 01 00 производитель IN: bRequest 0x7F, wValue 0, wIndex 0, wLength 1: STALL
SETUP 40 02 02 00 00 00 00 00 производитель OUT: bRequest 0x02, wValue 2, wIndex 0, wLength 0: STALL
A request with wIndex 8 (MS_OS_20_SET_ALT_ENUMERATION in Zephyr) is refused:
bAltEnumCode = 0, there is no second variant. The set requested with 10 bytes is
exactly the header. A request for 255 bytes gives all 162: the short third packet
(34 bytes) itself tells the host "end". GET_INFO before configuration is refused, after
SET_CONFIGURATION it gives 12 bytes (20д.6). The device does not return string 0xEE:
it does not need MS OS 1.0.
20д.5 The F103 driver: packet memory and the EPnR register
The F103 does not have OTG like the F407 and U585, but its own USB FS block (RM0008, chapter 23). The endpoint buffers live in a 512-byte packet memory (section 20.3). The processor sees it as 16-bit words with a 4-byte step: the local address is multiplied by two (23.5.3). At the start of the memory there is the buffer table, 8 bytes per endpoint: the transmit address and count, the receive address and count. After it, in our case, come the 64-byte buffers: 0x40 and 0x80 for endpoint 0, 0xC0 and 0x100 for endpoint 1.
#define BT_ADDR_TX(n) PMA16(8u * (n))
#define BT_COUNT_TX(n) PMA16(8u * (n) + 2u)
#define BT_ADDR_RX(n) PMA16(8u * (n) + 4u)
#define BT_COUNT_RX(n) PMA16(8u * (n) + 6u)
#define BUF_TX(n) (0x40u + 0x80u * (n))
#define BUF_RX(n) (0x80u + 0x80u * (n))
static void pma_write(uint16_t a, const uint8_t *p, uint16_t n)
{
for (uint16_t i = 0; i < n; i += 2) {
uint16_t w = p[i];
if (i + 1u < n) w |= (uint16_t)(p[i + 1] << 8);
PMA16(a + i) = w;
}
}
The main trap is the EPnR endpoint register. Its fields are of three kinds (23.5.2):
STAT_RX, STAT_TX, DTOG_RX, DTOG_TX change when a one is written
(writing a zero does not touch them); the CTR_RX and CTR_TX flags are cleared by writing
a zero; the type, EP_KIND and the number are ordinary. Read-modify-write here
breaks everything: a one read back from STAT will flip the bit when written. That is why
the required value is obtained with an exclusive OR, and the CTR flags are written as ones:
/* Запись в EPnR без чтения-изменения-записи: STAT и DTOG меняются записью 1,
* флаги CTR сбрасываются записью 0. «Ничего не менять» — 0 в переключатели
* и 1 во флаги CTR. Здесь в поля mask ставятся значения want. */
static void ep_set(uint32_t n, uint16_t mask, uint16_t want)
{
uint16_t r = (uint16_t)USB_RD(R_EPR(n));
USB_WR(R_EPR(n), (r & EP_KEEP) | EP_CTR_RX | EP_CTR_TX | ((r ^ want) & mask));
}
static void ep_clear_ctr(uint32_t n, uint16_t flag)
{
uint16_t r = (uint16_t)USB_RD(R_EPR(n));
USB_WR(R_EPR(n), ((r & EP_KEEP) | EP_CTR_RX | EP_CTR_TX) & ~flag);
}
The receive buffer size is written into the receive count in blocks: up to 62 bytes
in 2-byte blocks, above that in 32-byte blocks (table 177; 64 bytes is BL_SIZE = 1,
NUM_BLOCK = 1):
/* Размер приёмного буфера в COUNTn_RX: до 62 байт — блоки по 2, дальше — по 32 (табл. 177) */
static uint16_t rx_count_field(uint16_t mps)
{
return mps > 62u ? (uint16_t)(0x8000u | (mps / 32u - 1u) << 10) : (uint16_t)((mps / 2u) << 10);
}
Opening an endpoint: the type and the number are written directly, the rest through ep_set.
The first packet of a bulk endpoint is DATA0, so DTOG is cleared together
with the state:
void drv_ep_open(uint8_t addr, uint8_t type, uint16_t mps)
{
static const uint16_t kind[4] = { 0x0200u, 0x0400u, 0x0000u, 0x0600u }; /* упр., изохр., масс., прер. */
uint32_t n = addr & 0x0Fu;
USB_WR(R_EPR(n), EP_CTR_RX | EP_CTR_TX | kind[type & 3u] | n); /* тип и номер */
if (addr & 0x80u) {
BT_ADDR_TX(n) = BUF_TX(n);
BT_COUNT_TX(n) = 0;
ep_set(n, EP_STAT_TX | EP_DTOG_TX, TX_NAK); /* первый пакет — DATA0 */
} else {
BT_ADDR_RX(n) = BUF_RX(n);
BT_COUNT_RX(n) = rx_count_field(mps);
ep_set(n, EP_STAT_RX | EP_DTOG_RX, RX_NAK);
}
}
The address comes after the status stage. On OTG the address is written to the register right at
SETUP: RM0090 says so (34.17.5, "Endpoint initialization on SetAddress
command"), and that is how 198ш does it. On the F103 it is the opposite: until the host has taken the empty
status packet, the device must answer at address 0. The driver
remembers the address and writes it to DADDR on CTR_TX of endpoint 0:
void drv_set_address(uint8_t addr)
{
dev_addr = addr; /* в DADDR — только после стадии состояния */
addr_pending = 1;
}
void usb_f103_irq(void)
{
uint16_t istr;
while ((istr = (uint16_t)USB_RD(R_ISTR)) & (ISTR_CTR | ISTR_RESET)) {
if (istr & ISTR_RESET) {
USB_WR(R_ISTR, (uint16_t)~ISTR_RESET); /* флаги ISTR сбрасываются нулём */
bus_reset();
continue;
}
uint32_t n = istr & ISTR_EP_ID;
uint16_t r = (uint16_t)USB_RD(R_EPR(n));
if (r & EP_CTR_RX) {
uint16_t cnt = BT_COUNT_RX(n) & 0x3FFu;
if (cnt > sizeof rxbuf) cnt = sizeof rxbuf;
pma_read((uint16_t)BUF_RX(n), rxbuf, cnt);
ep_clear_ctr(n, EP_CTR_RX);
if (n == 0) {
if (r & EP_SETUP) {
ep0_stalled = 0;
usb_setup(rxbuf);
} else {
usb_ep0_out(rxbuf, cnt);
}
if (!ep0_stalled) ep_set(0, EP_STAT_RX, RX_VALID);
} else if (usb_ep_out((uint8_t)n, rxbuf, cnt)) {
ep_set(n, EP_STAT_RX, RX_VALID);
}
}
if (r & EP_CTR_TX) {
ep_clear_ctr(n, EP_CTR_TX);
if (n == 0) {
if (addr_pending) { /* стадия состояния SET_ADDRESS прошла */
addr_pending = 0;
USB_WR(R_DADDR, DADDR_EF | dev_addr);
}
usb_ep0_in_done();
} else {
usb_ep_in_done((uint8_t)(0x80u | n));
}
}
}
}
Power-up follows 23.4.2: clear PDWN, wait for the transceiver to start
(tSTARTUP — up to 1 µs, table 43 of DocID13587), clear FRES, clear the flags.
/* Включение: PDWN снять, подождать запуска приёмопередатчика (tSTARTUP до 1 мкс),
* снять FRES, сбросить ложные флаги. Такт USB и APB1 к этому моменту включены. */
void usb_f103_init(void)
{
USB_WR(R_CNTR, CNTR_FRES);
for (volatile uint32_t i = 0; i < 100u; i++) { }
USB_WR(R_CNTR, 0);
USB_WR(R_ISTR, 0);
USB_WR(R_CNTR, CNTR_CTRM | CNTR_RESETM);
}
The block model. On the computer the driver is built together with a model of the USB
FS block: the registers, the packet memory and the SETUP/IN/OUT transactions behave the way
RM0008 describes them. Toggle bits flip when 1 is written, CTR is cleared by
zero. SETUP is accepted with STAT_RX = NAK and STALL, but not with
CTR_RX set. A packet with the wrong DATA0/1 is acknowledged and discarded. A device
with a different address stays silent. The host (host_model.py) talks to the model as over a bus and
tracks DATA0/1 itself. The run starts with a bus reset:
сброс шины: адрес 0, DADDR 0x80, EP0R 0x3220
SETUP 80 06 00 01 00 00 40 00 GET_DESCRIPTOR(DEVICE 0, 64): IN 18 = 18 байт
12 01 01 02 00 00 00 40 09 12 02 00 00 01 01 02 03 01
SETUP 00 05 05 00 00 00 00 00 SET_ADDRESS(5): состояние принято
адрес устройства: 5 (DADDR 0x85)
EP0R = 0x3220: receive is enabled (STAT_RX = 11), transmit is NAK (STAT_TX =
10), type CONTROL (01). Address 5 appeared in DADDR only after the status
stage. If you write it immediately, as on OTG, the host loses the device:
== 6. Ошибка: адрес записан в DADDR сразу, в стадии SETUP
SETUP 80 06 00 01 00 00 40 00 GET_DESCRIPTOR(DEVICE 0, 64): IN 18 = 18 байт
SET_ADDRESS(5): состояние IN: нет ответа; DADDR 0x85 — устройство уже слушает адрес 5
Over the whole run the driver wrote to EPnR 192 times, and the run passed without discrepancies: a flipped bit would have desynchronised DATA0/1 or the endpoint states.
F407. The OTG_FS driver is from 198ш.6 with three differences: the block address is
0x50000000; VBUS sensing is switched off by the NOVBUSSENS bit (bit 21 of GCCFG), while on the U585 the same
bit is VBDEN with the opposite meaning (section 20.5); the F407 has no session signal override in
GOTGCTL. The FIFO memory is 1.25 KB, 320 words (34.2); the receive
FIFO is given 64 words, while the calculation in 34.13.1 requires at least 47: 10 for SETUP,
1 for Global OUT NAK, two packets of 64/4 + 1 and one word for each OUT endpoint.
/* Ядро OTG_FS: такт AHB2 и выводы PA11/PA12 к этому моменту включены (main_f407.c) */
void otg_f407_init(void)
{
REG(R_GAHBCFG) &= ~GAHBCFG_GINT;
REG(R_GUSBCFG) |= GUSBCFG_PHYSEL; /* встроенный приёмопередатчик FS */
while (!(REG(R_GRSTCTL) & GRSTCTL_AHBIDL)) { }
REG(R_GRSTCTL) |= GRSTCTL_CSRST; /* сброс ядра */
while (REG(R_GRSTCTL) & GRSTCTL_CSRST) { }
REG(R_GCCFG) = GCCFG_PWRDWN | GCCFG_NOVBUSSENS; /* приёмопередатчик включён, VBUS не следим */
REG(R_GUSBCFG) = (REG(R_GUSBCFG) & ~GUSBCFG_TRDT_Msk) | GUSBCFG_FDMOD | 6u << GUSBCFG_TRDT_Pos;
REG(R_DCTL) |= DCTL_SDIS; /* пока не готовы — подтяжка D+ снята */
REG(R_PCGCCTL) = 0;
REG(R_DCFG) |= DCFG_DSPD_FS;
fifo_setup();
flush_fifos();
REG(R_DIEPMSK) = EPINT_XFRC;
REG(R_DOEPMSK) = EPINT_XFRC | EPINT_STUP;
REG(R_DAINTMSK) = 0;
REG(R_GINTSTS) = 0xBFFFFFFFu;
REG(R_GINTMSK) = GINT_USBRST | GINT_ENUMDNE | GINT_RXFLVL | GINT_IEPINT | GINT_OEPINT;
REG(R_GAHBCFG) |= GAHBCFG_GINT;
REG(R_DCTL) &= ~DCTL_SDIS; /* подтяжка D+: хост видит устройство */
}
The offsets and bits of both drivers are checked against the CMSIS headers at build time:
selfcheck_f103.c and selfcheck_f407.c consist of _Static_assert.
20д.6 The PC program, the echo and the sizes
The echo on the device side is two functions. Until the reply has been taken, a new OUT packet is not accepted: the host gets a NAK and waits.
/* Эхо: принятый пакет, «посоленный» ключом, уходит обратно в точку IN.
* Пока ответ не забран хостом, новых пакетов OUT не принимаем — хост получит NAK. */
int usb_ep_out(uint8_t addr, const uint8_t *p, uint16_t n)
{
if (addr != EP_OUT || app.in_busy) return 0;
for (uint16_t i = 0; i < n; i++)
app.pkt[i] = p[i] ^ app.xor_key;
app.rx_bytes += n;
app.in_len = n;
app.in_busy = 1;
(void)drv_ep_tx(EP_IN, app.pkt, n); /* точка IN свободна: за этим следит in_busy */
return 0; /* приём откроет usb_ep_in_done */
}
void usb_ep_in_done(uint8_t addr)
{
if (addr != EP_IN || !app.in_busy) return;
app.tx_bytes += app.in_len;
app.in_busy = 0;
drv_ep_rx_arm(EP_OUT);
}
The PC program (vend_pc.py) talks to the device with three calls in
pyusb style: ctrl_transfer, write, read. With a real device the device object
is provided by pyusb on top of libusb; in the check it is the host model with the same
three methods.
def get_info(dev):
b = bytes(dev.ctrl_transfer(REQ_VENDOR_IN, VREQ_GET_INFO, 0, 0, 12))
return {"version": "%d.%02d" % (b[1], b[0]), "led": b[2], "xor": b[3],
"rx": int.from_bytes(b[4:8], "little"), "tx": int.from_bytes(b[8:12], "little")}
def set_xor(dev, key):
dev.ctrl_transfer(REQ_VENDOR_OUT, VREQ_SET_XOR, 0, 0, bytes([key]))
def echo(dev, data):
"""Отправить данные пакетами по 64 байта; на каждый пакет устройство отвечает своим."""
back = b""
for i in range(0, len(data), 64):
chunk = data[i:i + 64]
dev.write(EP_OUT, chunk)
back += bytes(dev.read(EP_IN, 64))
return back
A run of the program on top of the F103 model:
SETUP C0 01 00 00 00 00 0C 00 производитель IN: bRequest 0x01, wValue 0, wIndex 0, wLength 12: IN 12 = 12 байт
состояние: {'version': '1.00', 'led': 0, 'xor': 0, 'rx': 0, 'tx': 0}
SETUP 40 02 01 00 00 00 00 00 производитель OUT: bRequest 0x02, wValue 1, wIndex 0, wLength 0: состояние принято
SETUP 40 03 00 00 00 00 01 00 производитель OUT: bRequest 0x03, wValue 0, wIndex 0, wLength 1: OUT 1 байт, состояние принято
эхо: отправлено 200, принято 200, совпало с ожидаемым: да
SETUP C0 01 00 00 00 00 0C 00 производитель IN: bRequest 0x01, wValue 0, wIndex 0, wLength 12: IN 12 = 12 байт
состояние: {'version': '1.00', 'led': 1, 'xor': 90, 'rx': 200, 'tx': 200}
светодиод: 1, вызовов board_led: 1
программа на ПК: работает
200 bytes went out in four packets (64 + 64 + 64 + 8) and came back as the same
bytes XORed with the key 0x5A. The LED was turned on with a single
board_led call.
Streaming and halting an endpoint:
два пакета OUT без чтения: первый ACK, второй NAK
IN: 64 байт, первый байт 0x5B
повтор второго: ACK, IN 64 байт, первый байт 0x58
IN без запроса: NAK
SETUP 02 03 00 00 01 00 00 00 SET_FEATURE(ENDPOINT_HALT, точка 0x01): состояние принято
OUT в остановленную точку: STALL
SETUP 82 00 00 00 01 00 02 00 GET_STATUS: IN 2 = 2 байт
GET_STATUS точки 0x01: 01 00
SETUP 02 01 00 00 01 00 00 00 CLEAR_FEATURE(ENDPOINT_HALT, точка 0x01): состояние принято
после CLEAR_FEATURE: OUT ACK, IN 1 байт 0x5E, EP1R 0x7061
SETUP C0 01 00 00 00 00 0C 00 производитель IN: bRequest 0x01, wValue 0, wIndex 0, wLength 12: IN 12 = 12 байт
состояние: {'version': '1.00', 'led': 1, 'xor': 90, 'rx': 329, 'tx': 329}
записей в EPnR драйвером за прогон: 192
- the second packet sent without a read got a NAK: the device keeps the OUT endpoint closed while the reply sits in the IN buffer. The host repeated it after the read — ACK;
SET_FEATURE(ENDPOINT_HALT)puts the endpoint into STALL,GET_STATUSshows it (01 00). AfterCLEAR_FEATUREboth sides start with DATA0: the driver clearsDTOG_RX, the host its own counter. If the host forgets, the first packet will be lost silently — acknowledged but discarded;- the counters agree: 200 + 64 + 64 + 1 = 329 bytes there and back.
In Windows the device is found by its GUID with SetupAPI functions: SetupDiGetClassDevsW
with the flags DIGCF_PRESENT | DIGCF_DEVICEINTERFACE, then
SetupDiEnumDeviceInterfaces and SetupDiGetDeviceInterfaceDetailW give the device
path; the file at that path is opened and passed to WinUsb_Initialize.
Then come WinUsb_ControlTransfer, WinUsb_WritePipe, WinUsb_ReadPipe,
WinUsb_ResetPipe (SetupAPI.h, winusb.h of Windows SDK 10.0.19041). Such a
program was neither written nor run in this section.
Sizes — arm-none-eabi-gcc 12.2, -Os, without the HAL library:
| File | Cortex-M3 (F103): code / data / RAM | Cortex-M4 (F407): code / data / RAM |
|---|---|---|
vend_desc.c — descriptors and strings |
441 / 25 / 0 | 441 / 25 / 0 |
vend_core.c — endpoint 0, requests, echo |
948 / 0 / 168 | 948 / 0 / 168 |
driver: usb_f103.c / otg_f407.c |
974 / 0 / 67 | 1090 / 0 / 148 |
main_f103.c / main_f407.c — clocks, pins, vector table |
406 / 0 / 0 | 718 / 0 / 0 |
| the whole image | 3160 / 28 / 236 | 3664 / 28 / 316 |
All four descriptors in the F103 image are 18 + 32 + 33 + 162 = 245 bytes of Flash. The whole device on the F103 is less than 3.5 KB, on the F407 about 3.6 KB: without the ST library and without an RTOS.
20д.7 Pitfalls
WinUSB does not install although the firmware has the set. Check bcdUSB.
With version 2.00 the host does not ask for the BOS (20д.3): the set is never
requested.
The host asks for the set and gets a fragment. The length in the BOS, wTotalLength in
the set header and the sum of the records disagree. Keep all lengths in macros and
check the array sizes with _Static_assert: in an array with a given length the compiler
will silently fill a missing byte with zero.
A custom command's reply instead of the set. Your own vendor request took
the same bRequest as bMS_VendorCode and does not look at wIndex. Separate
the codes; return the set only for wIndex 7.
The set is not returned during enumeration. The handler requires SET_CONFIGURATION, but the host asks for the set earlier. MS OS works in any state, custom commands only after configuration.
The program cannot find the GUID. The string has no curly braces, is not in UTF-16LE or lacks the second null at the end of the REG_MULTI_SZ list. We have 38 characters in braces and 4 zero bytes at the end.
The F103 stops responding after SET_ADDRESS. The address was written to DADDR in
the SETUP stage: the host still sends the empty status packet to address 0, while the device
already listens on 5 (20д.5, "no response"). On OTG it is the opposite, the address is written immediately.
F103 endpoints "go crazy" after a write to EPnR. Read-modify-write
flips the STAT and DTOG bits and clears other CTR flags. Only
write through an exclusive OR with a mask and with ones in CTR.
After clearing a halt the first packet was lost. The host or the device did not
return to DATA0. After CLEAR_FEATURE(ENDPOINT_HALT) both use DATA0.
The device on the F407 is not visible, although the code worked on the U585. Bit 21 of GCCFG: on
the U585 it is VBDEN (0 — do not monitor VBUS), on the F407 it is NOVBUSSENS (1 — do not
monitor). Porting the line "as is" turns VBUS monitoring on.
Signatures and the Windows cache. How Windows remembers the reply to the MS OS descriptors
and whether you need to change bcdDevice or the PID after editing the set was not
tested in this section. On a prototype it is safer not to touch the set after the first
connection to a PC without changing the PID.
20д.8 Summary
- A class of your own, 0xFF, with two 64-byte bulk endpoints: an 18-byte device
descriptor with
bcdUSB2.01, a 32-byte configuration. - A 33-byte BOS: a root of 5 + a 28-byte MS OS 2.0 platform capability (UUID D8DD60DF-4589-4CC7-9CD2-659D9E648A9F, code 0x20, a 162-byte set).
- The MS OS 2.0 set is 162 bytes: header 10 + compatible ID "WINUSB" 20 +
the
DeviceInterfaceGUIDsproperty 132 (name 42 bytes, GUID 80 bytes). The host gets it with the request C0 20 withwIndex7, in packets of 64 + 64 + 34. The set and the BOS matched the Zephyr 4.4.2 structures byte by byte. - MS OS 1.0 (candleLight): string 0xEE and requests with
wIndex4 and 5, 228 bytes in total versus 195 for MS OS 2.0. - The F103 driver: packet memory — the address is multiplied by 2, EPnR — writes through XOR and ones in CTR, the address — after the status stage. On the block model, enumeration, refusals, a 200-byte echo, streaming NAK and endpoint halt all passed; 192 writes to EPnR, no discrepancies.
- The F407 driver is the OTG one from 198ш with
NOVBUSSENS; RX FIFO is 64 words against the computed 47. - The whole image: F103 — 3160 bytes of code, 28 of data, 236 of RAM; F407 — 3664, 28 and 316.
Full source code
vend.h — the common device header: endpoints, requests, descriptor lengths.
/* vend.h — своё USB-устройство без драйвера: класс 0xFF, две массовые точки,
* BOS с возможностью MS OS 2.0 и совместимый ID «WINUSB». Раздел 20д.
* Общая часть без железа: дескрипторы, точка 0, эхо. Драйверы — usb_f103.c, otg_f407.c. */
#ifndef VEND_H
#define VEND_H
#include <stdint.h>
#ifndef USB_BCD_USB
#define USB_BCD_USB 0x0201u /* 2.01: только у такого устройства хост спрашивает BOS */
#endif
#define USB_VID 0x1209u /* заглушки для макета: в изделии — свои номера */
#define USB_PID 0x0002u
#define EP0_SIZE 64u
#define EP_OUT 0x01u /* массовая OUT, 64 байта */
#define EP_IN 0x81u /* массовая IN, 64 байта */
#define BULK_SIZE 64u
#define EP_TYPE_BULK 2u
#define MS_VENDOR_CODE 0x20u /* bRequest, которым хост просит набор MS OS 2.0 */
#define MS_OS_20_INDEX 7u /* wIndex того же запроса */
#define VREQ_GET_INFO 0x01u /* свои запросы: IN, 12 байт состояния */
#define VREQ_SET_LED 0x02u /* без данных: wValue = 0 или 1 */
#define VREQ_SET_XOR 0x03u /* OUT, 1 байт: им «солится» эхо */
#define INFO_SIZE 12u
#define DEVICE_DESC_SIZE 18u
#define CONFIG_DESC_SIZE 32u
#define BOS_DESC_SIZE 33u
#define MSOS20_SET_SIZE 162u
#define LE16(x) ((x) & 0xFFu), (((x) >> 8) & 0xFFu)
/* длины массивов — в макросах выше; совпадение проверяет _Static_assert в vend_desc.c */
extern const uint8_t usb_device_desc[];
extern const uint8_t usb_config_desc[];
extern const uint8_t usb_bos_desc[];
extern const uint8_t usb_msos20_set[];
uint16_t usb_string_desc(uint8_t index, uint8_t *buf); /* длина или 0, если строки нет */
void usb_serial_from_uid(const uint32_t uid[3]); /* 96 бит номера кристалла -> 24 знака */
/* --- что ядро просит у драйвера (usb_f103.c, otg_f407.c; в модели — usb_f103.c + usbfs_model.c) --- */
void drv_ep0_tx(const uint8_t *p, uint16_t n); /* один пакет IN точки 0, n <= 64 */
void drv_ep0_stall(void);
void drv_set_address(uint8_t addr); /* вызывается в стадии SETUP */
void drv_ep_open(uint8_t addr, uint8_t type, uint16_t mps);
void drv_ep_close(uint8_t addr);
int drv_ep_tx(uint8_t addr, const uint8_t *p, uint16_t n); /* 0 — пакет взят, -1 — точка занята */
void drv_ep_rx_arm(uint8_t addr); /* снова принимать OUT */
void drv_ep_stall(uint8_t addr, int on);
/* --- что драйвер сообщает ядру (vend_core.c) --- */
void usb_reset(void);
void usb_setup(const uint8_t s[8]);
void usb_ep0_out(const uint8_t *p, uint16_t n);
void usb_ep0_in_done(void);
int usb_ep_out(uint8_t addr, const uint8_t *p, uint16_t n); /* 1 — можно сразу принимать дальше */
void usb_ep_in_done(uint8_t addr);
uint8_t usb_configured(void);
void board_led(uint8_t on); /* светодиод платы */
#endif
vend_desc.c — the descriptors byte by byte: device, configuration, BOS, MS OS 2.0, strings.
/* vend_desc.c — дескрипторы устройства по байтам: устройство, конфигурация, BOS,
* набор MS OS 2.0 и строки. Раздел 20д. Длины, записанные внутри дескрипторов,
* берутся из макросов vend.h, а _Static_assert в конце сверяет их с числом байт. */
#include "vend.h"
#define U16(ch) (ch), 0x00 /* знак UTF-16LE */
const uint8_t usb_device_desc[] = {
18, 0x01, /* bLength, DEVICE */
LE16(USB_BCD_USB), /* bcdUSB 2.01: у устройства есть BOS */
0x00, 0x00, 0x00, /* класс, подкласс, протокол — смотри интерфейс */
EP0_SIZE, /* bMaxPacketSize0 */
LE16(USB_VID),
LE16(USB_PID),
0x00, 0x01, /* bcdDevice 1.00 */
1, 2, 3, /* строки: изготовитель, изделие, серийный номер */
1 /* одна конфигурация */
};
const uint8_t usb_config_desc[] = {
/* конфигурация */
9, 0x02, LE16(CONFIG_DESC_SIZE), /* wTotalLength = 32 */
1, /* bNumInterfaces */
1, /* bConfigurationValue */
0, /* iConfiguration */
0x80, /* bmAttributes: питание от шины */
50, /* bMaxPower: 50 x 2 мА = 100 мА */
/* интерфейс 0: свой класс */
9, 0x04, 0, 0, 2, /* номер 0, настройка 0, две точки */
0xFF, 0x00, 0x00, /* класс 0xFF «у производителя», подкласс и протокол — свои */
0, /* iInterface */
/* точки */
7, 0x05, EP_OUT, EP_TYPE_BULK, LE16(BULK_SIZE), 0,
7, 0x05, EP_IN, EP_TYPE_BULK, LE16(BULK_SIZE), 0
};
const uint8_t usb_bos_desc[] = {
5, 0x0F, /* bLength, BOS */
LE16(BOS_DESC_SIZE), /* wTotalLength = 33: корень и все возможности */
1, /* bNumDeviceCaps */
/* платформенная возможность MS OS 2.0 */
28, 0x10, 0x05, /* bLength, DEVICE_CAPABILITY, PLATFORM */
0x00, /* bReserved */
0xDF, 0x60, 0xDD, 0xD8, 0x89, 0x45, 0xC7, 0x4C, /* UUID D8DD60DF-4589-4CC7- */
0x9C, 0xD2, 0x65, 0x9D, 0x9E, 0x64, 0x8A, 0x9F, /* 9CD2-659D9E648A9F */
0x00, 0x00, 0x00, 0x0A, /* dwWindowsVersion 0x0A000000: Windows 10 */
LE16(MSOS20_SET_SIZE), /* wMSOSDescriptorSetTotalLength = 162 */
MS_VENDOR_CODE, /* bMS_VendorCode: этим bRequest набор и попросят */
0x00 /* bAltEnumCode: второго варианта нет */
};
const uint8_t usb_msos20_set[] = {
/* заголовок набора */
0x0A, 0x00, /* wLength = 10 */
0x00, 0x00, /* MS_OS_20_SET_HEADER_DESCRIPTOR */
0x00, 0x00, 0x00, 0x0A, /* dwWindowsVersion 0x0A000000 */
LE16(MSOS20_SET_SIZE), /* wTotalLength = 162: весь набор */
/* совместимый ID: какой драйвер ставить */
0x14, 0x00, /* wLength = 20 */
0x03, 0x00, /* MS_OS_20_FEATURE_COMPATIBLE_ID */
'W', 'I', 'N', 'U', 'S', 'B', 0x00, 0x00, /* CompatibleID */
0, 0, 0, 0, 0, 0, 0, 0, /* SubCompatibleID: нет */
/* свойство реестра: GUID, по которому программа найдёт устройство */
0x84, 0x00, /* wLength = 132 */
0x04, 0x00, /* MS_OS_20_FEATURE_REG_PROPERTY */
0x07, 0x00, /* wPropertyDataType: REG_MULTI_SZ, список строк */
0x2A, 0x00, /* wPropertyNameLength = 42 */
U16('D'), U16('e'), U16('v'), U16('i'), U16('c'), U16('e'),
U16('I'), U16('n'), U16('t'), U16('e'), U16('r'), U16('f'), U16('a'), U16('c'), U16('e'),
U16('G'), U16('U'), U16('I'), U16('D'), U16('s'), 0x00, 0x00,
0x50, 0x00, /* wPropertyDataLength = 80 */
U16('{'), U16('9'), U16('D'), U16('E'), U16('E'), U16('3'), U16('3'), U16('1'), U16('4'),
U16('-'), U16('0'), U16('6'), U16('2'), U16('6'),
U16('-'), U16('4'), U16('A'), U16('2'), U16('1'),
U16('-'), U16('B'), U16('C'), U16('E'), U16('4'),
U16('-'), U16('9'), U16('4'), U16('5'), U16('6'), U16('A'), U16('E'), U16('2'), U16('2'),
U16('7'), U16('C'), U16('2'), U16('8'), U16('}'),
0x00, 0x00, 0x00, 0x00 /* конец строки и конец списка */
};
_Static_assert(sizeof usb_device_desc == DEVICE_DESC_SIZE, "дескриптор устройства: не 18 байт");
_Static_assert(sizeof usb_config_desc == CONFIG_DESC_SIZE, "конфигурация: wTotalLength не сходится");
_Static_assert(sizeof usb_bos_desc == BOS_DESC_SIZE, "BOS: wTotalLength не сходится");
_Static_assert(sizeof usb_msos20_set == MSOS20_SET_SIZE, "набор MS OS 2.0: wTotalLength не сходится");
static char serial[25] = "000000000000000000000000";
void usb_serial_from_uid(const uint32_t uid[3])
{
static const char hex[] = "0123456789ABCDEF";
for (int w = 0; w < 3; w++)
for (int i = 0; i < 8; i++)
serial[w * 8 + i] = hex[(uid[w] >> (28 - 4 * i)) & 0xFu];
}
uint16_t usb_string_desc(uint8_t index, uint8_t *buf)
{
static const char *const text[] = { "STM32 Handbook", "WinUSB echo 20d", serial };
if (index == 0) { /* список языков: 0x0409, английский (США) */
buf[0] = 4; buf[1] = 0x03; buf[2] = 0x09; buf[3] = 0x04;
return 4;
}
if (index > 3) return 0; /* в том числе 0xEE: MS OS 1.0 не отдаём */
const char *s = text[index - 1];
uint16_t n = 2;
while (*s && n + 2u <= EP0_SIZE) {
buf[n++] = (uint8_t)*s++;
buf[n++] = 0;
}
buf[0] = (uint8_t)n;
buf[1] = 0x03;
return n;
}
vend_core.c — endpoint 0, vendor requests and the echo.
/* vend_core.c — точка 0 (стандартные запросы, BOS, запросы производителя)
* и эхо по массовым точкам. Раздел 20д. Железа не касается: всё через drv_*. */
#include <string.h>
#include "vend.h"
enum { REQ_STALL = -1, REQ_NODATA = 0, REQ_IN = 1, REQ_OUT = 2 };
static struct {
const uint8_t *tx; /* стадия данных IN: что осталось отдать */
uint16_t tx_left;
uint8_t tx_zlp; /* после полных пакетов нужен пустой */
uint8_t setup[8]; /* запрос, чьи данные OUT ещё идут */
uint16_t rx_want, rx_len;
uint8_t buf[64];
uint8_t config;
uint8_t halt_in, halt_out;
} c;
static struct {
uint8_t led, xor_key, in_busy;
uint16_t in_len;
uint32_t rx_bytes, tx_bytes;
uint8_t pkt[BULK_SIZE];
} app;
static void ep0_next(void)
{
uint16_t n = c.tx_left < EP0_SIZE ? c.tx_left : EP0_SIZE;
drv_ep0_tx(c.tx, n);
c.tx += n;
c.tx_left -= n;
}
static void ep0_send(const uint8_t *p, uint16_t len, uint16_t wlength)
{
if (len > wlength) len = wlength;
c.tx = p;
c.tx_left = len;
c.tx_zlp = len < wlength && len != 0 && len % EP0_SIZE == 0;
ep0_next();
}
void usb_ep0_in_done(void)
{
if (c.tx_left) ep0_next();
else if (c.tx_zlp) { c.tx_zlp = 0; drv_ep0_tx(0, 0); }
}
static void ep0_status(void) { drv_ep0_tx(0, 0); }
static int get_descriptor(uint8_t type, uint8_t index, const uint8_t **p, uint16_t *len)
{
switch (type) {
case 0x01: *p = usb_device_desc; *len = DEVICE_DESC_SIZE; return 1;
case 0x02: *p = usb_config_desc; *len = CONFIG_DESC_SIZE; return index == 0;
case 0x03: *p = c.buf; *len = usb_string_desc(index, c.buf); return *len != 0;
case 0x0F: *p = usb_bos_desc; *len = BOS_DESC_SIZE; return index == 0;
default: return 0; /* 0x06 DEVICE_QUALIFIER: только полная скорость — отказ */
}
}
static uint8_t *halt_flag(uint8_t ep)
{
if (!c.config) return 0;
if (ep == EP_IN) return &c.halt_in;
if (ep == EP_OUT) return &c.halt_out;
return 0;
}
static void set_config(uint8_t v)
{
if (c.config) {
drv_ep_close(EP_OUT);
drv_ep_close(EP_IN);
}
c.config = v;
c.halt_in = c.halt_out = 0;
app.in_busy = 0;
if (v) {
drv_ep_open(EP_OUT, EP_TYPE_BULK, BULK_SIZE);
drv_ep_open(EP_IN, EP_TYPE_BULK, BULK_SIZE);
drv_ep_rx_arm(EP_OUT);
}
}
static int standard_request(const uint8_t s[8], const uint8_t **p, uint16_t *len)
{
uint8_t rcpt = s[0] & 0x1Fu, req = s[1];
uint16_t wvalue = (uint16_t)(s[2] | s[3] << 8), windex = (uint16_t)(s[4] | s[5] << 8);
uint8_t *h;
switch (req) {
case 0x00: /* GET_STATUS */
if (rcpt == 2 && (h = halt_flag((uint8_t)windex)) != 0) c.buf[0] = *h;
else if (rcpt == 2 && (windex & 0x7Fu) != 0) return REQ_STALL;
else c.buf[0] = 0; /* питание от шины, будить хост не умеем */
c.buf[1] = 0;
*p = c.buf; *len = 2;
return REQ_IN;
case 0x01: case 0x03: /* CLEAR_FEATURE, SET_FEATURE: только ENDPOINT_HALT */
if (rcpt != 2 || wvalue != 0 || (h = halt_flag((uint8_t)windex)) == 0) return REQ_STALL;
*h = req == 0x03;
drv_ep_stall((uint8_t)windex, *h);
if (!*h) { /* остановку сняли: эхо начинается с чистого листа */
app.in_busy = 0;
if (!c.halt_out) drv_ep_rx_arm(EP_OUT);
}
return REQ_NODATA;
case 0x05: /* SET_ADDRESS: когда писать в регистр, решает драйвер */
drv_set_address((uint8_t)(wvalue & 0x7Fu));
return REQ_NODATA;
case 0x06: /* GET_DESCRIPTOR */
return get_descriptor(s[3], s[2], p, len) ? REQ_IN : REQ_STALL;
case 0x08: /* GET_CONFIGURATION */
c.buf[0] = c.config;
*p = c.buf; *len = 1;
return REQ_IN;
case 0x09: /* SET_CONFIGURATION */
if (wvalue > 1) return REQ_STALL;
set_config((uint8_t)wvalue);
return REQ_NODATA;
case 0x0A: /* GET_INTERFACE: запасных настроек нет */
if (!c.config || windex != 0) return REQ_STALL;
c.buf[0] = 0;
*p = c.buf; *len = 1;
return REQ_IN;
case 0x0B: /* SET_INTERFACE */
return (c.config && windex == 0 && wvalue == 0) ? REQ_NODATA : REQ_STALL;
default:
return REQ_STALL;
}
}
static void put32(uint8_t *d, uint32_t v)
{
d[0] = (uint8_t)v; d[1] = (uint8_t)(v >> 8); d[2] = (uint8_t)(v >> 16); d[3] = (uint8_t)(v >> 24);
}
/* Запросы производителя к устройству. Набор MS OS 2.0 отдаётся только на свой
* bRequest и wIndex = 7 и в любом состоянии — хост просит его до SET_CONFIGURATION.
* Свои команды — только в настроенном устройстве. */
static int vendor_request(const uint8_t s[8], const uint8_t **p, uint16_t *len)
{
uint8_t in = s[0] & 0x80u;
uint16_t wvalue = (uint16_t)(s[2] | s[3] << 8), windex = (uint16_t)(s[4] | s[5] << 8);
if (s[1] == MS_VENDOR_CODE) {
if (!in || windex != MS_OS_20_INDEX) return REQ_STALL;
*p = usb_msos20_set; *len = MSOS20_SET_SIZE;
return REQ_IN;
}
if (!c.config) return REQ_STALL;
switch (s[1]) {
case VREQ_GET_INFO: /* версия, светодиод, ключ, счётчики байт */
if (!in) return REQ_STALL;
c.buf[0] = 0x00; c.buf[1] = 0x01; /* версия протокола 1.00 */
c.buf[2] = app.led;
c.buf[3] = app.xor_key;
put32(c.buf + 4, app.rx_bytes);
put32(c.buf + 8, app.tx_bytes);
*p = c.buf; *len = INFO_SIZE;
return REQ_IN;
case VREQ_SET_LED:
if (in || wvalue > 1) return REQ_STALL;
app.led = (uint8_t)wvalue;
board_led(app.led);
return REQ_NODATA;
case VREQ_SET_XOR: /* байт ключа придёт в стадии данных */
return in ? REQ_STALL : REQ_OUT;
default:
return REQ_STALL;
}
}
static int vendor_data(const uint8_t s[8], const uint8_t *d, uint16_t n)
{
if (s[1] != VREQ_SET_XOR || n != 1) return REQ_STALL;
app.xor_key = d[0];
return REQ_NODATA;
}
void usb_setup(const uint8_t s[8])
{
const uint8_t *p = 0;
uint16_t len = 0, wlength = (uint16_t)(s[6] | s[7] << 8);
uint8_t type = (s[0] >> 5) & 3u, rcpt = s[0] & 0x1Fu;
int r = REQ_STALL;
c.rx_want = 0;
if (type == 0) r = standard_request(s, &p, &len);
else if (type == 2 && rcpt == 0) r = vendor_request(s, &p, &len); /* производитель, устройству */
if (r == REQ_IN && (s[0] & 0x80u) && wlength) {
ep0_send(p, len, wlength);
} else if (r == REQ_OUT && !(s[0] & 0x80u) && wlength && wlength <= sizeof c.buf) {
memcpy(c.setup, s, 8); /* данные придут следующими пакетами OUT */
c.rx_len = 0;
c.rx_want = wlength;
} else if (r == REQ_NODATA && !wlength) {
ep0_status();
} else {
drv_ep0_stall();
}
}
void usb_ep0_out(const uint8_t *p, uint16_t n)
{
if (!c.rx_want) return; /* пустой пакет — стадия состояния после IN */
if (n > c.rx_want - c.rx_len) n = (uint16_t)(c.rx_want - c.rx_len);
memcpy(c.buf + c.rx_len, p, n);
c.rx_len = (uint16_t)(c.rx_len + n);
if (c.rx_len < c.rx_want) return;
c.rx_want = 0;
if (vendor_data(c.setup, c.buf, c.rx_len) == REQ_NODATA) ep0_status();
else drv_ep0_stall();
}
void usb_reset(void)
{
memset(&c, 0, sizeof c);
app.in_busy = 0;
}
uint8_t usb_configured(void) { return c.config; }
/* Эхо: принятый пакет, «посоленный» ключом, уходит обратно в точку IN.
* Пока ответ не забран хостом, новых пакетов OUT не принимаем — хост получит NAK. */
int usb_ep_out(uint8_t addr, const uint8_t *p, uint16_t n)
{
if (addr != EP_OUT || app.in_busy) return 0;
for (uint16_t i = 0; i < n; i++)
app.pkt[i] = p[i] ^ app.xor_key;
app.rx_bytes += n;
app.in_len = n;
app.in_busy = 1;
(void)drv_ep_tx(EP_IN, app.pkt, n); /* точка IN свободна: за этим следит in_busy */
return 0; /* приём откроет usb_ep_in_done */
}
void usb_ep_in_done(uint8_t addr)
{
if (addr != EP_IN || !app.in_busy) return;
app.tx_bytes += app.in_len;
app.in_busy = 0;
drv_ep_rx_arm(EP_OUT);
}
usb_f103.c — the STM32F103 USB FS driver at register level.
/* usb_f103.c — драйвер USB FS STM32F103 в режиме устройства, регистрами (RM0008, глава 23).
* Раздел 20д. Буферы точек лежат в памяти пакетов (512 байт): процессор видит её
* 16-битными словами через 32 бита, поэтому локальный адрес умножается на два.
* Точка n: передача с 0x40 + 0x80·n, приём с 0x80 + 0x80·n, по 64 байта; таблица — с 0. */
#include "vend.h"
#ifdef HOST_MODEL /* модель на компьютере: регистры — функции модели */
uint16_t usbm_read(uint32_t off);
void usbm_write(uint32_t off, uint16_t v);
extern uint8_t usbm_pma[1024];
#define USB_RD(off) usbm_read(off)
#define USB_WR(off, v) usbm_write((off), (uint16_t)(v))
#define PMA_BASE ((uintptr_t)usbm_pma)
#else
#define USB_RD(off) (*(volatile uint32_t *)(0x40005C00u + (off)))
#define USB_WR(off, v) (*(volatile uint32_t *)(0x40005C00u + (off)) = (uint16_t)(v))
#define PMA_BASE 0x40006000u
#endif
#define PMA16(a) (*(volatile uint16_t *)(PMA_BASE + 2u * (a)))
#define R_EPR(n) (4u * (n))
#define R_CNTR 0x40u
#define R_ISTR 0x44u
#define R_DADDR 0x4Cu
#define R_BTABLE 0x50u
#define CNTR_FRES 0x0001u
#define CNTR_RESETM 0x0400u
#define CNTR_CTRM 0x8000u
#define ISTR_EP_ID 0x000Fu
#define ISTR_RESET 0x0400u
#define ISTR_CTR 0x8000u
#define DADDR_EF 0x0080u
#define EP_CTR_RX 0x8000u /* сбрасывается записью 0 */
#define EP_DTOG_RX 0x4000u /* переключается записью 1 */
#define EP_STAT_RX 0x3000u /* переключается записью 1 */
#define EP_SETUP 0x0800u /* только чтение */
#define EP_TYPE 0x0600u
#define EP_KIND 0x0100u
#define EP_CTR_TX 0x0080u
#define EP_DTOG_TX 0x0040u
#define EP_STAT_TX 0x0030u
#define EP_EA 0x000Fu
#define EP_KEEP (EP_TYPE | EP_KIND | EP_EA)
#define RX_STALL 0x1000u
#define RX_NAK 0x2000u
#define RX_VALID 0x3000u
#define TX_STALL 0x0010u
#define TX_NAK 0x0020u
#define TX_VALID 0x0030u
#define BT_ADDR_TX(n) PMA16(8u * (n))
#define BT_COUNT_TX(n) PMA16(8u * (n) + 2u)
#define BT_ADDR_RX(n) PMA16(8u * (n) + 4u)
#define BT_COUNT_RX(n) PMA16(8u * (n) + 6u)
#define BUF_TX(n) (0x40u + 0x80u * (n))
#define BUF_RX(n) (0x80u + 0x80u * (n))
static uint8_t rxbuf[64];
static uint8_t addr_pending, dev_addr, ep0_stalled;
/* Запись в EPnR без чтения-изменения-записи: STAT и DTOG меняются записью 1,
* флаги CTR сбрасываются записью 0. «Ничего не менять» — 0 в переключатели
* и 1 во флаги CTR. Здесь в поля mask ставятся значения want. */
static void ep_set(uint32_t n, uint16_t mask, uint16_t want)
{
uint16_t r = (uint16_t)USB_RD(R_EPR(n));
USB_WR(R_EPR(n), (r & EP_KEEP) | EP_CTR_RX | EP_CTR_TX | ((r ^ want) & mask));
}
static void ep_clear_ctr(uint32_t n, uint16_t flag)
{
uint16_t r = (uint16_t)USB_RD(R_EPR(n));
USB_WR(R_EPR(n), ((r & EP_KEEP) | EP_CTR_RX | EP_CTR_TX) & ~flag);
}
/* Размер приёмного буфера в COUNTn_RX: до 62 байт — блоки по 2, дальше — по 32 (табл. 177) */
static uint16_t rx_count_field(uint16_t mps)
{
return mps > 62u ? (uint16_t)(0x8000u | (mps / 32u - 1u) << 10) : (uint16_t)((mps / 2u) << 10);
}
static void pma_write(uint16_t a, const uint8_t *p, uint16_t n)
{
for (uint16_t i = 0; i < n; i += 2) {
uint16_t w = p[i];
if (i + 1u < n) w |= (uint16_t)(p[i + 1] << 8);
PMA16(a + i) = w;
}
}
static void pma_read(uint16_t a, uint8_t *p, uint16_t n)
{
for (uint16_t i = 0; i < n; i += 2) {
uint16_t w = PMA16(a + i);
p[i] = (uint8_t)w;
if (i + 1u < n) p[i + 1] = (uint8_t)(w >> 8);
}
}
void drv_ep0_tx(const uint8_t *p, uint16_t n)
{
pma_write(BUF_TX(0), p, n);
BT_COUNT_TX(0) = n;
ep_set(0, EP_STAT_TX, TX_VALID);
}
void drv_ep0_stall(void)
{
ep_set(0, EP_STAT_TX | EP_STAT_RX, TX_STALL | RX_STALL); /* SETUP примется и так */
ep0_stalled = 1;
}
void drv_set_address(uint8_t addr)
{
dev_addr = addr; /* в DADDR — только после стадии состояния */
addr_pending = 1;
}
void drv_ep_open(uint8_t addr, uint8_t type, uint16_t mps)
{
static const uint16_t kind[4] = { 0x0200u, 0x0400u, 0x0000u, 0x0600u }; /* упр., изохр., масс., прер. */
uint32_t n = addr & 0x0Fu;
USB_WR(R_EPR(n), EP_CTR_RX | EP_CTR_TX | kind[type & 3u] | n); /* тип и номер */
if (addr & 0x80u) {
BT_ADDR_TX(n) = BUF_TX(n);
BT_COUNT_TX(n) = 0;
ep_set(n, EP_STAT_TX | EP_DTOG_TX, TX_NAK); /* первый пакет — DATA0 */
} else {
BT_ADDR_RX(n) = BUF_RX(n);
BT_COUNT_RX(n) = rx_count_field(mps);
ep_set(n, EP_STAT_RX | EP_DTOG_RX, RX_NAK);
}
}
void drv_ep_close(uint8_t addr)
{
uint32_t n = addr & 0x0Fu;
if (addr & 0x80u) ep_set(n, EP_STAT_TX, 0);
else ep_set(n, EP_STAT_RX, 0);
}
int drv_ep_tx(uint8_t addr, const uint8_t *p, uint16_t n)
{
uint32_t e = addr & 0x0Fu;
if ((USB_RD(R_EPR(e)) & EP_STAT_TX) == TX_VALID) return -1; /* прошлый пакет не забран */
pma_write((uint16_t)BUF_TX(e), p, n);
BT_COUNT_TX(e) = n;
ep_set(e, EP_STAT_TX, TX_VALID);
return 0;
}
void drv_ep_rx_arm(uint8_t addr)
{
ep_set(addr & 0x0Fu, EP_STAT_RX, RX_VALID);
}
void drv_ep_stall(uint8_t addr, int on)
{
uint32_t n = addr & 0x0Fu;
if (addr & 0x80u) ep_set(n, EP_STAT_TX | (on ? 0u : EP_DTOG_TX), on ? TX_STALL : TX_NAK);
else ep_set(n, EP_STAT_RX | (on ? 0u : EP_DTOG_RX), on ? RX_STALL : RX_NAK);
}
static void bus_reset(void)
{
USB_WR(R_BTABLE, 0);
BT_ADDR_TX(0) = BUF_TX(0);
BT_COUNT_TX(0) = 0;
BT_ADDR_RX(0) = BUF_RX(0);
BT_COUNT_RX(0) = rx_count_field(EP0_SIZE);
USB_WR(R_EPR(0), EP_CTR_RX | EP_CTR_TX | 0x0200u); /* точка 0, управляющая */
ep_set(0, EP_STAT_RX | EP_STAT_TX | EP_DTOG_RX | EP_DTOG_TX, RX_VALID | TX_NAK);
USB_WR(R_DADDR, DADDR_EF); /* адрес 0, функция включена */
addr_pending = ep0_stalled = 0;
usb_reset();
}
/* Включение: PDWN снять, подождать запуска приёмопередатчика (tSTARTUP до 1 мкс),
* снять FRES, сбросить ложные флаги. Такт USB и APB1 к этому моменту включены. */
void usb_f103_init(void)
{
USB_WR(R_CNTR, CNTR_FRES);
for (volatile uint32_t i = 0; i < 100u; i++) { }
USB_WR(R_CNTR, 0);
USB_WR(R_ISTR, 0);
USB_WR(R_CNTR, CNTR_CTRM | CNTR_RESETM);
}
void usb_f103_irq(void)
{
uint16_t istr;
while ((istr = (uint16_t)USB_RD(R_ISTR)) & (ISTR_CTR | ISTR_RESET)) {
if (istr & ISTR_RESET) {
USB_WR(R_ISTR, (uint16_t)~ISTR_RESET); /* флаги ISTR сбрасываются нулём */
bus_reset();
continue;
}
uint32_t n = istr & ISTR_EP_ID;
uint16_t r = (uint16_t)USB_RD(R_EPR(n));
if (r & EP_CTR_RX) {
uint16_t cnt = BT_COUNT_RX(n) & 0x3FFu;
if (cnt > sizeof rxbuf) cnt = sizeof rxbuf;
pma_read((uint16_t)BUF_RX(n), rxbuf, cnt);
ep_clear_ctr(n, EP_CTR_RX);
if (n == 0) {
if (r & EP_SETUP) {
ep0_stalled = 0;
usb_setup(rxbuf);
} else {
usb_ep0_out(rxbuf, cnt);
}
if (!ep0_stalled) ep_set(0, EP_STAT_RX, RX_VALID);
} else if (usb_ep_out((uint8_t)n, rxbuf, cnt)) {
ep_set(n, EP_STAT_RX, RX_VALID);
}
}
if (r & EP_CTR_TX) {
ep_clear_ctr(n, EP_CTR_TX);
if (n == 0) {
if (addr_pending) { /* стадия состояния SET_ADDRESS прошла */
addr_pending = 0;
USB_WR(R_DADDR, DADDR_EF | dev_addr);
}
usb_ep0_in_done();
} else {
usb_ep_in_done((uint8_t)(0x80u | n));
}
}
}
}
otg_f407.c — the STM32F407 OTG_FS driver at register level.
/* otg_f407.c — драйвер OTG_FS STM32F407 в режиме устройства, регистрами (RM0090, глава 34).
* Раздел 20д. Тот же драйвер, что для U585 в разделе 198ш, с тремя отличиями F407:
* адрес блока 0x50000000; VBUS выключается битом NOVBUSSENS (бит 21 GCCFG, а у U585
* этот бит — VBDEN с обратным смыслом); подмены сигнала сессии в GOTGCTL нет.
* Каждая передача IN — один пакет не больше 64 байт: он целиком входит в свой FIFO. */
#include "vend.h"
#define OTG_BASE 0x50000000u /* USB_OTG_FS_PERIPH_BASE */
#define REG(off) (*(volatile uint32_t *)(OTG_BASE + (off)))
#define R_GAHBCFG 0x008u
#define R_GUSBCFG 0x00Cu
#define R_GRSTCTL 0x010u
#define R_GINTSTS 0x014u
#define R_GINTMSK 0x018u
#define R_GRXSTSP 0x020u
#define R_GRXFSIZ 0x024u
#define R_DIEPTXF0 0x028u
#define R_GCCFG 0x038u
#define R_DIEPTXF(n) (0x104u + 4u * ((n) - 1u))
#define R_DCFG 0x800u
#define R_DCTL 0x804u
#define R_DIEPMSK 0x810u
#define R_DOEPMSK 0x814u
#define R_DAINT 0x818u
#define R_DAINTMSK 0x81Cu
#define R_DIEPCTL(n) (0x900u + 0x20u * (n))
#define R_DIEPINT(n) (0x908u + 0x20u * (n))
#define R_DIEPTSIZ(n) (0x910u + 0x20u * (n))
#define R_DOEPCTL(n) (0xB00u + 0x20u * (n))
#define R_DOEPINT(n) (0xB08u + 0x20u * (n))
#define R_DOEPTSIZ(n) (0xB10u + 0x20u * (n))
#define R_PCGCCTL 0xE00u
#define R_DFIFO(n) (0x1000u + 0x1000u * (n))
#define GAHBCFG_GINT (1u << 0)
#define GUSBCFG_PHYSEL (1u << 6)
#define GUSBCFG_TRDT_Pos 10
#define GUSBCFG_TRDT_Msk (0xFu << 10)
#define GUSBCFG_FDMOD (1u << 30)
#define GRSTCTL_CSRST (1u << 0)
#define GRSTCTL_RXFFLSH (1u << 4)
#define GRSTCTL_TXFFLSH (1u << 5)
#define GRSTCTL_TXFNUM_Pos 6
#define GRSTCTL_AHBIDL (1u << 31)
#define GINT_RXFLVL (1u << 4)
#define GINT_USBRST (1u << 12)
#define GINT_ENUMDNE (1u << 13)
#define GINT_IEPINT (1u << 18)
#define GINT_OEPINT (1u << 19)
#define GCCFG_PWRDWN (1u << 16)
#define GCCFG_VBUSASEN (1u << 18)
#define GCCFG_VBUSBSEN (1u << 19)
#define GCCFG_NOVBUSSENS (1u << 21)
#define DCFG_DSPD_FS 3u
#define DCFG_DAD_Pos 4
#define DCFG_DAD_Msk (0x7Fu << 4)
#define DCTL_SDIS (1u << 1)
#define DCTL_CGINAK (1u << 8)
#define EPCTL_USBAEP (1u << 15)
#define EPCTL_EPTYP_Pos 18
#define EPCTL_STALL (1u << 21)
#define EPCTL_TXFNUM_Pos 22
#define EPCTL_CNAK (1u << 26)
#define EPCTL_SNAK (1u << 27)
#define EPCTL_SD0PID (1u << 28)
#define EPCTL_EPDIS (1u << 30)
#define EPCTL_EPENA (1u << 31)
#define EPINT_XFRC (1u << 0)
#define EPINT_STUP (1u << 3)
#define TSIZ_PKTCNT_Pos 19
#define TSIZ_STUPCNT_Pos 29
#define RXSTS_EPNUM(s) ((s) & 0xFu)
#define RXSTS_BCNT(s) (((s) >> 4) & 0x7FFu)
#define RXSTS_PKTSTS(s) (((s) >> 17) & 0xFu)
#define PKTSTS_OUT_DATA 2u
#define PKTSTS_SETUP_DATA 6u
#define FIFO_RAM_WORDS 320u /* 1,25 КБ (RM0090, 34.2) */
#define FIFO_RX_WORDS 64u
#define FIFO_TX0_WORDS 16u /* точка 0: пакет 64 байта */
#define FIFO_TX1_WORDS 16u /* массовая IN: пакет 64 байта */
_Static_assert(FIFO_RX_WORDS + FIFO_TX0_WORDS + FIFO_TX1_WORDS <= FIFO_RAM_WORDS, "FIFO не помещаются");
_Static_assert(FIFO_RX_WORDS >= 10u + 1u + 2u * (64u / 4u + 1u) + 2u,
"RX FIFO меньше расчёта RM0090 34.13.1: SETUP, Global OUT NAK, два пакета, по слову на точку OUT");
static uint16_t out_mps[4];
static uint8_t setup_pkt[8];
static uint8_t rx_buf[2][64]; /* принятые пакеты: [0] — точка 0, [1] — массовая OUT */
static uint16_t rx_len[2];
static void fifo_setup(void)
{
REG(R_GRXFSIZ) = FIFO_RX_WORDS;
REG(R_DIEPTXF0) = FIFO_TX0_WORDS << 16 | FIFO_RX_WORDS; /* глубина | начало */
REG(R_DIEPTXF(1)) = FIFO_TX1_WORDS << 16 | (FIFO_RX_WORDS + FIFO_TX0_WORDS);
}
static void ep0_out_arm(void)
{
REG(R_DOEPTSIZ(0)) = 3u << TSIZ_STUPCNT_Pos | 1u << TSIZ_PKTCNT_Pos | EP0_SIZE;
REG(R_DOEPCTL(0)) |= EPCTL_EPENA | EPCTL_CNAK;
}
void drv_ep_open(uint8_t addr, uint8_t type, uint16_t mps)
{
uint8_t n = addr & 0x0Fu;
if (addr & 0x80u) { /* IN: свой FIFO с тем же номером, что у точки */
REG(R_DIEPCTL(n)) = mps | (uint32_t)type << EPCTL_EPTYP_Pos | (uint32_t)n << EPCTL_TXFNUM_Pos
| EPCTL_SD0PID | EPCTL_USBAEP | EPCTL_SNAK;
REG(R_DAINTMSK) |= 1u << n;
} else {
out_mps[n] = mps;
REG(R_DOEPCTL(n)) = mps | (uint32_t)type << EPCTL_EPTYP_Pos | EPCTL_SD0PID | EPCTL_USBAEP | EPCTL_SNAK;
REG(R_DAINTMSK) |= 1u << (16u + n);
}
}
void drv_ep_close(uint8_t addr)
{
uint8_t n = addr & 0x0Fu;
if (addr & 0x80u) {
REG(R_DAINTMSK) &= ~(1u << n);
REG(R_DIEPCTL(n)) = (REG(R_DIEPCTL(n)) & EPCTL_EPENA) ? EPCTL_EPDIS | EPCTL_SNAK : 0;
} else {
REG(R_DAINTMSK) &= ~(1u << (16u + n));
REG(R_DOEPCTL(n)) = (REG(R_DOEPCTL(n)) & EPCTL_EPENA) ? EPCTL_EPDIS | EPCTL_SNAK : 0;
}
}
void drv_ep_rx_arm(uint8_t addr)
{
uint8_t n = addr & 0x0Fu;
REG(R_DOEPTSIZ(n)) = 1u << TSIZ_PKTCNT_Pos | out_mps[n];
REG(R_DOEPCTL(n)) |= EPCTL_EPENA | EPCTL_CNAK;
}
int drv_ep_tx(uint8_t addr, const uint8_t *p, uint16_t len)
{
uint8_t n = addr & 0x0Fu;
if (REG(R_DIEPCTL(n)) & EPCTL_EPENA) return -1; /* прошлый пакет ещё не ушёл */
REG(R_DIEPTSIZ(n)) = 1u << TSIZ_PKTCNT_Pos | len;
REG(R_DIEPCTL(n)) |= EPCTL_EPENA | EPCTL_CNAK;
for (uint16_t i = 0; i < len; i += 4) {
uint32_t w = p[i];
if (i + 1u < len) w |= (uint32_t)p[i + 1] << 8;
if (i + 2u < len) w |= (uint32_t)p[i + 2] << 16;
if (i + 3u < len) w |= (uint32_t)p[i + 3] << 24;
REG(R_DFIFO(n)) = w;
}
return 0;
}
void drv_ep0_tx(const uint8_t *p, uint16_t len) { (void)drv_ep_tx(0x80, p, len); }
void drv_ep0_stall(void)
{
REG(R_DIEPCTL(0)) |= EPCTL_STALL; /* ядро само снимет остановку со следующим SETUP */
REG(R_DOEPCTL(0)) |= EPCTL_STALL;
ep0_out_arm();
}
void drv_ep_stall(uint8_t addr, int on)
{
uint32_t off = (addr & 0x80u) ? R_DIEPCTL(addr & 0x0Fu) : R_DOEPCTL(addr & 0x0Fu);
if (on) REG(off) |= EPCTL_STALL;
else REG(off) = (REG(off) & ~EPCTL_STALL) | EPCTL_SD0PID; /* после снятия — снова DATA0 */
}
void drv_set_address(uint8_t addr) /* у OTG адрес пишут сразу, до стадии состояния (RM0090) */
{
REG(R_DCFG) = (REG(R_DCFG) & ~DCFG_DAD_Msk) | (uint32_t)addr << DCFG_DAD_Pos;
}
static void flush_fifos(void)
{
REG(R_GRSTCTL) = GRSTCTL_TXFFLSH | 0x10u << GRSTCTL_TXFNUM_Pos; /* 0x10 — все передающие */
while (REG(R_GRSTCTL) & GRSTCTL_TXFFLSH) { }
REG(R_GRSTCTL) = GRSTCTL_RXFFLSH;
while (REG(R_GRSTCTL) & GRSTCTL_RXFFLSH) { }
}
/* Ядро OTG_FS: такт AHB2 и выводы PA11/PA12 к этому моменту включены (main_f407.c) */
void otg_f407_init(void)
{
REG(R_GAHBCFG) &= ~GAHBCFG_GINT;
REG(R_GUSBCFG) |= GUSBCFG_PHYSEL; /* встроенный приёмопередатчик FS */
while (!(REG(R_GRSTCTL) & GRSTCTL_AHBIDL)) { }
REG(R_GRSTCTL) |= GRSTCTL_CSRST; /* сброс ядра */
while (REG(R_GRSTCTL) & GRSTCTL_CSRST) { }
REG(R_GCCFG) = GCCFG_PWRDWN | GCCFG_NOVBUSSENS; /* приёмопередатчик включён, VBUS не следим */
REG(R_GUSBCFG) = (REG(R_GUSBCFG) & ~GUSBCFG_TRDT_Msk) | GUSBCFG_FDMOD | 6u << GUSBCFG_TRDT_Pos;
REG(R_DCTL) |= DCTL_SDIS; /* пока не готовы — подтяжка D+ снята */
REG(R_PCGCCTL) = 0;
REG(R_DCFG) |= DCFG_DSPD_FS;
fifo_setup();
flush_fifos();
REG(R_DIEPMSK) = EPINT_XFRC;
REG(R_DOEPMSK) = EPINT_XFRC | EPINT_STUP;
REG(R_DAINTMSK) = 0;
REG(R_GINTSTS) = 0xBFFFFFFFu;
REG(R_GINTMSK) = GINT_USBRST | GINT_ENUMDNE | GINT_RXFLVL | GINT_IEPINT | GINT_OEPINT;
REG(R_GAHBCFG) |= GAHBCFG_GINT;
REG(R_DCTL) &= ~DCTL_SDIS; /* подтяжка D+: хост видит устройство */
}
/* Пакет из приёмного FIFO: слово состояния, за ним данные словами */
static void rx_pop(void)
{
uint32_t s = REG(R_GRXSTSP);
uint32_t n = RXSTS_EPNUM(s), cnt = RXSTS_BCNT(s), sts = RXSTS_PKTSTS(s);
if (sts != PKTSTS_OUT_DATA && sts != PKTSTS_SETUP_DATA) return; /* «передача окончена» — без данных */
uint8_t *dst = sts == PKTSTS_SETUP_DATA ? setup_pkt : rx_buf[n ? 1 : 0];
for (uint32_t i = 0; i < cnt; i += 4) {
uint32_t w = REG(R_DFIFO(0));
for (uint32_t k = 0; k < 4 && i + k < cnt; k++)
if (i + k < 64u) dst[i + k] = (uint8_t)(w >> (8 * k));
}
if (sts == PKTSTS_OUT_DATA) rx_len[n ? 1 : 0] = (uint16_t)cnt;
}
void otg_f407_irq(void)
{
uint32_t st = REG(R_GINTSTS) & REG(R_GINTMSK);
if (st & GINT_USBRST) {
REG(R_GINTSTS) = GINT_USBRST;
REG(R_DCFG) &= ~DCFG_DAD_Msk;
REG(R_DAINTMSK) = 1u | 1u << 16; /* точка 0 в обе стороны */
usb_reset();
ep0_out_arm();
}
if (st & GINT_ENUMDNE) {
REG(R_GINTSTS) = GINT_ENUMDNE;
REG(R_DIEPCTL(0)) &= ~0x3u; /* MPSIZ точки 0: 00 — 64 байта */
REG(R_DCTL) |= DCTL_CGINAK;
}
while (REG(R_GINTSTS) & GINT_RXFLVL) rx_pop();
if (st & GINT_OEPINT) {
uint32_t eps = REG(R_DAINT) >> 16;
for (uint8_t n = 0; n < 4; n++) {
if (!(eps & 1u << n)) continue;
uint32_t f = REG(R_DOEPINT(n));
REG(R_DOEPINT(n)) = f;
if (f & EPINT_STUP) {
usb_setup(setup_pkt);
ep0_out_arm();
} else if (f & EPINT_XFRC) {
if (n == 0) {
usb_ep0_out(rx_buf[0], rx_len[0]);
rx_len[0] = 0;
ep0_out_arm();
} else if (usb_ep_out(n, rx_buf[1], rx_len[1])) {
drv_ep_rx_arm(n);
}
}
}
}
if (st & GINT_IEPINT) {
uint32_t eps = REG(R_DAINT) & 0xFFFFu;
for (uint8_t n = 0; n < 4; n++) {
if (!(eps & 1u << n)) continue;
uint32_t f = REG(R_DIEPINT(n));
REG(R_DIEPINT(n)) = f;
if (!(f & EPINT_XFRC)) continue;
if (n == 0) usb_ep0_in_done();
else usb_ep_in_done(0x80u | n);
}
}
}
vend_pc.py — the PC program in pyusb style.
# -*- coding: utf-8 -*-
"""Программа на ПК для устройства раздела 20д. Обращается к устройству тремя
вызовами в духе pyusb: ctrl_transfer, write, read. На настоящем устройстве dev даёт
open_device() (нужны pyusb и libusb); в проверке — модель хоста (host_model.py)."""
VID, PID = 0x1209, 0x0002
EP_OUT, EP_IN = 0x01, 0x81
VREQ_GET_INFO, VREQ_SET_LED, VREQ_SET_XOR = 0x01, 0x02, 0x03
REQ_VENDOR_IN, REQ_VENDOR_OUT = 0xC0, 0x40 # производитель, устройству
def open_device():
import usb.core # pyusb
dev = usb.core.find(idVendor=VID, idProduct=PID)
if dev is None:
raise SystemExit("устройство %04X:%04X не найдено" % (VID, PID))
dev.set_configuration()
return dev
def get_info(dev):
b = bytes(dev.ctrl_transfer(REQ_VENDOR_IN, VREQ_GET_INFO, 0, 0, 12))
return {"version": "%d.%02d" % (b[1], b[0]), "led": b[2], "xor": b[3],
"rx": int.from_bytes(b[4:8], "little"), "tx": int.from_bytes(b[8:12], "little")}
def set_led(dev, on):
dev.ctrl_transfer(REQ_VENDOR_OUT, VREQ_SET_LED, 1 if on else 0, 0)
def set_xor(dev, key):
dev.ctrl_transfer(REQ_VENDOR_OUT, VREQ_SET_XOR, 0, 0, bytes([key]))
def echo(dev, data):
"""Отправить данные пакетами по 64 байта; на каждый пакет устройство отвечает своим."""
back = b""
for i in range(0, len(data), 64):
chunk = data[i:i + 64]
dev.write(EP_OUT, chunk)
back += bytes(dev.read(EP_IN, 64))
return back
def main(dev):
print("состояние:", get_info(dev))
set_led(dev, True)
set_xor(dev, 0x5A)
data = bytes(range(200))
back = echo(dev, data)
ok = back == bytes(b ^ 0x5A for b in data)
print("эхо: отправлено %d, принято %d, совпало с ожидаемым: %s" % (len(data), len(back), "да" if ok else "нет"))
print("состояние:", get_info(dev))
return ok
if __name__ == "__main__":
main(open_device())
Next: section 21 "SWO and ITM" — debug output that takes neither a UART nor a single pin beyond those already used by the debugger.