理解描述符的层级结构

USB 描述符这个东西,说白了就是设备拿来向主机“自我介绍”的一堆数据结构。主机插上设备后,会像警察查户口一样,一级一级地往下问:你是谁?你有什么功能?每个接口用什么协议?端点怎么传数据?这些问题,全靠描述符来回答。

我个人习惯把描述符比作一本“设备说明书”,第一章是目录(设备描述符),然后是各章节(配置描述符、接口描述符、端点描述符),最后还有附录(HID 报告描述符、字符串描述符等等)。主机读这本说明书的顺序是固定的,不能跳着读。我早年做 USB 复合设备时,就因为描述符层级顺序写错,导致主机只认到一个接口,折腾了两天才发现是设备描述符里配置数量写少了……

核心考点:主机通过 GET_DESCRIPTOR 请求,从设备描述符开始层层向下读取,最终构建出完整的设备拓扑树。

设备描述符 —— 说明书首页

设备描述符是主机问的第一个问题,也就是请求索引为 0 的描述符。它只有 18 个字节,但信息密度极高。你可以把它看成设备的“身份证”:

  • bLength:整个描述符长度,固定 18。
  • bDescriptorType:类型码,设备描述符是 0x01。
  • bcdUSB:USB 规范版本号,比如 0x0200 表示 USB 2.0。
  • bDeviceClass / bDeviceSubClass / bDeviceProtocol:设备大类、子类、协议。如果这三个都是 0,说明功能在接口描述符里定义——也就是复合设备。
  • bMaxPacketSize0:端点 0 的最大包长。这个值如果是 64,那么后续控制传输就能一次发 64 字节。
  • idVendor / idProduct / bcdDevice:厂商 ID、产品 ID、设备版本号。主机靠这个装驱动。
  • iManufacturer / iProduct / iSerialNumber:字符串描述符索引,没有就写 0。
  • bNumConfigurations:配置描述符的数量,通常是 1。如果写 2,主机就要问两次配置描述符。

嗯,这里要注意:bMaxPacketSize0 在 USB 2.0 下必须是 8、16、32、64 中的一个。我遇到过有人写 128,结果主机直接复位设备……

配置描述符 —— 设备的功能模块

读完设备描述符后,主机接着请求配置描述符(索引 0)。配置描述符本身只有 9 个字节,但它后面跟着“配置描述符集合”——也就是接口描述符、端点描述符以及可能的其他描述符。主机先读前 9 字节,根据 wTotalLength 知道整个集合大小,然后一次性读完整包。

配置描述符的关键字段:

字段 意义
bLength 9 字节
bDescriptorType 0x02
wTotalLength 整个配置描述符集合的总字节数
bNumInterfaces 该配置下有多少个接口
bConfigurationValue 选择该配置用的数值(Set Configuration 时用到)
iConfiguration 字符串描述符索引
bmAttributes 供电模式(自供电/总线供电)
bMaxPower 最大电流,每单位 2mA
实战经验:复合设备(比如带 HID 键盘 + CDC 串口)通常只有一个配置,但两个接口。你务必保证 bNumInterfaces 等于实际接口数,否则主机只会枚举到第一个接口。

接口描述符 —— 设备的功能角色

接口描述符是描述符层级的第三层。一个配置下可以有多个接口,每个接口代表一个“功能角色”。比如设备描述符说“我有 1 个配置”,配置描述符说“我有 2 个接口”,那主机就会连续读 2 个接口描述符。

接口描述符结构:

bLength: 9
bDescriptorType: 0x04
bInterfaceNumber: 0    // 接口编号,从0开始
bAlternateSetting: 0    // 备用设置,用于切换不同带宽
bNumEndpoints: 2        // 该接口下端点个数(不包括端点0)
bInterfaceClass: 3      // HID 类
bInterfaceSubClass: 1   // 启动接口
bInterfaceProtocol: 1   // 键盘
iInterface: 0           // 字符串索引

注意 bAlternateSetting 这个字段。我司曾有个摄像头项目,用备用设置切换 480p 和 720p 流。结果备用设置里的端点描述符写错了带宽,导致视频卡顿。

端点描述符 —— 数据的真实通道

每个接口下的端点描述符描述了一个具体的传输管道。主机读接口描述符时,会根据 bNumEndpoints 知道接下来有几个端点描述符要读。端点 0 是默认控制端点,不在描述符里列出。

端点描述符字段:

  1. bLength:7 字节(等时端点 9 字节,多了同步属性)。
  2. bDescriptorType:0x05。
  3. bEndpointAddress:低 4 位是端点号,高位 DIR 位表示方向(1=IN,0=OUT)。
  4. bmAttributes:传输类型 0=控制,1=等时,2=批量,3=中断。
  5. wMaxPacketSize:最大包大小,高速批量端点可达 512。
  6. bInterval:轮询间隔,中断/等时用。
常见坑:我曾经帮客户调试一个 HID 游戏摇杆,主机只收到部分按键数据。查了好久,发现是端点描述符的 wMaxPacketSize 写的 64,但实际上 HID 报告长度只有 8 字节。主机认为每次都能收 64 字节,导致缓冲区错位。正确的做法是 wMaxPacketSize 设成 8,跟报告长度一致。

其他描述符 —— 附属信息

除了上述四类核心描述符,还有字符串描述符、HID 报告描述符、接口关联描述符(IAD)等。它们不是独立的层级,而是依附在配置描述符集合中。比如字符串描述符通过索引被设备描述符和配置描述符引用,主机在需要时才单独请求。

这里我提一句 IAD(Interface Association Descriptor)。做复合设备时,需要把两个接口绑定成一个功能(比如 CDC 虚拟串口需要 2 个接口:控制接口和数据接口)。IAD 告诉主机:“这些接口是同一个设备功能的。” 如果没有 IAD,早期系统可能会把它们当成两个独立设备。

整体层级关系图

下面的 SVG 图展示了主机枚举时读取描述符的层次顺序。你可以从设备描述符开始,沿着箭头往下走:

USB 描述符层级结构(主机读取顺序) 设备描述符 (Device) 配置描述符 (Configuration) 接口描述符 0 (Interface) 可以有多个接口 端点描述符 IN 端点描述符 OUT 读取顺序说明: 1. 主机先读设备描述符(bNumConfigurations 确定配置数) 2. 再读配置描述符集合(包括接口描述符、端点描述符等) 3. 接口描述符告诉主机该接口下有多少个端点 4. 端点描述符描述每个端点的传输特性 字符串描述符、HID报告描述符等作为附属,通过索引和集合嵌套引用

从这张图可以清晰看到:主机不会一下子把所有描述符读完,而是分层请求。每层描述符都包含下一层存在多少个子元素的计数。要是你在设备描述符里写 bNumConfigurations = 2,主机就会重复第二层和第三层的读取过程。

描述符的读取顺序总结

记住这个顺序:设备描述符 → 配置描述符 → (接口描述符 → 端点描述符) × 接口个数。

这里有个容易被忽视的点:配置描述符集合中,接口描述符和端点描述符是连续存放的。举个例子,配置描述符后紧接着第一个接口描述符,然后是它的端点描述符,再是第二个接口描述符,以此类推。你不能把所有的接口描述符写在一起,再统一写端点描述符——那样主机解析时会完全错乱。

我曾经调试过一个 3D 打印机的固件,因为把端点描述符放到了所有接口描述符之后,结果主机枚举时以为第二个接口下没有端点,传输一直超时。老实按照规范顺序重排后,问题就解决了。

最后提一嘴,描述符的 bDescriptorType 字段也很关键。主机通过类型码知道当前读的是什么。如果你在配置描述符集合里混入错误类型码,主机可能会中断枚举或回退。常用的类型码:

类型码 描述符名称
0x01设备描述符
0x02配置描述符
0x04接口描述符
0x05端点描述符
0x03字符串描述符
0x21HID 描述符(位于配置集合中的第一个)

理解了这个层级结构,后续分析具体设备的描述符时就会非常清晰。主机眼里没有“设备”,只有一张张描述符表格。你把这些表格写对,设备就能正常工作;写错了,轻则驱动装不上,重则设备完全不识别。