What Is the HID Keyboard Report Format?
The 8-byte structure of an HID keyboard report: the modifier byte, the reserved byte, and 6 keycode slots. How to send a report from STM32 middleware.
An HID keyboard report is the packet firmware sends to the host any time a key’s state
changes. The easy thing to get wrong here is that this report doesn’t contain ASCII
characters. Firmware doesn’t send the letter a. It sends the usage ID for the a key, and
the host decides the final character based on the current keyboard layout and the modifier
value.
Many HID keyboards support the 8-byte boot keyboard format because it’s simple and works well with firmware, BIOS, and UEFI. Some more advanced keyboards use their own Report Protocol instead, an NKRO bitmap or a consumer control report, for example.
1. The 8-byte structure
Offset Content
──────────────────────────────────────────
0 Modifier byte
1 Reserved (0x00)
2 Keycode 1
3 Keycode 2
4 Keycode 3
5 Keycode 4
6 Keycode 5
7 Keycode 6
In C:
typedef struct { uint8_t modifier; /* Modifier keys (Ctrl, Shift, Alt, GUI) */ uint8_t reserved; /* Always 0x00 */ uint8_t keycodes[6]; /* Up to 6 simultaneous keycodes */} HID_KeyboardReport_t;Here’s a real report, captured with Wireshark, from pressing “1” on a keypad:
00 00 1E 00 00 00 00 00. 0x1E is the HID usage for the 1/! key in the Keyboard Usage
Page.
2. The modifier byte
The modifier byte is a bitmask, with each bit tied to one modifier key:
| Bit | Mask | Key |
|---|---|---|
| 0 | 0x01 | Left Ctrl |
| 1 | 0x02 | Left Shift |
| 2 | 0x04 | Left Alt |
| 3 | 0x08 | Left GUI |
| 4 | 0x10 | Right Ctrl |
| 5 | 0x20 | Right Shift |
| 6 | 0x40 | Right Alt |
| 7 | 0x80 | Right GUI |
An example of pressing Left Shift + A:
HID_KeyboardReport_t report = {0};report.modifier = 0x02; /* Left Shift */report.keycodes[0] = 0x04; /* HID keycode for 'a' */3. The keycode array: 6-key rollover
The 6 keycode slots allow up to 6 keys, on top of the modifiers, to be pressed at once. This is also the limit of the Boot Protocol.
/* On keypress: find an empty slot and fill it in */for (uint8_t i = 0; i < 6; i++) { if (report.keycodes[i] == 0x00) { report.keycodes[i] = new_keycode; break; }}/* On key release: find that keycode and clear it */for (uint8_t i = 0; i < 6; i++) { if (report.keycodes[i] == new_keycode) { report.keycodes[i] = 0x00; break; }}No sorting or compaction needed. Keycode 0x00 means “no key here.” The host only cares about non-zero values.
4. ErrorRollOver
When firmware can’t determine a valid key combination, a diode-less matrix with more than 6
keys pressed, for example, the keyboard’s Boot Protocol has an ErrorRollOver usage, 0x01:
The ErrorRollOver report: 00 00 01 01 01 01 01 01
Followed by a null report: 00 00 00 00 00 00 00 00
A null report should follow right after ErrorRollOver, so the host knows the state has been reset. This isn’t a software error, and the host doesn’t display any message or special character for it either. So from the end user’s perspective, it has no visible effect at all.
The only way to actually confirm this report happened is with a USB protocol analyzer, a Wireshark capture, or a similar dedicated USB debugging tool.
5. Sending a report from application code
The example below uses the ST USB Device Library. API names and calling conventions can differ depending on the middleware.
extern USBD_HandleTypeDef hUsbDeviceFS;void Keyboard_SendReport(const HID_KeyboardReport_t *report){ USBD_HID_SendReport(&hUsbDeviceFS, (uint8_t *)report, sizeof(HID_KeyboardReport_t));}6. Reporting when no key is pressed
A keypress is only half the story. The host also needs to know when a key has been released.
HID_KeyboardReport_t empty_report = {0};Keyboard_SendReport(&empty_report);If firmware only sends a report on keypress but never sends an empty report once every key is released, the host will assume the key is still held down and repeat it endlessly.
A common approach is “tap-style”: right after each key-down report, firmware immediately sends a null report on the very next call, instead of waiting for an actual release event. This lets firmware control its own repeat rate, rather than letting the OS apply its own typematic repeat at whatever pace it defaults to.
7. Rollover and ghosting
6-key rollover (6KRO) is the Boot Protocol’s limit: up to 6 keycodes plus modifiers. If you need to support more simultaneous keys than that, design a custom Report Descriptor instead of trying to squeeze it into the 8-byte boot report.
N-key rollover (NKRO) has no limit on simultaneous keys, but it needs a different HID Report Descriptor (a bitmap instead of an array) and isn’t compatible with Boot Protocol.
Ghosting is a hardware phenomenon on diode-less matrices. It’s a hardware problem, and has nothing to do with the HID report format itself.
Related posts
- What Is an HID Report Descriptor?
- What Are HID Usage Tables?
- What Are Keyboard Matrix Fundamentals?
- HID Keyboard Baseline (blog)
Public references
- HID Usage Tables 1.5, USB-IF, Section 10: Keyboard/Keypad Page
- Device Class Definition for HID 1.11, USB-IF, Appendix B: Boot Interface Descriptors
Found this article useful?
Share, give feedback, or support if you find this content valuable.
Nội dung liên quan
Một số bài viết, ghi chú hoặc project có liên quan đến nội dung bạn vừa đọc.
What Is a USB Descriptor Overview?
The hierarchical structure of USB descriptors: Device, Configuration, Interface, Endpoint - wTotalLength, bDescriptorType, and what commonly breaks enumeration.
Finishing a Real HID Keyboard, from Keypress to Release
Turning key press and release events into HID reports and sending them to the host, plus what to watch out for with Boot Protocol and tap-style null reports.
What Is a USB Descriptor?
The concept and structure of USB descriptors, explained with real examples. Includes a look at how Boot Protocol fits into the Configuration Descriptor.
Biến note thành bài viết hoàn chỉnh
Notes là nơi ghi nhanh khái niệm.