What Is a USB Descriptor Overview?

The hierarchical structure of USB descriptors: Device, Configuration, Interface, Endpoint - wTotalLength, bDescriptorType, and what commonly breaks enumeration.

Updated 5 min read
Đọc bằng English Tiếng Việt
USB cover

A descriptor is a set of structs a device uses to introduce itself to the host during enumeration. The host knows nothing about a device before reading its descriptors. Class, speed, endpoint count, transfer types, all of it comes from here.

On a first read of the spec, these descriptors look like a bunch of disconnected structs. But while debugging, it’s more useful to think of them as a tree: get a parent node wrong, and the host usually can’t make it to the child nodes at all.

1. The hierarchy

Device Descriptor (1)
└── Configuration Descriptor (1..n)
    └── Interface Descriptor (1..n)
        ├── Class-specific Descriptor (HID, CDC...)
        └── Endpoint Descriptor (1..n)

The Device Descriptor describes the device at the highest level: USB version, VID, PID, class, and number of configurations.

The Configuration Descriptor describes one operating mode for the device. Most devices only have 1 configuration. It carries wTotalLength, the total byte size of the entire configuration block, including every Interface and Endpoint Descriptor inside it.

The Interface Descriptor describes one “function” within a configuration. A composite device (CDC + HID, for example) has 2 interfaces. Each interface has its own class, subclass, and protocol.

The Endpoint Descriptor describes a single data channel belonging to an interface: the endpoint address, transfer type, max packet size, and polling interval.

2. The first two bytes of every descriptor

Every USB descriptor starts with the same two bytes:

Offset 0: bLength         - the total byte size of this descriptor
Offset 1: bDescriptorType - what kind of descriptor this is
bDescriptorType:
  0x01 = Device
  0x02 = Configuration
  0x03 = String
  0x04 = Interface
  0x05 = Endpoint
  0x21 = HID
  0x22 = HID Report Descriptor

The host uses bLength to know how many bytes to read for this descriptor, then bDescriptorType to know how to parse it. Get either one wrong, and the host can’t correctly parse anything after it.

3. wTotalLength: the field most likely to break things

Among the Configuration Descriptor’s fields, wTotalLength is a small field that causes far more trouble than its size suggests.

wTotalLength is the total byte count of the entire configuration block, including the Configuration Descriptor itself and every Interface, Endpoint, and class-specific descriptor inside it.

wTotalLength = sizeof(Configuration Descriptor)   9 bytes
             + sizeof(Interface Descriptor)        9 bytes
             + sizeof(HID Descriptor)              9 bytes
             + sizeof(Endpoint Descriptor)         7 bytes
             ─────────────────────────────────────────────
                                                  34 bytes

4. What order does the host read descriptors in?

The exact order can vary by host, but here’s the common flow:

1. GET_DESCRIPTOR(Device, 8 bytes)        learns bMaxPacketSize0
2. GET_DESCRIPTOR(Device, full)           VID, PID, bNumConfigurations
3. GET_DESCRIPTOR(Configuration, 9 bytes) learns wTotalLength
4. GET_DESCRIPTOR(Configuration, full)    the entire config block
5. GET_DESCRIPTOR(String, 0)              the language ID list
6. GET_DESCRIPTOR(String, n)              Manufacturer, Product, Serial...

The host typically reads the first 9 bytes of the Configuration Descriptor first, just to learn wTotalLength, and only then requests the full configuration block. By the end of step 4, the host has everything it needs to load a driver.

5. Class-specific descriptors

Some USB classes add their own descriptor between the Interface and Endpoint Descriptors. HID adds a 9-byte HID Descriptor carrying information about the HID Report Descriptor:

Interface Descriptor  (bInterfaceClass = 0x03 = HID)
├── HID Descriptor    (bDescriptorType = 0x21)
└── Endpoint Descriptor IN Interrupt

For HID, the host also reads the HID Report Descriptor separately, via GET_DESCRIPTOR with descriptor type 0x22 on the HID interface. This is a descriptor defined by the HID class itself, and doesn’t live directly inside the configuration block.

6. Common mistakes

Common descriptor mistakes and their consequences
Item Value Note
Wrong wTotalLength The host reads too little or too much, endpoints go unrecognized
Wrong bLength in a single descriptor The host misparses everything after it
bNumEndpoints doesn't match the actual Endpoint Descriptors Some endpoints get skipped
Duplicate endpoint address/direction Transfers fail with no clear reason
Missing or misplaced HID Descriptor The host never loads the HID driver

When debugging descriptors, it’s worth remembering that this is a small field with an outsized effect on how the host understands everything that follows it.

Public references

Notes from a real project

  • CubeMX generates a default mouse descriptor even when HID Keyboard is selected. Always verify with USBView after enumeration, not by reading the source.
  • wTotalLength has to be recalculated by hand any time an interface is added. A static_assert comparing it against sizeof(array) catches this at build time.
  • A composite device needs bDeviceClass=0xEF/0x02/0x01 when using an IAD. Without this triplet, USBView reports *!*ERROR: device class should be Multi-interface Function 0xEF.

See more detail: post 3, USB Descriptor

Found this article useful?

Share, give feedback, or support if you find this content valuable.

Feedback

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.

Biến note thành bài viết hoàn chỉnh

Notes là nơi ghi nhanh khái niệm.