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!

🌡️ Temperature Sensor Descriptor
0xA1 COLLECT
Collection (Application) Starts a new HID application collection. All items inside belong to this device.
0x01 APP
Application Type This collection represents a complete application (as opposed to physical, logical, etc.)
0x05 USAGE PG
Usage Page (Global) Sets the category for all following items. Affects all subsequent fields until changed.
0x01 GEN DESK
Generic Desktop Usage Page 0x01 = Generic Desktop Controls (mouse, keyboard, joystick, etc.)
0x09 USAGE
Usage (Local) Defines what this specific field represents. Only affects the next Main item.
0x00 CUSTOM
Undefined/Custom Usage 0x00 = Undefined. We're using a custom/vendor-defined sensor.
0x16 LOG MIN
Logical Minimum (2 bytes) Sets the minimum value for data fields. 0x16 means 2 bytes of data follow.
0x00 LOW
Min Value Low Byte Low byte of minimum value: 0x8000 = -32768 (little-endian)
0x80 HIGH
Min Value High Byte High byte of minimum value: 0x8000 = -32768 (signed 16-bit min)
0x26 LOG MAX
Logical Maximum (2 bytes) Sets the maximum value for data fields. 0x26 means 2 bytes of data follow.
0xFF LOW
Max Value Low Byte Low byte of maximum value: 0x7FFF = 32767 (little-endian)
0x7F HIGH
Max Value High Byte High byte of maximum value: 0x7FFF = 32767 (signed 16-bit max)
0x75 RPT SIZE
Report Size Number of BITS per field. This affects all following fields.
0x10 16 bits
16 Bits 0x10 = 16 decimal. Each field will be 16 bits = 2 bytes.
0x95 RPT CNT
Report Count Number of fields. Total data = Report Size × Report Count.
0x01 1 field
1 Field Send 1 field of 16 bits = 2 bytes total per report.
0x81 INPUT
Input (Main Item) Declares an input field (device → host). Uses all previously set Global and Local items.
0x02 VAR
Data, Variable, Absolute 0x02 = 0b00000010. Bit 0=Data (not const), Bit 1=Variable (not array), Bit 2=Absolute (not relative)
0xC0 END
End Collection Closes the application collection started with 0xA1.
Main Item
Global Item
Local Item
Data Byte

📊 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!

🎯 3-Axis Accelerometer Descriptor
0xA1 COLLECT
Collection (Application) Start of application collection
0x01 APP
Application Type Collection type = Application
0x85 RPT ID
Report ID Assigns an ID to this report type. First byte of every report will be this ID.
0x01 ID=1
Report ID 1 This report will have ID = 1. Useful when device has multiple report types.
0x05 USAGE PG
Usage Page Sets data category
0x01 GEN DESK
Generic Desktop Category: Generic Desktop Controls
0x09 USAGE
Usage (X axis) First field will be X axis
0x30 X
X Axis Usage 0x30 in Generic Desktop = X axis
0x09 USAGE
Usage (Y axis) Second field will be Y axis
0x31 Y
Y Axis Usage 0x31 in Generic Desktop = Y axis
0x09 USAGE
Usage (Z axis) Third field will be Z axis
0x32 Z
Z Axis Usage 0x32 in Generic Desktop = Z axis
0x16 LOG MIN
Logical Minimum Minimum value (2 bytes follow)
0x00 LOW
-32768 Low Byte 0x8000 little-endian = -32768
0x80 HIGH
-32768 High Byte Min = -32768
0x26 LOG MAX
Logical Maximum Maximum value (2 bytes follow)
0xFF LOW
32767 Low Byte 0x7FFF little-endian = 32767
0x7F HIGH
32767 High Byte Max = 32767
0x75 RPT SIZE
Report Size Bits per field
0x10 16 bits
16 Bits Per Field Each axis = 16 bits = 2 bytes
0x95 RPT CNT
Report Count Number of fields
0x03 3 fields
3 Fields 3 axes × 16 bits = 48 bits = 6 bytes total
0x81 INPUT
Input (Main) Declares input fields using all previous settings
0x02 VAR
Variable, Absolute Data is variable and absolute (not relative)
0xC0 END
End Collection Closes the application collection
📦 Generated Report Structure
report_id uint8_t (1 byte)
x int16_t (2 bytes)
y int16_t (2 bytes)
z int16_t (2 bytes)
Total Report Size: 7 bytes
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 bytes

  • 01 = 1 byte

  • 10 = 2 bytes

  • 11 = 4 bytes

Type field:

  • 00 = Main

  • 01 = Global

  • 10 = Local

  • 11 = 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

Device with Sensor Input + LED Output
Input Report ID 1: Temperature sensor (2 bytes)
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:

0x02 = Data, Variable, Absolute
0 0 0 0 0 0 1 0
Bit 0 (0): Data (not Constant)
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

0x01

Generic Desktop

X, Y, Z, Rx, Ry, Rz, Wheel

0x02

Simulation

Steering, Throttle, Brake

0x06

Generic Device

Battery, Wireless Signal

0x07

Keyboard

Key codes, modifiers

0x08

LEDs

Num Lock, Caps Lock, indicators

0x09

Button

Button 1-65535

0x0C

Consumer

Volume, Mute, Play/Pause

0x20

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

  1. Start collection: 0xA1, 0x01

  2. Set Report ID (optional): 0x85, <id>

  3. Set Usage Page: 0x05, <page>

  4. For each field:

    • Set Usage: 0x09, <usage>

    • Set Min/Max: 0x15/0x25 or 0x16/0x26

    • Set Report Size: 0x75, <bits>

    • Set Report Count: 0x95, <count>

    • Add Input/Output: 0x81/0x91, 0x02

  5. Close collection: 0xC0

Type Quick Reference

Data Type

Report Size

Min

Max

uint8

0x75, 0x08

0x15, 0x00

0x25, 0xFF

int8

0x75, 0x08

0x15, 0x80

0x25, 0x7F

uint16

0x75, 0x10

0x16, 0x00, 0x00

0x26, 0xFF, 0xFF

int16

0x75, 0x10

0x16, 0x00, 0x80

0x26, 0xFF, 0x7F

uint32

0x75, 0x20

0x17, ...

0x27, ...

float32

0x75, 0x20

Use physical range

Use physical range

See Also