20в. USB HID in practice: the report descriptor and a composite device
Section 20 called HID a class that needs no drivers, section 20а showed a host that reads a boot keyboard. Here — the device side. A HID device sends the host short reports, and their format is described by the report descriptor: a small program made of bytes, which the host parses on connection. We will work through this language, read the mouse from the ST library byte by byte with our own parser, build a boot keyboard and a composite device — a keyboard, a mouse and volume keys on one interface — and write the code that sends reports.
Sources: the ST USB Device library from the STM32Cube_FW_F1 V1.8.7 and
STM32Cube_FW_F4 V1.28.0 packages (the HID and Custom HID classes, the
CustomHID_Standalone example for the STM3210E_EVAL board), the ST USB Host library
(usbh_hid_keybd.c, usbh_hid.c), MDK-Middleware 8.1.0 (usb_hid.h,
the USBD_User_HID_Mouse.c template, USBD_Config_HID.h), the Zephyr tree
4.4.2 (include/zephyr/usb/class/hid.h, usb_ch9.h) and hidusage.h from
the Windows SDK 10.0.19041 — of these sources only it has the volume key
codes. The HID 1.11 specification and the HUT usage tables are not quoted:
all codes are taken from these headers.
What it is called in the documentation
Original Meaning report, input / output / feature report a report: input (device → host), output (host → device), feature report report descriptor the description of all reports of the device item: main, global, local a descriptor item: main, global, local Usage Page, Usage a usage page; a usage — what a field means Report Size, Report Count field size in bits; number of fields Report ID the report number: the first byte when there are several reports collection (application, physical) a collection: application, physical boot protocol, report protocol boot protocol (fixed format), report protocol (per the descriptor) modifier, keycode a modifier key (Ctrl, Shift…); a key code consumer control consumer keys: volume, pause bInterval the period at which the host polls an interrupt endpoint
20в.1 A report and its description
A HID device sends data in reports through an interrupt IN endpoint:
a mouse — movement and buttons, a keyboard — modifiers and codes of pressed
keys. The host polls the endpoint once every bInterval. Output reports
(the Caps Lock LEDs) go back — through an OUT endpoint or with a
SET_REPORT request on endpoint zero; the ST Custom HID class accepts both paths.
What is in a report the device describes with the report descriptor. The host
reads it with a GET_DESCRIPTOR request of type 0x22 (HID_REPORT_DESC in
usbd_hid.h, USB_DESC_HID_REPORT in Zephyr's hid.h), and learns its length
in advance from the HID descriptor (type 0x21) inside the configuration
descriptor — the wDescriptorLength field (wItemLength in the ST comments). The ST class returns
MIN(USBD_CUSTOM_HID_REPORT_DESC_SIZE, req->wLength) bytes: it takes the length
from the macro, not from the array.
If there are several reports, each has its own number — the Report ID, and the number
goes as the first byte. That is what the ST example CustomHID_Standalone does:
the button report is SendBuffer[0] = KEY_REPORT_ID, SendBuffer[1] is
the state, USBD_CUSTOM_HID_SendReport(…, SendBuffer, 2); a received
output report goes to the handler as a "number, byte" pair
(OutEvent(Report_buf[0], Report_buf[1])).
20в.2 The descriptor language: items and their codes
A descriptor is a sequence of items. The first byte of an item is the
prefix: the tag in the upper four bits, the type in two, the data size in the two
low bits. That is how Zephyr's HID_ITEM(bTag, bType, bSize) macro in
hid.h builds it. The size code 0, 1, 2, 3 means 0, 1, 2 and 4 data bytes
(HID_LOGICAL_MIN32 sets code 3 and writes four bytes). Multi-byte
data goes low byte first: HID_USAGE_PAGE16(page) in Zephyr
writes (uint8_t)page first, HID_LogicalMaxS(x) in MDK — x & 0xFFU.
| Item | Type | Tag | Code with 1 data byte |
|---|---|---|---|
| Input | main (0) | 0x8 | 0x81 |
| Output | main (0) | 0x9 | 0x91 |
| Feature | main (0) | 0xB | 0xB1 |
| Collection | main (0) | 0xA | 0xA1 |
| Usage Page | global (1) | 0x0 | 0x05 |
| Logical Minimum | global (1) | 0x1 | 0x15 |
| Logical Maximum | global (1) | 0x2 | 0x25 |
| Report Size | global (1) | 0x7 | 0x75 |
| Report ID | global (1) | 0x8 | 0x85 |
| Report Count | global (1) | 0x9 | 0x95 |
| Usage | local (2) | 0x0 | 0x09 |
| Usage Minimum | local (2) | 0x1 | 0x19 |
| Usage Maximum | local (2) | 0x2 | 0x29 |
End Collection is 0xC0, with no data. The same codes are produced by the MDK macros
(HID_Input(x) — 0x81U,x, HID_ReportID(x) — 0x85U,x) and by the tag
table in usbh_hid.h of the ST USB Host library.
It works as a small language with state:
- global items (Usage Page, Logical Minimum/Maximum, Report Size, Report Count, Report ID) set the state and hold until they are overwritten;
- local items (Usage, Usage Minimum/Maximum) say what the fields of the nearest main item mean;
- a main Input, Output or Feature item adds to the report of its type
and number Report Count fields of Report Size bits. Its data byte is
flags: bit 0 — constant (padding), bit 1 — variable (each field
on its own) or array (the fields are a list of codes), bit 2 —
relative value (
HID_Constant,HID_Variable,HID_Relativein MDK'susb_hid.h); - Collection and End Collection group items: an application collection (Application, 0x01) is one device for the host, a physical one (Physical, 0x00) is one point of measurement.
The report size is the sum of Report Size × Report Count of all main items of one type and number, divided by 8, plus one byte for the number if there are numbers.
20в.3 The mouse from the ST library byte by byte
The model _verify/hid20c/hid20c.py is our own parser following the rules of 20в.2.
It takes the HID_MOUSE_ReportDesc array straight from usbd_hid.c (in the
F1 V1.8.7 and F4 V1.28.0 packages the bytes are identical) and prints a tree:
05 01 Usage Page (Generic Desktop)
09 02 Usage (Mouse)
A1 01 Collection (Application)
09 01 Usage (Pointer)
A1 00 Collection (Physical)
05 09 Usage Page (Button)
19 01 Usage Minimum (0x01)
29 03 Usage Maximum (0x03)
15 00 Logical Minimum (0)
25 01 Logical Maximum (1)
95 03 Report Count (3)
75 01 Report Size (1)
81 02 Input (Data,Var,Abs; 3 × 1 бит, биты 0…2)
95 01 Report Count (1)
75 05 Report Size (5)
81 01 Input (Const,Array,Abs; 1 × 5 бит, биты 3…7)
05 01 Usage Page (Generic Desktop)
09 30 Usage (X)
09 31 Usage (Y)
09 38 Usage (Wheel)
15 81 Logical Minimum (-127)
25 7F Logical Maximum (127)
75 08 Report Size (8)
95 03 Report Count (3)
81 06 Input (Data,Var,Rel; 3 × 8 бит, биты 8…31)
C0 End Collection
09 3C Usage (Motion Wakeup)
05 FF Usage Page (0xFF)
09 01 Usage (0x01)
15 00 Logical Minimum (0)
25 01 Logical Maximum (1)
75 01 Report Size (1)
95 02 Report Count (2)
B1 22 Feature (Data,Var,Abs; 2 × 1 бит, биты 0…1)
75 06 Report Size (6)
95 01 Report Count (1)
B1 01 Feature (Const,Array,Abs; 1 × 6 бит, биты 2…7)
C0 End Collection
The result of the parse: 74 bytes, 38 items, 2 collections. The input report is
32 bits, 4 bytes: a button byte (3 button bits and 5 padding bits), then X,
Y and the wheel as signed bytes, from −127 to 127, relative. The feature
report is 1 byte. A check against what the library declared:
HID_MOUSE_REPORT_DESC_SIZE = 74 — the length matches, HID_EPIN_SIZE =
4 — the input report size matches.
The last 22 bytes are a feature report with the Motion Wakeup usage (0x3C,
HID_USAGE_GENERIC_MOTION_WAKEUP in MDK). The MDK mouse template
USBD_User_HID_Mouse.c does not have them: the parser got 52 bytes — exactly
as many as stated in the template comment — and the same 4 bytes of
input. Zephyr's HID_MOUSE_REPORT_DESC(3) macro gives the same 52 bytes
from the same items, only Report Size and Report Count are in a different
order: for global items the order does not matter, what matters is that they are
set before the main item.
20в.4 The keyboard: the boot protocol and key codes
The boot keyboard report is fixed in advance so that a host that does not read
the descriptor can understand it. That is how the ST USB Host library works:
it requests the report descriptor but does not parse it, sets the
boot protocol (USBH_HID_SetProtocol(phost, 0U)) and reads
8 bytes at fixed offsets: USBH_HID_KEYBD_REPORT_SIZE = 8,
the modifiers are the bits of byte 0, the codes are six bytes from byte 2, from 0 to 101.
A device whose bInterfaceProtocol is neither 1 (keyboard) nor 2
(mouse) it does not accept: "Protocol not supported".
The descriptor of such a report is Zephyr's HID_KEYBOARD_REPORT_DESC() macro.
The parser: 63 bytes, 32 items, an input report of 64 bits — 8 bytes,
an output report of 1 byte. The input layout:
| Byte | 0 | 1 | 2…7 |
|---|---|---|---|
| what | modifiers, 8 bits | reserved, constant | up to six codes of pressed keys, array |
The modifier bits (enum hid_kbd_modifier in hid.h): left Ctrl —
0x01, left Shift — 0x02, left Alt — 0x04, left GUI — 0x08, the right ones —
0x10, 0x20, 0x40, 0x80. The output byte is the LEDs: Num Lock 0x01, Caps
Lock 0x02, Scroll Lock 0x04. Key codes (enum hid_kbd_code; the same in
ST's usbh_hid_keybd.h and MDK's usb_hid.h): A — 0x04, …, Z — 0x1D, 1 —
0x1E, …, 0 — 0x27, Enter — 0x28, space — 0x2C. A capital letter is
the same code with Shift: "H" is 02 00 0B 00 00 00 00 00.
A press is a report with the code, a release is a report without it. The model
turns "Hello, STM32!" (13 characters) into 26 reports, and with the
HID_KEYBRD_Key and HID_KEYBRD_ShiftKey tables of the ST USB Host library
(the QWERTY layout) turns them back — and gets the same text. The host in the
model counts as a character a code that was not in the previous report; that is why
two "l" in a row need a release between them.
The typing speed is set by polling: one report per poll, two reports per character. With polling once every 1 ms the string goes out in 26 ms — 500 characters per second, with 10 ms — in 260 ms, 50 characters per second.
20в.5 A composite device: report numbers and consumer keys
The composite device from section 20 is several interfaces with different classes. For HID there is a simpler path: one interface, one IN endpoint and several application collections, each with its own Report ID. The host sees a keyboard, a mouse and a volume remote at once, and in the ST library the Custom HID class is enough — it takes the report descriptor from the program.
Our own descriptor in _verify/hid20c/hid20c.c has three collections:
a keyboard (number 1: an 8-byte input as in the boot one, the output is the LEDs),
a mouse (number 2) and consumer keys (number 3). The last block:
0x05, 0x0C, /* Usage Page (Consumer) */
0x09, 0x01, /* Usage (Consumer Control) */
0xA1, 0x01, /* Collection (Application) */
0x85, RID_CONSUMER, /* Report ID (3) */
0x09, 0xE9, 0x09, 0xEA, /* Usage: Volume Increment, Decrement */
0x09, 0xE2, 0x09, 0xCD, /* Usage: Mute, Play/Pause */
0x15, 0x00, 0x25, 0x01, /* Logical Min/Max: 0 … 1 */
0x75, 0x01, 0x95, 0x04, /* Report Size 1, Report Count 4 */
0x81, 0x02, /* Input (Data, Var, Abs): четыре клавиши */
0x95, 0x04, /* Report Count 4 */
0x81, 0x01, /* Input (Const): добивка */
0xC0, /* End Collection */
The Consumer page is 0x0C (HID_USAGE_PAGE_CONSUMER in MDK and in
hidusage.h), the usages are from hidusage.h: volume up 0xE9, volume down 0xEA,
mute 0xE2, pause 0xCD. Four bits — four keys; for the padding Report Size
is not repeated: the 1 from the line above applies.
The parser on the bytes from the object file (the data section that
armclang put into hid20c.o): 150 bytes, 77 items, 4 collections
(three application ones and a physical one in the mouse), no errors. The reports:
| Number | Kind | Data bits | Bytes with the number |
|---|---|---|---|
| 1 | input, keyboard | 64 | 9 |
| 1 | output, LEDs | 8 | 2 |
| 2 | input, mouse | 32 | 5 |
| 3 | input, consumer | 8 | 2 |
Hence the class setup: USBD_CUSTOM_HID_REPORT_DESC_SIZE = 150,
the IN endpoint at least 9 bytes, the output report buffer
USBD_CUSTOMHID_OUTREPORT_BUF_SIZE = 2 — exactly the number and the LED
byte, which is also the default value.
The ST example CustomHID_Standalone uses the same technique: 163 bytes, seven
numbers, each report is 2 bytes with the number, hence
CUSTOM_HID_EPIN_SIZE and CUSTOM_HID_EPOUT_SIZE of 2 each. The parser
also shows what the example relies on: in the block for number 5 Report
Count is not set — the 1 from the block for number 4 applies.
Such a device does not declare itself as a boot device: Custom HID sets subclass 0 and protocol 0. A host that needs the boot protocol — the ST USB Host library or, probably, the BIOS setup — will not see the keyboard.
20в.6 Code: a report queue, bInterval and latency
The file _verify/hid20c/hid20c.c is built by armclang for the STM32F103
(the library from the F1 V1.8.7 package) and the STM32F407 (F4 V1.28.0) without
warnings. The reports and the compile-time size checks:
struct kbd_report { uint8_t id, mod, reserved, keys[6]; };
struct mouse_report { uint8_t id, buttons; int8_t x, y, wheel; };
struct consumer_report { uint8_t id, bits; };
enum { CC_VOL_UP = 1u << 0, CC_VOL_DOWN = 1u << 1, CC_MUTE = 1u << 2, CC_PLAY_PAUSE = 1u << 3 };
_Static_assert(sizeof(hid_report_desc) == USBD_CUSTOM_HID_REPORT_DESC_SIZE,
"USBD_CUSTOM_HID_REPORT_DESC_SIZE не равен длине дескриптора");
_Static_assert(sizeof(struct kbd_report) == 9u && sizeof(struct mouse_report) == 5u
&& sizeof(struct consumer_report) == 2u, "размеры отчётов: 9, 5 и 2 байта с номером");
_Static_assert(CUSTOM_HID_EPIN_SIZE >= sizeof(struct kbd_report),
"точка IN меньше отчёта клавиатуры: поправить CUSTOM_HID_EPIN_SIZE");
_Static_assert(USBD_CUSTOMHID_OUTREPORT_BUF_SIZE >= 2u, "отчёт светодиодов: номер + байт");
The third check catches a difference between the packages. In F4 V1.28.0 the endpoint size
CUSTOM_HID_EPIN_SIZE is wrapped in #ifndef, and it is set in
usbd_conf.h. In F1 V1.8.7 it is a plain #define CUSTOM_HID_EPIN_SIZE
0x02U, and without editing the class header the build stops on this
line ("2 >= 9"). This is intended: otherwise the endpoint would stay at 2 bytes, and the
keyboard report would not fit into it.
Sending is a queue from which the main loop takes one report at a time when the endpoint is free:
void hid_kbd_tap(uint8_t mod, uint8_t key)
{
struct kbd_report r = { RID_KBD, mod, 0, { key } };
if (!q_room(2u)) /* нажатие без отпускания — залипшая клавиша */
return;
q_put(&r, sizeof r);
memset(&r.mod, 0, sizeof r - 1u); /* отпускание: всё, кроме номера, — нули */
q_put(&r, sizeof r);
}
void hid_task(void)
{
static uint8_t tx[2][sizeof(struct kbd_report)]; /* один в передаче, в другой пишем */
static uint32_t cur;
const struct hid_slot *s;
uint8_t st;
if (hUsbDeviceFS.dev_state != USBD_STATE_CONFIGURED || q_tail == q_head)
return; /* до SET_CONFIGURATION SendReport молча не шлёт */
s = &q[q_tail % Q_LEN];
memcpy(tx[cur], s->data, s->len);
NVIC_DisableIRQ(USB_IRQ);
st = USBD_CUSTOM_HID_SendReport(&hUsbDeviceFS, tx[cur], s->len);
NVIC_EnableIRQ(USB_IRQ);
if (st == (uint8_t)USBD_OK) { /* USBD_BUSY: прошлый ещё не забран — позже */
cur ^= 1u;
q_tail++;
}
}
Why it is done this way. USBD_CUSTOM_HID_SendReport does not copy the report but passes
a pointer — the buffer must not be touched until the host has taken it; that is why
there are two buffers. While the endpoint is busy, the function returns USBD_BUSY, and the report
stays in the queue. Before the device is configured it sends nothing but
returns USBD_OK — hence the dev_state check. The USB interrupt is
disabled for the duration of the call, as in 20.7: the library is not called from two places
at once. A queue of 16 reports of 10 bytes takes 160 bytes of RAM.
The output report is received by hid_out_event. For the F103 armclang with -O2
turns it into 7 instructions — a comparison of the number and a byte write:
CMP r0,#1
ITTT EQ
MOVWEQ r0,#:LOWER16: kbd_leds
MOVTEQ r0,#:UPPER16: kbd_leds
STRBEQ r1,[r0,#0]
MOVS r0,#0
BX lr
For the F407 it also contains a call to USBD_CUSTOM_HID_ReceivePacket:
the class from F4 V1.28.0 does not re-arm the OUT endpoint by itself after reception, the
program does it (as in the ST example too). The class from F1 V1.8.7 re-arms
it by itself.
Polling. bInterval in the endpoint descriptor is the period at which the host
polls it. At full speed it is in milliseconds, 1…255
(USB_FS_INT_EP_INTERVAL in Zephyr, a hint in MDK's USBD_Config_HID.h).
At high speed it is 2^(bInterval−1) × 125 µs: the ST HID class has
HID_HS_BINTERVAL = 7, that is 64 × 125 µs = 8 ms. An event waits for
the nearest poll — on average half a period, in the worst case the whole period:
| Setting | bInterval | average wait | worst case |
|---|---|---|---|
ST HID class, HID_FS_BINTERVAL |
10 ms | 5.0 ms | 10 ms |
ST Custom HID class, CUSTOM_HID_FS_BINTERVAL |
5 ms | 2.5 ms | 5 ms |
hid20c, usbd_conf.h |
1 ms | 0.5 ms | 1 ms |
The host may also poll less often than declared: the ST USB Host library does not
poll more often than HID_MIN_POLL = 10 ms, and bInterval = 1 becomes
10 ms in it.
20в.7 Pitfalls
A key got stuck, double letters disappear. The ST HID class: while the endpoint
is busy, USBD_HID_SendReport silently drops the report and returns
USBD_OK. Code that sends a press and immediately a release loses the
release. Model: a character every 20 ms, polling 10 ms — of 26 reports
13 got through, "Hello, STM32!" turned into "Helo, STM32!", and the last
key stayed pressed. You need a queue, as in 20в.6.
The keyboard report does not get through, the mouse works. The IN endpoint is smaller than the
report. In F1 V1.8.7 CUSTOM_HID_EPIN_SIZE = 2 and without #ifndef — it is
edited in the class header.
The host sees garbage or does not see the device.
USBD_CUSTOM_HID_REPORT_DESC_SIZE does not equal the array length: the class returns
as many bytes as the macro says — a truncated descriptor or a tail of
someone else's memory. A _Static_assert saves you.
The report is one byte longer than the host expects. The descriptor has a Report ID, but the number was not put as the first byte — or the other way round. If the number is in the descriptor, it is in every report.
The bit total is not divisible by 8. The padding was forgotten: after three button bits you need a 5-bit Const field, after five LEDs — a 3-bit one.
Caps Lock on the F4 lit up once. In the Custom HID class from F4
V1.28.0 the class does not re-arm the OUT endpoint by itself: the output report
handler in the program does it with a call to USBD_CUSTOM_HID_ReceivePacket; without it the next report will not arrive.
The vendor page is written backwards. In the ST example
CustomHID_Standalone the bytes 06 FF 00 are labelled "Vendor Page:
0xFF00", but data goes low byte first — in the same place 26 FF 00
means 255 for them. The result is page 0x00FF, while vendor pages per
hidusage.h are 0xFF00…0xFFFF; HID_UsagePageVendor(0x00) from MDK writes
06 00 FF.
The keyboard does not work on a simple host. A composite device with report numbers is not a boot device: the ST USB Host library and the like will not accept it. If you need a keyboard that works "everywhere" — a separate interface with subclass 1 and protocol 1 and an 8-byte report without a number.
20в.8 Summary
- Item prefix: tag << 4 | type << 2 | size; Usage Page 0x05, Usage 0x09, Report Size 0x75, Report Count 0x95, Report ID 0x85, Input 0x81, Output 0x91, Collection 0xA1, End Collection 0xC0.
- ST mouse: 74 bytes, input 4 bytes (
HID_EPIN_SIZE= 4), feature 1 byte; the MDK and Zephyr mouse — 52 bytes and the same 4 bytes of input. - Boot keyboard: 8 bytes — modifiers, reserved, six codes; A — 0x04, Enter — 0x28, Shift — 0x02; output — 1 byte of LEDs.
- Composite on one interface: a 150-byte descriptor, reports of 9, 2, 5 and 2 bytes with the number; consumer keys 0xE9, 0xEA, 0xE2, 0xCD on page 0x0C.
- Polling: ST HID — 10 ms, Custom HID — 5 ms, our own — 1 ms; the average wait is half a period; 13 characters at 1 ms — 26 ms, at 10 ms — 260 ms.
Full sources
hid20c.c — a composite HID device: keyboard, mouse and consumer keys.
/* hid20c.c — раздел 20в: составное HID-устройство на одном интерфейсе.
* Клавиатура (отчёт 1), мышь (отчёт 2) и потребительские клавиши (отчёт 3)
* поверх класса Custom HID библиотеки USB Device ST.
* Сборка: armclang, STM32F103 (STM32Cube_FW_F1 V1.8.7) и STM32F407 (STM32Cube_FW_F4 V1.28.0).
* На железе не запускался. */
#include <string.h>
#include "usbd_customhid.h"
#if defined(STM32F103xB)
#define USB_IRQ USB_LP_CAN1_RX0_IRQn
#elif defined(STM32F407xx)
#define USB_IRQ OTG_FS_IRQn
#endif
enum { RID_KBD = 1, RID_MOUSE = 2, RID_CONSUMER = 3 };
__ALIGN_BEGIN static uint8_t hid_report_desc[] __ALIGN_END = {
0x05, 0x01, /* Usage Page (Generic Desktop) */
0x09, 0x06, /* Usage (Keyboard) */
0xA1, 0x01, /* Collection (Application) */
0x85, RID_KBD, /* Report ID (1) */
0x05, 0x07, /* Usage Page (Keyboard) */
0x19, 0xE0, 0x29, 0xE7, /* Usage Min/Max: Left Control … Right GUI */
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): модификаторы */
0x75, 0x08, 0x95, 0x01, /* Report Size 8, Report Count 1 */
0x81, 0x01, /* Input (Const): резервный байт */
0x05, 0x08, /* Usage Page (LEDs) */
0x19, 0x01, 0x29, 0x05, /* Usage Min/Max: Num Lock … Kana */
0x75, 0x01, 0x95, 0x05, /* Report Size 1, Report Count 5 */
0x91, 0x02, /* Output (Data, Var, Abs): светодиоды */
0x75, 0x03, 0x95, 0x01, /* Report Size 3, Report Count 1 */
0x91, 0x01, /* Output (Const): добивка до байта */
0x05, 0x07, /* Usage Page (Keyboard) */
0x19, 0x00, 0x29, 0x65, /* Usage Min/Max: коды 0 … 101 */
0x15, 0x00, 0x25, 0x65, /* Logical Min/Max: 0 … 101 */
0x75, 0x08, 0x95, 0x06, /* Report Size 8, Report Count 6 */
0x81, 0x00, /* Input (Data, Array): нажатые клавиши */
0xC0, /* End Collection */
0x05, 0x01, /* Usage Page (Generic Desktop) */
0x09, 0x02, /* Usage (Mouse) */
0xA1, 0x01, /* Collection (Application) */
0x85, RID_MOUSE, /* Report ID (2) */
0x09, 0x01, /* Usage (Pointer) */
0xA1, 0x00, /* Collection (Physical) */
0x05, 0x09, /* Usage Page (Button) */
0x19, 0x01, 0x29, 0x03, /* Usage Min/Max: кнопки 1 … 3 */
0x15, 0x00, 0x25, 0x01, /* Logical Min/Max: 0 … 1 */
0x75, 0x01, 0x95, 0x03, /* Report Size 1, Report Count 3 */
0x81, 0x02, /* Input (Data, Var, Abs): кнопки */
0x75, 0x05, 0x95, 0x01, /* Report Size 5, Report Count 1 */
0x81, 0x01, /* Input (Const): добивка */
0x05, 0x01, /* Usage Page (Generic Desktop) */
0x09, 0x30, 0x09, 0x31, /* Usage (X), Usage (Y) */
0x09, 0x38, /* Usage (Wheel) */
0x15, 0x81, 0x25, 0x7F, /* Logical Min/Max: −127 … 127 */
0x75, 0x08, 0x95, 0x03, /* Report Size 8, Report Count 3 */
0x81, 0x06, /* Input (Data, Var, Rel): сдвиги */
0xC0, /* End Collection */
0xC0, /* End Collection */
0x05, 0x0C, /* Usage Page (Consumer) */
0x09, 0x01, /* Usage (Consumer Control) */
0xA1, 0x01, /* Collection (Application) */
0x85, RID_CONSUMER, /* Report ID (3) */
0x09, 0xE9, 0x09, 0xEA, /* Usage: Volume Increment, Decrement */
0x09, 0xE2, 0x09, 0xCD, /* Usage: Mute, Play/Pause */
0x15, 0x00, 0x25, 0x01, /* Logical Min/Max: 0 … 1 */
0x75, 0x01, 0x95, 0x04, /* Report Size 1, Report Count 4 */
0x81, 0x02, /* Input (Data, Var, Abs): четыре клавиши */
0x95, 0x04, /* Report Count 4 */
0x81, 0x01, /* Input (Const): добивка */
0xC0, /* End Collection */
};
struct kbd_report { uint8_t id, mod, reserved, keys[6]; };
struct mouse_report { uint8_t id, buttons; int8_t x, y, wheel; };
struct consumer_report { uint8_t id, bits; };
enum { CC_VOL_UP = 1u << 0, CC_VOL_DOWN = 1u << 1, CC_MUTE = 1u << 2, CC_PLAY_PAUSE = 1u << 3 };
_Static_assert(sizeof(hid_report_desc) == USBD_CUSTOM_HID_REPORT_DESC_SIZE,
"USBD_CUSTOM_HID_REPORT_DESC_SIZE не равен длине дескриптора");
_Static_assert(sizeof(struct kbd_report) == 9u && sizeof(struct mouse_report) == 5u
&& sizeof(struct consumer_report) == 2u, "размеры отчётов: 9, 5 и 2 байта с номером");
_Static_assert(CUSTOM_HID_EPIN_SIZE >= sizeof(struct kbd_report),
"точка IN меньше отчёта клавиатуры: поправить CUSTOM_HID_EPIN_SIZE");
_Static_assert(USBD_CUSTOMHID_OUTREPORT_BUF_SIZE >= 2u, "отчёт светодиодов: номер + байт");
#define Q_LEN 16u /* степень двойки */
struct hid_slot { uint8_t len; uint8_t data[sizeof(struct kbd_report)]; };
static struct hid_slot q[Q_LEN];
static uint32_t q_head, q_tail, q_lost; /* всё зовётся из главного цикла */
extern USBD_HandleTypeDef hUsbDeviceFS;
static int q_room(uint32_t n)
{
if (Q_LEN - (q_head - q_tail) >= n)
return 1;
q_lost++;
return 0;
}
static void q_put(const void *r, uint8_t len)
{
struct hid_slot *s = &q[q_head % Q_LEN];
s->len = len;
memcpy(s->data, r, len);
q_head++;
}
void hid_kbd_tap(uint8_t mod, uint8_t key)
{
struct kbd_report r = { RID_KBD, mod, 0, { key } };
if (!q_room(2u)) /* нажатие без отпускания — залипшая клавиша */
return;
q_put(&r, sizeof r);
memset(&r.mod, 0, sizeof r - 1u); /* отпускание: всё, кроме номера, — нули */
q_put(&r, sizeof r);
}
void hid_mouse_move(uint8_t buttons, int8_t dx, int8_t dy, int8_t wheel)
{
struct mouse_report r = { RID_MOUSE, buttons, dx, dy, wheel };
if (q_room(1u))
q_put(&r, sizeof r);
}
void hid_consumer_tap(uint8_t bits)
{
struct consumer_report r = { RID_CONSUMER, bits };
if (!q_room(2u))
return;
q_put(&r, sizeof r);
r.bits = 0;
q_put(&r, sizeof r);
}
void hid_task(void)
{
static uint8_t tx[2][sizeof(struct kbd_report)]; /* один в передаче, в другой пишем */
static uint32_t cur;
const struct hid_slot *s;
uint8_t st;
if (hUsbDeviceFS.dev_state != USBD_STATE_CONFIGURED || q_tail == q_head)
return; /* до SET_CONFIGURATION SendReport молча не шлёт */
s = &q[q_tail % Q_LEN];
memcpy(tx[cur], s->data, s->len);
NVIC_DisableIRQ(USB_IRQ);
st = USBD_CUSTOM_HID_SendReport(&hUsbDeviceFS, tx[cur], s->len);
NVIC_EnableIRQ(USB_IRQ);
if (st == (uint8_t)USBD_OK) { /* USBD_BUSY: прошлый ещё не забран — позже */
cur ^= 1u;
q_tail++;
}
}
static volatile uint8_t kbd_leds; /* бит 0 — Num Lock, 1 — Caps Lock, 2 — Scroll Lock */
static int8_t hid_out_event(uint8_t report_id, uint8_t state)
{
if (report_id == RID_KBD)
kbd_leds = state;
#if defined(STM32F407xx)
(void)USBD_CUSTOM_HID_ReceivePacket(&hUsbDeviceFS); /* F4 V1.28.0: точку OUT перевзводит программа */
#endif
return (int8_t)USBD_OK;
}
static int8_t hid_init(void)
{
return (int8_t)USBD_OK;
}
static int8_t hid_deinit(void)
{
return (int8_t)USBD_OK;
}
USBD_CUSTOM_HID_ItfTypeDef hid_fops = { hid_report_desc, hid_init, hid_deinit, hid_out_event };
uint8_t hid_leds(void)
{
return kbd_leds;
}
uint32_t hid_lost(void)
{
return q_lost;
}
Next: section 20г "A keyboard on STM32: dissecting QMK — matrix, debounce, layers and USB HID" — real keyboard firmware: the matrix, debounce, layers and reports.