4. USB描述符体系:五层结构,层层递进

USB描述符这玩意儿,说白了就是给主机递的“自我介绍”。设备插上后,主机啥也不知道,得靠这些结构体一步步问清楚:你是谁?你干嘛用?你怎么干活?

我自己刚接触USB协议时,总觉得描述符就是些固定表格。直到有一次调试一个复合设备,主机怎么都枚举失败,我才发现——配置描述符里一个字节写错了,整条链路就断掉。从那以后,我养成了逐字节核对描述符的习惯。

4.1 先看整体架构

USB描述符体系是树形的。根节点是设备描述符,往下分支到配置描述符,再往下是接口描述符,最下面挂着端点描述符。字符串描述符像个“共享资料库”,谁都能引用。

关键点: 主机枚举时,会依次请求设备描述符 → 配置描述符(含所有接口和端点) → 字符串描述符。顺序不能乱,一次漏掉整个枚举就挂了。
USB描述符树形体系 设备描述符 根节点,描述设备整体 配置描述符 供电模式、接口数量 接口描述符 功能类、协议、子类 端点描述符 0 控制端点(默认) 端点描述符 1 批量/中断/同步 字符串描述符 共享的文本信息 注意:设备描述符只能有一个,配置描述符可以有多个,接口描述符和端点描述符可以嵌套重复。 字符串描述符通过索引引用,不是必需的。
我的习惯: 做描述符配置时,我总先画个树状图,标清每个节点的数量。比如复合设备可能有2个配置、4个接口、8个端点。画清楚了再填代码,少走弯路。

4.2 设备描述符——你的门面

设备描述符只有1个,是主机问的第一句话。长度固定18字节,结构如下:

偏移 字段 长度 含义
0 bLength 1 本描述符长度,固定0x12
1 bDescriptorType 1 类型码,设备描述符固定0x01
2 bcdUSB 2 USB规范版本号,如0x0200(2.0)
4 bDeviceClass 1 设备类(0x00表示在接口描述符定义)
5 bDeviceSubClass 1 子类码
6 bDeviceProtocol 1 协议码
7 bMaxPacketSize0 1 端点0最大包大小,如0x40(64字节)
8 idVendor 2 厂商ID(VID)
10 idProduct 2 产品ID(PID)
12 bcdDevice 2 设备版本号
14 iManufacturer 1 厂商字符串索引
15 iProduct 1 产品字符串索引
16 iSerialNumber 1 序列号字符串索引
17 bNumConfigurations 1 配置描述符数量
我曾经: 在bMaxPacketSize0字段上吃过亏。USB 2.0高速设备必须填64,我填了8,结果主机协商速率时直接报错。折腾了两天才找到原因。

4.3 配置描述符——设备的供电方案

配置描述符描述了一组接口的供电和功耗信息。为什么要有多个配置?你想想看——USB 2.0允许设备在不同电流模式下切换,比如全速模式功耗低,高速模式功耗高。

举个例子:我的一个项目里,设备插USB 2.0口时用500mA配置,插USB 3.0口时功耗更高,就换另一个900mA配置。主机根据当前总线供电能力选配置。

配置描述符的核心字段:

  • bLength: 固定9字节
  • bDescriptorType: 0x02
  • wTotalLength: 整个配置描述符集合的总长度(包括所有接口和端点)
  • bNumInterfaces: 该配置下的接口数量
  • bConfigurationValue: 配置编号,主机用SetConfiguration选择
  • bmAttributes: 供电属性,bit7=总线供电,bit6=自供电
  • bMaxPower: 最大电流,单位2mA
注意: wTotalLength这个字段很容易出错。你得把配置描述符、所有接口描述符、所有端点描述符的长度都加起来。我习惯先算好总数再填,或者用工具自动生成。

4.4 接口描述符——功能的分界线

接口描述符定义了设备的一个功能块。比如一个U盘有一个接口(Mass Storage),一个复合设备可能有多个接口(HID + Audio)。

主机通过接口描述符知道:这个接口用什么类、什么协议、需不需要驱动。

字段解读:

  • bLength: 9字节
  • bDescriptorType: 0x04
  • bInterfaceNumber: 接口编号,从0开始
  • bAlternateSetting: 备用设置,用于切换配置细节
  • bNumEndpoints: 该接口使用的端点数量(不含端点0)
  • bInterfaceClass: 接口类,如0x03=HID,0x08=Mass Storage
  • bInterfaceSubClass: 子类
  • bInterfaceProtocol: 协议
避坑指南: bAlternateSetting这个字段常被忽略。我有个项目里同一个接口用了两个备用设置,一个低速模式只开中断端点,一个高速模式开批量端点。主机用SetInterface切换。如果不理解这个字段,你会发现设备只能工作在一种模式。

4.5 端点描述符——数据传输的通道

端点描述符定义了数据怎么进出设备。每个端点都有自己的“身份证”,告诉主机:方向、类型、最大包大小、轮询间隔。

这里有个小陷阱:端点0是控制端点,不包含在端点描述符里。bNumEndpoints里提到的端点才是1~15号。

字段一览:

偏移 字段 含义
0 bLength 7字节
1 bDescriptorType 0x05
2 bEndpointAddress bit7: 方向(0=OUT,1=IN),bit3-0: 端点号
3 bmAttributes 传输类型:0=控制,1=同步,2=批量,3=中断
4 wMaxPacketSize 最大包大小,高速批量端点固定512字节
6 bInterval 轮询间隔,中断端点用,单位ms或125us

端点类型怎么选?我总结一个经验:

  • 批量端点: 大数据量、无实时要求(U盘、串口)
  • 中断端点: 小数据量、有实时要求(键盘、鼠标)
  • 同步端点: 实时流数据(音频、视频)
我曾经: 在一个HID设备里,中断端点的bInterval填了0。主机直接认为无效,设备无法枚举。USB规范要求中断端点bInterval最小1(全速)或1ms对应值。这个坑新手常踩。

4.6 字符串描述符——给人看的名字

字符串描述符不是必须的,但它决定了用户看到什么。比如设备管理器里显示的产品名称、厂商信息。

字符串描述符的结构很灵活:

  • bLength: 不固定,由字符串长度决定
  • bDescriptorType: 0x03
  • wLANGID[0]: 语言ID,如0x0409(英语)
  • bString: Unicode编码的字符串

主机怎么获取字符串?它会先请求一个0号字符串描述符,里面放的是支持的语言列表。然后根据语言ID请求具体的字符串。

我的建议: 即使设备不需要界面展示,也至少提供厂商和产品字符串。调试时用USB分析仪看描述符,看到一堆0x00很头疼的。给字符串,方便自己也方便别人。

4.7 描述符的排列顺序——必须严格

主机希望看到这样的顺序:

  1. 设备描述符(唯一)
  2. 配置描述符(第一个)
  3. 该配置下所有接口描述符,按编号递增
  4. 每个接口下的所有端点描述符,按地址递增
  5. 如果有备用设置,需要重复3-4步骤
  6. 字符串描述符(在主机主动请求时才返回)

我曾经见过一个设备,把端点描述符放到了所有接口描述符之后。主机解析到一半就卡住了。记住:接口描述符和它的端点描述符必须连续出现。

用代码表示一个描述符集合:

// 一个典型的USB描述符集合(HID键盘)
uint8_t config_descriptor[] = {
    // 配置描述符
    0x09, 0x02, 0x22, 0x00, 0x01, 0x01, 0x00, 0x80, 0x32,
    // 接口描述符
    0x09, 0x04, 0x00, 0x00, 0x01, 0x03, 0x01, 0x01, 0x00,
    // HID描述符(中间描述符)
    0x09, 0x21, 0x11, 0x01, 0x00, 0x01, 0x22, 0x41, 0x00,
    // 端点描述符(中断IN)
    0x07, 0x05, 0x81, 0x03, 0x08, 0x00, 0x10
};

整个章节总结一下:描述符体系是USB枚举的骨架。设备描述符定身份,配置描述符定供电,接口描述符定功能,端点描述符定通道,字符串描述符定名字。每个字节都不能错,不然主机会直接拒绝。

嗯,这个体系清楚了,后面讲故障注入时你就能精准定位——哪个描述符出了错,影响了哪一层,怎么恢复。这个基础扎实了,后面的调试会顺手很多。