HID Report Descriptor Debugging for USB Devices

Debug HID report descriptors by comparing declared usages, report IDs, sizes, counts, ranges, bit packing, and actual USB interrupt or control report bytes.

USB, HID, report descriptor, report ID, interrupt endpoint, firmware

A USB HID device can enumerate, bind to the standard host driver, and still deliver shifted axes, missing buttons, impossible negative values, ignored reports, or output that works only on one operating system. Enumeration proves that the host recognized a HID interface. It does not prove that the HID report descriptor and the firmware's report bytes describe the same bit-level contract.

Direct answer: capture the HID descriptor, complete report descriptor, protocol-selection requests, and representative input, output, and feature reports. Parse the report descriptor by report ID, direction, usage, bit offset, size, count, logical range, and flags. Then compare the declared report length and bit layout with every byte the firmware actually sends or accepts. Fix the first mismatch and repeat the same report sequence.

Bus Scope can keep raw descriptor bytes and USB report traffic together. A bus capture shows what crossed USB; it does not show how an application maps a valid OS HID event after the class driver parses it. If USB declarations and payloads agree, continue with host HID APIs and application logs.

Treat the report descriptor as executable data layout

The HID report descriptor is a compact instruction stream. Main items define Input, Output, Feature, Collection, and End Collection behavior. Global items carry state such as Usage Page, Logical Minimum, Logical Maximum, Report Size, Report Count, and Report ID. Local items such as Usage and Usage Minimum/Maximum apply to the next main item.

That stateful format creates common firmware mistakes:

  • a Usage applies to a different main item than intended;
  • Report Size × Report Count does not equal the packed payload;
  • padding bits are omitted from the firmware structure;
  • a Report ID is declared but not sent as the first byte;
  • a Report ID byte is sent even though no IDs are declared;
  • signed samples use a nonnegative logical range;
  • an array is implemented as variable fields or vice versa;
  • the HID descriptor advertises the wrong report-descriptor length;
  • firmware and host tools use different descriptor revisions.

Do not debug those cases from a C structure alone. Compiler padding and bitfield ordering are not the USB descriptor contract. Calculate offsets from the descriptor item stream and compare raw bytes.

Capture the full HID evidence chain

A useful session starts before the host reads the report descriptor and continues through several representative reports:

Evidence Why it matters
Device and configuration descriptors identify the exact device and interface
HID interface descriptor proves class, subclass, protocol, and endpoint ownership
HID descriptor contains report-descriptor type and length
GET_DESCRIPTOR(Report) proves what bytes the host actually received
SET_PROTOCOL or GET_PROTOCOL distinguishes boot and report protocol where supported
SET_IDLE or GET_IDLE explains repeated or suppressed keyboard-style input behavior
Interrupt IN reports show device-to-host payload and timing
Interrupt OUT reports show host-to-device output when an OUT endpoint is used
GET_REPORT and SET_REPORT show control-path input, output, or feature reports

Capture several known states: all controls idle, one known button, one axis near minimum, one near maximum, and combinations that cross byte boundaries. A single random report rarely exposes bit-packing mistakes clearly.

The USB descriptor viewer workflow provides the parent descriptor-validation process.

Build a report-layout table

Turn the descriptor into a table before changing firmware:

Report ID Direction Usage Bit offset Size Count Logical range
1 Input buttons 1–8 0 1 8 0–1
1 Input X axis 8 16 1 -32768–32767
1 Input Y axis 24 16 1 -32768–32767
2 Output status LEDs 0 1 3 0–1
2 Output constant padding 3 5 1 n/a

If any nonzero Report ID exists, the ID byte prefixes that report's data on the wire or control path. It is not part of the bit offsets inside the report payload when engineers document the layout, so state your convention explicitly. Mixing conventions causes off-by-eight-bit arguments.

Calculate the payload length for each ID separately:

payload bytes = ceil(total declared report bits / 8)
wire bytes = payload bytes + report ID byte when IDs are used

A descriptor can define several report IDs with different lengths. Comparing every transfer against one global length creates false failures.

Check Report Size, Report Count, and padding together

Report Size defines bits per field; Report Count defines how many fields the next main item contributes. Eight one-bit buttons consume eight bits. Three one-bit LEDs plus five constant padding bits also consume one byte.

Typical defects include:

  • firmware sends one byte per Boolean although the descriptor packs bits;
  • a 12-bit value is stored in two bytes but the upper four bits are not masked;
  • padding exists in firmware but is not declared as constant;
  • descriptor changes a field from 8 to 16 bits without updating report length;
  • the last field crosses a byte boundary differently in host and device code.

Use known test patterns such as 0x00, 0x01, 0x80, 0xFF, alternating bits, and minimum/maximum axis values. Record both the physical action and raw payload. The first unexpected bit offset points to the relevant descriptor item or packer.

Logical ranges control signedness and validation

The host interprets values using the descriptor's logical minimum and maximum. A negative logical minimum normally indicates signed data. If firmware sends two's-complement negative samples but the descriptor declares 0 to 65535, a negative axis can appear as a large positive value.

Check:

  • logical minimum and maximum encoding width;
  • whether the field's report size can represent that range;
  • unit and unit exponent where physical meaning matters;
  • whether values are clamped before packing;
  • whether a null state or preferred state flag is intentional.

Physical minimum and maximum do not repair a wrong logical representation. They describe physical scaling; the logical range still defines the transmitted integer domain.

Match usages to the intended application collection

Usages tell the host what controls mean. The Usage Page selects a namespace, and Usage values identify controls or collections in that namespace. A syntactically valid descriptor can still use an unsuitable page or place controls under the wrong Application Collection.

Inspect the collection tree:

  1. Which top-level Application Collection owns the report?
  2. Which Usage Page is active at each main item?
  3. Are local usages consumed by the intended Input, Output, or Feature item?
  4. Does a Usage Minimum/Maximum range contain exactly the declared count?
  5. Are vendor-defined usages intentionally vendor-defined?

Do not insert a generic desktop usage merely because a host tool displays it nicely. Select usages that match the device's real function and the current USB-IF usage tables. If no standardized usage fits, document the vendor-defined contract for host software.

Separate variable fields from arrays

The Input, Output, and Feature flags distinguish Data versus Constant and Variable versus Array, among other properties. Those choices change interpretation:

  • Variable fields usually map each usage to a fixed field position, useful for axes or buttons.
  • Array fields usually contain selectors into a usage set, common in keyboard keycode reports.
  • Constant fields are padding or non-data.

A keyboard-style array that firmware fills as a button bitmap will not behave like a bitmap. A button set declared as variable fields but populated with usage IDs will also fail. Decode the main-item flags rather than assuming all reports are raw structures.

Report ID failures have recognizable patterns

Report ID mistakes often produce total failure for one report family while another works:

Symptom Likely check
every field shifted by one byte unexpected or missing ID byte
ID 1 works, ID 2 ignored ID 2 absent from descriptor or wrong length
control SET_REPORT stalls ID/type encoded incorrectly in wValue
interrupt report accepted only without ID descriptor defines no Report ID
feature report length off by one host API includes or separates ID differently

For GET_REPORT and SET_REPORT, decode the report type and ID carried in wValue, the target interface in wIndex, and wLength. Confirm that firmware routes the request to the correct HID interface in a composite device.

Boot protocol and report protocol are different modes

Boot-capable keyboard and mouse interfaces can support a fixed boot report format in addition to their report-descriptor format. The host may select boot protocol with SET_PROTOCOL, particularly in pre-OS environments. If firmware always sends the report-protocol layout, boot consumers may read nonsense. If firmware stays in boot format after the OS selects report protocol, richer reports may fail.

Record:

  • interface subclass and protocol;
  • observed SET_PROTOCOL value;
  • payload layout before and after the request;
  • whether the endpoint report length changes;
  • resume and reset behavior.

Do not mark every HID device as boot-capable. A vendor sensor can use report protocol only and should describe itself accordingly.

Composite HID routing checks

When HID shares a device with CDC, audio, storage, or vendor functions, verify:

  • the HID descriptor follows the correct interface;
  • report-descriptor wIndex targets that interface;
  • interrupt endpoint address belongs to that interface and alternate setting;
  • endpoint addresses do not collide;
  • wTotalLength includes all class-specific descriptors;
  • firmware request dispatch uses the actual interface number.

If adding a new function shifts HID from interface 0 to interface 3, hard-coded endpoint-zero routing can return the wrong descriptor or stall the request. The composite-device binding guide covers function grouping and child-driver evidence.

Timing is separate from descriptor correctness

A correct report layout can still arrive late or disappear. After proving the bytes, examine interrupt polling and firmware queues:

  • bInterval and negotiated speed;
  • missing versus duplicate reports;
  • host polling with no data ready;
  • firmware overwrite of unread reports;
  • long critical sections;
  • suspend, resume, and remote wakeup;
  • transitions between control and interrupt report paths.

The USB interrupt interval guide handles that timing layer. Do not "fix" latency by changing report fields when the report bytes already match.

A repeatable HID debugging workflow

  1. Record firmware revision, VID, PID, serial, OS, port, and device speed.
  2. Capture from connection through report-descriptor retrieval.
  3. Save the exact raw report descriptor and advertised length.
  4. Parse collections, report IDs, usages, flags, sizes, counts, and logical ranges.
  5. Build a bit-offset table for every report ID and direction.
  6. Generate known physical states and record expected values.
  7. Compare actual interrupt and control report lengths.
  8. Mark the first bit or field that differs.
  9. Check protocol mode and composite interface routing.
  10. Fix one descriptor or packing issue at a time.
  11. Repeat the same known-state sequence.
  12. Save before-and-after .bscope sessions for regression review.

Use Bus Scope platform capture to ensure the correct host controller or usbmon bus is recorded.

QA questions

Why does the HID device enumerate but send wrong values?

Enumeration validates enough descriptors to load the HID stack. Wrong values usually come from a mismatch in report ID, size, count, bit offset, signed logical range, usage, or main-item flags between the report descriptor and firmware payload.

Does an interrupt payload include the Report ID?

When the descriptor uses nonzero Report IDs, the selected ID prefixes the report data. Account for that byte when comparing transfer length, and state whether layout offsets in your documentation exclude or include it.

Can a valid report descriptor still be the wrong descriptor?

Yes. Firmware can return an older but syntactically valid descriptor while sending a newer payload layout. Preserve raw bytes, firmware revision, advertised descriptor length, and actual reports in the same capture.

Should HID output always use an interrupt OUT endpoint?

No. Output and feature reports may use class control requests on endpoint zero; some devices also expose an interrupt OUT endpoint. Verify the interface contract and the path the host actually uses.

Can Bus Scope prove the application mapped a usage correctly?

It can prove the descriptors, control requests, endpoint traffic, timing, and raw report bytes visible on USB. If those agree, inspect the host HID API and application mapping rather than attributing the issue to the bus.

Final diagnosis

HID report descriptor debugging becomes deterministic when every observed byte has a declared report ID, direction, usage, bit offset, size, and logical meaning. Build that map, compare known physical states, and fix the first contract mismatch. Once the descriptor and payload agree, move deliberately to timing or application interpretation instead of continuing to rewrite the descriptor.

<!-- multilingual-related-reading:start -->

Related guides

Continue with the same-language pages below. They cover adjacent stages without changing the canonical owner of this topic:

<!-- multilingual-related-reading:end -->