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

20в. USB HID на практике: дескриптор отчёта и составное устройство

Раздел 20 назвал HID классом, которому не нужны драйверы, раздел 20а показал хост, который читает загрузочную клавиатуру. Здесь — сторона устройства. HID-устройство шлёт хосту короткие отчёты, а их формат описывает дескриптор отчёта: маленькая программа из байтов, которую хост разбирает при подключении. Разберём этот язык, прочитаем байт за байтом мышь из библиотеки ST своим разборщиком, соберём загрузочную клавиатуру и составное устройство — клавиатура, мышь и клавиши громкости на одном интерфейсе — и напишем код отправки отчётов.

Опоры: библиотека USB Device ST из пакетов STM32Cube_FW_F1 V1.8.7 и STM32Cube_FW_F4 V1.28.0 (классы HID и Custom HID, пример CustomHID_Standalone для платы STM3210E_EVAL), библиотека USB Host ST (usbh_hid_keybd.c, usbh_hid.c), MDK-Middleware 8.1.0 (usb_hid.h, шаблон USBD_User_HID_Mouse.c, USBD_Config_HID.h), дерево Zephyr 4.4.2 (include/zephyr/usb/class/hid.h, usb_ch9.h) и hidusage.h из Windows SDK 10.0.19041 — из этих источников коды клавиш громкости есть только в нём. Спецификация HID 1.11 и таблицы назначений HUT не цитируются: все коды взяты из этих заголовков.

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

Оригинал Перевод
report, input / output / feature report отчёт: входной (устройство → хост), выходной (хост → устройство), отчёт свойства
report descriptor дескриптор отчёта: описание всех отчётов устройства
item: main, global, local элемент дескриптора: главный, глобальный, локальный
Usage Page, Usage страница назначений; назначение — что значит поле
Report Size, Report Count размер поля в битах; число полей
Report ID номер отчёта: первый байт, когда отчётов несколько
collection (application, physical) коллекция: прикладная, физическая
boot protocol, report protocol загрузочный протокол (формат зафиксирован), протокол отчётов (по дескриптору)
modifier, keycode клавиша-модификатор (Ctrl, Shift…); код клавиши
consumer control потребительские клавиши: громкость, пауза
bInterval период опроса прерывающей точки хостом

20в.1 Отчёт и его описание

HID-устройство передаёт данные отчётами через прерывающую точку IN: мышь — сдвиг и кнопки, клавиатура — модификаторы и коды нажатых клавиш. Хост спрашивает точку раз в bInterval. Выходные отчёты (светодиоды Caps Lock) идут обратно — через точку OUT или запросом SET_REPORT по нулевой точке; класс Custom HID ST принимает оба пути.

Что лежит в отчёте, устройство описывает дескриптором отчёта. Хост читает его запросом GET_DESCRIPTOR с типом 0x22 (HID_REPORT_DESC в usbd_hid.h, USB_DESC_HID_REPORT в hid.h Zephyr), а его длину заранее узнаёт из HID-дескриптора (тип 0x21) внутри дескриптора конфигурации — поле wDescriptorLength (в комментариях ST — wItemLength). Класс ST отдаёт MIN(USBD_CUSTOM_HID_REPORT_DESC_SIZE, req->wLength) байт: длину берёт из макроса, а не из массива.

Если отчётов несколько, у каждого свой номер — Report ID, и номер идёт первым байтом. Так делает пример ST CustomHID_Standalone: отчёт кнопки — SendBuffer[0] = KEY_REPORT_ID, SendBuffer[1] — состояние, USBD_CUSTOM_HID_SendReport(…, SendBuffer, 2); принятый выходной отчёт уходит в обработчик как пара «номер, байт» (OutEvent(Report_buf[0], Report_buf[1])).

Составное HID-устройство из 20в.5: входной отчёт клавиатуры 9 байт с номером 1, выходной отчёт светодиодов 2 байта с номером 1, мышь 5 байт с номером 2, потребительские клавиши 2 байта с номером 3; номер всегда первый байт, точка IN одна


20в.2 Язык дескриптора: элементы и их коды

Дескриптор — последовательность элементов. Первый байт элемента — префикс: тег в старших четырёх битах, тип в двух, размер данных в двух младших. Так его собирает макрос HID_ITEM(bTag, bType, bSize) в hid.h Zephyr. Код размера 0, 1, 2, 3 значит 0, 1, 2 и 4 байта данных (HID_LOGICAL_MIN32 ставит код 3 и пишет четыре байта). Многобайтовые данные идут младшим байтом вперёд: HID_USAGE_PAGE16(page) в Zephyr пишет сначала (uint8_t)page, HID_LogicalMaxS(x) в MDK — x & 0xFFU.

Элемент Тип Тег Код с 1 байтом данных
Input главный (0) 0x8 0x81
Output главный (0) 0x9 0x91
Feature главный (0) 0xB 0xB1
Collection главный (0) 0xA 0xA1
Usage Page глобальный (1) 0x0 0x05
Logical Minimum глобальный (1) 0x1 0x15
Logical Maximum глобальный (1) 0x2 0x25
Report Size глобальный (1) 0x7 0x75
Report ID глобальный (1) 0x8 0x85
Report Count глобальный (1) 0x9 0x95
Usage локальный (2) 0x0 0x09
Usage Minimum локальный (2) 0x1 0x19
Usage Maximum локальный (2) 0x2 0x29

End Collection — 0xC0, без данных. Те же коды дают макросы MDK (HID_Input(x) — 0x81U,x, HID_ReportID(x) — 0x85U,x) и таблица тегов в usbh_hid.h библиотеки ST USB Host.

Работает это как маленький язык с состоянием:

Размер отчёта — сумма Report Size × Report Count всех главных элементов одного вида и номера, делённая на 8, и ещё байт номера, если номера есть.


20в.3 Мышь из библиотеки ST байт за байтом

Модель _verify/hid20c/hid20c.py — свой разборщик по правилам 20в.2. Он берёт массив HID_MOUSE_ReportDesc прямо из usbd_hid.c (в пакетах F1 V1.8.7 и F4 V1.28.0 байты одинаковы) и печатает дерево:

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

Итог разбора: 74 байта, 38 элементов, 2 коллекции. Входной отчёт — 32 бита, 4 байта: байт кнопок (3 бита кнопок и 5 бит добивки), затем X, Y и колесо по байту со знаком, от −127 до 127, относительные. Отчёт свойства — 1 байт. Сверка с тем, что объявила библиотека: HID_MOUSE_REPORT_DESC_SIZE = 74 — длина совпадает, HID_EPIN_SIZE = 4 — размер входного отчёта совпадает.

Последние 22 байта — отчёт свойства с назначением Motion Wakeup (0x3C, HID_USAGE_GENERIC_MOTION_WAKEUP в MDK). Шаблон мыши MDK USBD_User_HID_Mouse.c без них: разборщик получил 52 байта — ровно столько, сколько заявлено в комментарии шаблона, — и те же 4 байта входа. Макрос HID_MOUSE_REPORT_DESC(3) Zephyr даёт те же 52 байта из тех же элементов, только Report Size и Report Count стоят в другом порядке: для глобальных элементов порядок не важен, важно, что они заданы до главного.


20в.4 Клавиатура: загрузочный протокол и коды клавиш

Загрузочный отчёт клавиатуры зафиксирован заранее, чтобы его понимал хост, не читающий дескриптор. Так устроена библиотека ST USB Host: она запрашивает дескриптор отчёта, но не разбирает его, ставит загрузочный протокол (USBH_HID_SetProtocol(phost, 0U)) и читает 8 байт по постоянным смещениям: USBH_HID_KEYBD_REPORT_SIZE = 8, модификаторы — биты байта 0, коды — шесть байт с байта 2, от 0 до 101. Устройство, у которого bInterfaceProtocol не 1 (клавиатура) и не 2 (мышь), она не берёт: «Protocol not supported».

Дескриптор такого отчёта — макрос HID_KEYBOARD_REPORT_DESC() Zephyr. Разборщик: 63 байта, 32 элемента, входной отчёт 64 бита — 8 байт, выходной 1 байт. Раскладка входного:

Байт 0 1 2…7
что модификаторы, 8 бит резерв, константа до шести кодов нажатых клавиш, массив

Биты модификаторов (enum hid_kbd_modifier в hid.h): левый Ctrl — 0x01, левый Shift — 0x02, левый Alt — 0x04, левый GUI — 0x08, правые — 0x10, 0x20, 0x40, 0x80. Выходной байт — светодиоды: Num Lock 0x01, Caps Lock 0x02, Scroll Lock 0x04. Коды клавиш (enum hid_kbd_code; те же в usbh_hid_keybd.h ST и usb_hid.h MDK): A — 0x04, …, Z — 0x1D, 1 — 0x1E, …, 0 — 0x27, Enter — 0x28, пробел — 0x2C. Заглавная буква — это тот же код с Shift: «H» — 02 00 0B 00 00 00 00 00.

Нажатие — это отчёт с кодом, отпускание — отчёт без него. Модель переводит «Hello, STM32!» (13 символов) в 26 отчётов, а таблицами HID_KEYBRD_Key и HID_KEYBRD_ShiftKey библиотеки ST USB Host (раскладка QWERTY) переводит обратно — получается тот же текст. Хост в модели считает символом код, которого не было в прошлом отчёте; поэтому две «l» подряд требуют отпускания между ними.

Скорость набора определяет опрос: один отчёт за опрос, два отчёта на символ. При опросе раз в 1 мс строка уходит за 26 мс — 500 символов в секунду, при 10 мс — за 260 мс, 50 символов в секунду.


20в.5 Составное устройство: номера отчётов и потребительские клавиши

Составное устройство из раздела 20 — это несколько интерфейсов с разными классами. Для HID есть путь проще: один интерфейс, одна точка IN и несколько прикладных коллекций, каждая со своим Report ID. Хост видит клавиатуру, мышь и пульт громкости сразу, а в библиотеке ST хватает класса Custom HID — он берёт дескриптор отчёта у программы.

Свой дескриптор в _verify/hid20c/hid20c.c — три коллекции: клавиатура (номер 1: вход 8 байт как у загрузочной, выход — светодиоды), мышь (номер 2) и потребительские клавиши (номер 3). Последний блок:

    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                            */

Страница Consumer — 0x0C (HID_USAGE_PAGE_CONSUMER в MDK и в hidusage.h), назначения — из hidusage.h: громче 0xE9, тише 0xEA, без звука 0xE2, пауза 0xCD. Четыре бита — четыре клавиши; у добивки Report Size не повторён: действует 1 из строки выше.

Разборщик по байтам из объектного файла (секция данных, которую armclang положил в hid20c.o): 150 байт, 77 элементов, 4 коллекции (три прикладные и физическая у мыши), ошибок нет. Отчёты:

Номер Вид Бит данных Байт с номером
1 вход, клавиатура 64 9
1 выход, светодиоды 8 2
2 вход, мышь 32 5
3 вход, потребительские 8 2

Отсюда настройка класса: USBD_CUSTOM_HID_REPORT_DESC_SIZE = 150, точка IN не меньше 9 байт, буфер выходного отчёта USBD_CUSTOMHID_OUTREPORT_BUF_SIZE = 2 — как раз номер и байт светодиодов, это и значение по умолчанию.

У примера ST CustomHID_Standalone тот же приём: 163 байта, семь номеров, каждый отчёт — 2 байта с номером, отсюда CUSTOM_HID_EPIN_SIZE и CUSTOM_HID_EPOUT_SIZE по 2. Разборщик показывает и то, на чём пример держится: в блоке номера 5 Report Count не задан — действует 1 от блока номера 4.

Загрузочным такое устройство себя не объявляет: Custom HID ставит подкласс 0 и протокол 0. Хост, которому нужен загрузочный протокол, — библиотека ST USB Host или, вероятно, настройка BIOS — клавиатуру не увидит.


20в.6 Код: очередь отчётов, bInterval и задержка

Файл _verify/hid20c/hid20c.c собирается armclang под STM32F103 (библиотека из пакета F1 V1.8.7) и STM32F407 (F4 V1.28.0) без предупреждений. Отчёты и проверки размеров на этапе компиляции:

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, "отчёт светодиодов: номер + байт");

Третья проверка ловит разницу пакетов. В F4 V1.28.0 размер точки CUSTOM_HID_EPIN_SIZE обёрнут в #ifndef, и его задают в usbd_conf.h. В F1 V1.8.7 — просто #define CUSTOM_HID_EPIN_SIZE 0x02U, и без правки заголовка класса сборка останавливается на этой строке («2 >= 9»). Так и задумано: иначе точка останется на 2 байта, а отчёт клавиатуры в неё не влезет.

Отправка — очередь, из которой главный цикл берёт по отчёту, когда точка свободна:

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

Почему так. USBD_CUSTOM_HID_SendReport не копирует отчёт, а передаёт указатель — буфер нельзя трогать, пока хост его не забрал; поэтому буферов два. Пока точка занята, функция отвечает USBD_BUSY, и отчёт остаётся в очереди. До конфигурации устройства она ничего не шлёт, но отвечает USBD_OK — отсюда проверка dev_state. Прерывание USB на время вызова запрещено, как в 20.7: библиотеку не зовут из двух мест сразу. Очередь на 16 отчётов по 10 байт занимает 160 байт ОЗУ.

Выходной отчёт принимает hid_out_event. Для F103 armclang с -O2 делает из неё 7 команд — сравнение номера и запись байта:

CMP      r0,#1
ITTT     EQ
MOVWEQ   r0,#:LOWER16: kbd_leds
MOVTEQ   r0,#:UPPER16: kbd_leds
STRBEQ   r1,[r0,#0]
MOVS     r0,#0
BX       lr

Для F407 в ней есть ещё вызов USBD_CUSTOM_HID_ReceivePacket: класс из F4 V1.28.0 после приёма точку OUT сам не перевзводит, это делает программа (так и в примере ST). Класс из F1 V1.8.7 перевзводит её сам.

Опрос. bInterval в дескрипторе точки — период, с которым хост спрашивает её. На полной скорости это миллисекунды, 1…255 (USB_FS_INT_EP_INTERVAL в Zephyr, подсказка в USBD_Config_HID.h MDK). На высокой — 2^(bInterval−1) × 125 мкс: у класса HID ST HID_HS_BINTERVAL = 7, это 64 × 125 мкс = 8 мс. Событие ждёт ближайшего опроса — в среднем половину периода, в худшем случае весь:

Настройка bInterval ожидание в среднем в худшем
класс HID ST, HID_FS_BINTERVAL 10 мс 5,0 мс 10 мс
класс Custom HID ST, CUSTOM_HID_FS_BINTERVAL 5 мс 2,5 мс 5 мс
hid20c, usbd_conf.h 1 мс 0,5 мс 1 мс

Хост может опрашивать и реже заявленного: библиотека ST USB Host не опрашивает чаще HID_MIN_POLL = 10 мс, и bInterval = 1 превращается у неё в 10 мс.


20в.7 Грабли

Клавиша залипла, двойные буквы пропадают. Класс HID ST: пока точка занята, USBD_HID_SendReport молча выбрасывает отчёт и отвечает USBD_OK. Код, который шлёт нажатие и сразу отпускание, теряет отпускание. Модель: символ раз в 20 мс, опрос 10 мс — из 26 отчётов дошло 13, «Hello, STM32!» превратилось в «Helo, STM32!», последняя клавиша осталась нажатой. Нужна очередь, как в 20в.6.

Отчёт клавиатуры не проходит, мышь работает. Точка IN меньше отчёта. В F1 V1.8.7 CUSTOM_HID_EPIN_SIZE = 2 и без #ifndef — его правят в заголовке класса.

Хост видит мусор или не видит устройство. USBD_CUSTOM_HID_REPORT_DESC_SIZE не равен длине массива: класс отдаёт столько байт, сколько в макросе, — обрезанный дескриптор или хвост чужой памяти. Спасает _Static_assert.

Отчёт на байт длиннее, чем ждёт хост. Report ID в дескрипторе есть, а номер первым байтом не положили — или наоборот. Номер есть в дескрипторе — он есть в каждом отчёте.

Сумма бит не делится на 8. Забыли добивку: после трёх бит кнопок нужно поле Const на 5 бит, после пяти светодиодов — на 3.

Caps Lock на F4 зажёгся один раз. В классе Custom HID из F4 V1.28.0 точку OUT класс сам не перевзводит: это делает обработчик выходного отчёта в программе вызовом USBD_CUSTOM_HID_ReceivePacket; без него следующий отчёт не придёт.

Страница производителя записана задом наперёд. В примере ST CustomHID_Standalone байты 06 FF 00 подписаны «Vendor Page: 0xFF00», но данные идут младшим байтом вперёд — там же 26 FF 00 у них значит 255. Выходит страница 0x00FF, а страницы производителя по hidusage.h — 0xFF00…0xFFFF; HID_UsagePageVendor(0x00) из MDK пишет 06 00 FF.

Клавиатура не работает на простом хосте. Составное устройство с номерами отчётов не загрузочное: библиотека ST USB Host и ей подобные его не возьмут. Если нужна клавиатура «везде» — отдельный интерфейс с подклассом 1 и протоколом 1 и 8-байтовым отчётом без номера.


20в.8 Итоги

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

hid20c.c — составное HID-устройство: клавиатура, мышь и потребительские клавиши.

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

Дальше: раздел 20г «Клавиатура на STM32: разбор QMK — матрица, антидребезг, слои и USB HID» — настоящая прошивка клавиатуры: матрица, антидребезг, слои и отчёты.

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