Understanding HID Report Descriptors
A comprehensive, interactive guide to USB HID report descriptors - what they are, how they work, and how to interpret every byte.
What is a HID Report Descriptor?
A HID report descriptor is a binary data structure that tells the USB host (your computer) exactly how to interpret the data sent by your HID device. Think of it as a “data dictionary” that describes:
What data the device sends (inputs) and receives (outputs)
How much data (size in bits/bytes)
What format the data is in (signed/unsigned, range, units)
What it represents (buttons, axes, sensors, LEDs)
Without a report descriptor, the raw bytes from your device would be meaningless. The descriptor gives them structure and meaning.
Report Structure Overview
Reports vs. Descriptors
Report Descriptor (sent once at connection):
"I will send you 8 bytes. The first 4 bytes are a float representing
temperature, the next 2 bytes are a signed 16-bit X position, and the
last 2 bytes are a signed 16-bit Y position."
Report Data (sent repeatedly during operation):
[0x42, 0x28, 0x00, 0x00, 0xFF, 0x64, 0x00, 0xC8]
└─────────┬─────────┘ └──┬──┘ └──┬──┘
Temperature X=100 Y=200
(25.5°C)
The descriptor is the schema, the report is the data.
Interactive Example: Temperature Sensor
Let’s build a complete descriptor for a simple temperature sensor. Hover over each byte to see what it means!
📊 What This Descriptor Creates
The above descriptor (15 bytes total) tells the USB host:
- Data type: Signed 16-bit integer (range -32768 to 32767)
- Report structure: 1 field × 16 bits = 2 bytes per report
- Direction: Input (device sends to host)
Your firmware structure would look like:
struct temperature_report {
int16_t temperature; // -32768 to 32767
} __attribute__((packed));
// Total: 2 bytes
Interactive Example: 3-Axis Accelerometer
Now let’s look at a more complex example with multiple fields. Hover over each byte to understand the structure!
struct accelerometer_report {
uint8_t report_id; // Always 0x01
int16_t x; // -32768 to 32767
int16_t y; // -32768 to 32767
int16_t z; // -32768 to 32767
} __attribute__((packed));
Understanding Item Types
HID descriptors are built from three types of items, each serving a different purpose:
Main Items (Red)
Define inputs, outputs, and structure
0xA1- Collection (start grouping)0xC0- End Collection (end grouping)0x81- Input (device → host data)0x91- Output (host → device data)0xB1- Feature (bidirectional config)
Main items consume the Global and Local items that came before them.
Global Items (Blue)
Affect all following items until changed
0x05- Usage Page (data category)0x15/0x16/0x17- Logical Minimum (1/2/4 bytes)0x25/0x26/0x27- Logical Maximum (1/2/4 bytes)0x75- Report Size (bits per field)0x95- Report Count (number of fields)0x85- Report ID (report identifier)
Global items remain active until explicitly changed.
Local Items (Green)
Affect only the next Main item
0x09- Usage (what the field represents)0x19- Usage Minimum (range start)0x29- Usage Maximum (range end)
Local items are reset after each Main item.
Byte-Level Item Format
Every HID item follows this structure:
┌──────────┬─────────────────┐
│ Prefix │ Data (0-4 │
│ Byte │ bytes) │
└──────────┴─────────────────┘
Prefix Byte Breakdown
0x95
│
┌───┴───┐
1001 0101
│ │ │ │
│ │ │ └──> Size bits [1:0] = 01 (1 byte of data)
│ │ └─────> Type bits [3:2] = 10 (Global item)
│ └───────> Reserved
└──────────> Tag [7:4] = 1001 (Report Count)
Size field:
00= 0 bytes01= 1 byte10= 2 bytes11= 4 bytes
Type field:
00= Main01= Global10= Local11= Reserved
Report Size vs Report Count
This is the most important concept to understand:
Report Size
Number of BITS per individual field
0x75, 0x10 → 16 bits per field
0x75, 0x08 → 8 bits per field
0x75, 0x01 → 1 bit per field (for buttons)
Report Count
Number of fields
0x95, 0x03 → 3 fields
0x95, 0x01 → 1 field
0x95, 0x08 → 8 fields (e.g., 8 buttons)
The Math
Total data bits = Report Size × Report Count
Example: 3 axes, 16 bits each
0x75, 0x10 (Report Size = 16 bits)
0x95, 0x03 (Report Count = 3)
Total = 16 × 3 = 48 bits = 6 bytes
Input vs Output Reports
Input Reports (Device → Host)
0x81, 0x02 // Input (Data, Variable, Absolute)
Direction: Device sends to computer
Use cases: Sensor data, button states, joystick position
Flow: Firmware → USB → Host → ROS 2 Topic
Output Reports (Host → Device)
0x91, 0x02 // Output (Data, Variable, Absolute)
Direction: Computer sends to device
Use cases: LED control, motor commands, haptic feedback
Flow: ROS 2 Topic → Host → USB → Firmware
Bidirectional Example
Output Report ID 2: LED brightness (1 byte)
const uint8_t descriptor[] = {
0xA1, 0x01, // Collection (Application)
// INPUT: Temperature
0x85, 0x01, // Report ID (1)
0x05, 0x01, // Usage Page (Generic Desktop)
0x15, 0x00, // Logical Min (0)
0x26, 0xFF, 0x00, // Logical Max (255)
0x75, 0x08, // Report Size (8 bits)
0x95, 0x01, // Report Count (1)
0x81, 0x02, // Input
// OUTPUT: LED
0x85, 0x02, // Report ID (2)
0x05, 0x08, // Usage Page (LEDs)
0x15, 0x00, // Logical Min (0)
0x25, 0x64, // Logical Max (100)
0x75, 0x08, // Report Size (8 bits)
0x95, 0x01, // Report Count (1)
0x91, 0x02, // Output
0xC0 // End Collection
};
Main Item Flags
The byte following Input/Output/Feature contains important flags:
Bit 1 (1): Variable (not Array)
Bit 2 (0): Absolute (not Relative)
Bits 3-7: Various other flags
Common values:
0x01- Constant (padding)0x02- Data, Variable, Absolute (sensors, values)0x06- Data, Variable, Relative (mouse movement)
Usage Pages Reference
Code |
Name |
Examples |
|---|---|---|
|
Generic Desktop |
X, Y, Z, Rx, Ry, Rz, Wheel |
|
Simulation |
Steering, Throttle, Brake |
|
Generic Device |
Battery, Wireless Signal |
|
Keyboard |
Key codes, modifiers |
|
LEDs |
Num Lock, Caps Lock, indicators |
|
Button |
Button 1-65535 |
|
Consumer |
Volume, Mute, Play/Pause |
|
Sensors |
Accelerometer, Gyro, Temperature |
Common Mistakes
❌ Wrong: Report Size in bytes
0x75, 0x02, // Report Size (2) ← This means 2 BITS, not bytes!
✅ Correct: Report Size in bits
0x75, 0x10, // Report Size (16 bits) = 2 bytes
❌ Wrong: Forgetting padding
// 3 buttons = 3 bits (missing 5 bits of padding!)
0x75, 0x01, // Report Size (1 bit)
0x95, 0x03, // Report Count (3)
0x81, 0x02, // Input
✅ Correct: Add padding to byte boundary
// 3 buttons + 5 padding bits = 1 byte
0x75, 0x01, // Report Size (1 bit)
0x95, 0x03, // Report Count (3 buttons)
0x81, 0x02, // Input
0x95, 0x05, // Report Count (5 padding bits)
0x81, 0x01, // Input (Constant) ← PADDING
Quick Reference
Building a Descriptor
Start collection:
0xA1, 0x01Set Report ID (optional):
0x85, <id>Set Usage Page:
0x05, <page>For each field:
Set Usage:
0x09, <usage>Set Min/Max:
0x15/0x25or0x16/0x26Set Report Size:
0x75, <bits>Set Report Count:
0x95, <count>Add Input/Output:
0x81/0x91, 0x02
Close collection:
0xC0
Type Quick Reference
Data Type |
Report Size |
Min |
Max |
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Use physical range |
Use physical range |
See Also
Schema Reference - YAML to descriptor mapping
Type System - Type conversions and limitations
Firmware Integration - Using descriptors in firmware
Debugging Guide - Troubleshooting descriptor issues