4. USB描述符体系:五层结构,层层递进
USB描述符这玩意儿,说白了就是给主机递的“自我介绍”。设备插上后,主机啥也不知道,得靠这些结构体一步步问清楚:你是谁?你干嘛用?你怎么干活?
我自己刚接触USB协议时,总觉得描述符就是些固定表格。直到有一次调试一个复合设备,主机怎么都枚举失败,我才发现——配置描述符里一个字节写错了,整条链路就断掉。从那以后,我养成了逐字节核对描述符的习惯。
4.1 先看整体架构
USB描述符体系是树形的。根节点是设备描述符,往下分支到配置描述符,再往下是接口描述符,最下面挂着端点描述符。字符串描述符像个“共享资料库”,谁都能引用。
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 | 配置描述符数量 |
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
4.4 接口描述符——功能的分界线
接口描述符定义了设备的一个功能块。比如一个U盘有一个接口(Mass Storage),一个复合设备可能有多个接口(HID + Audio)。
主机通过接口描述符知道:这个接口用什么类、什么协议、需不需要驱动。
字段解读:
- bLength: 9字节
- bDescriptorType: 0x04
- bInterfaceNumber: 接口编号,从0开始
- bAlternateSetting: 备用设置,用于切换配置细节
- bNumEndpoints: 该接口使用的端点数量(不含端点0)
- bInterfaceClass: 接口类,如0x03=HID,0x08=Mass Storage
- bInterfaceSubClass: 子类
- bInterfaceProtocol: 协议
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盘、串口)
- 中断端点: 小数据量、有实时要求(键盘、鼠标)
- 同步端点: 实时流数据(音频、视频)
4.6 字符串描述符——给人看的名字
字符串描述符不是必须的,但它决定了用户看到什么。比如设备管理器里显示的产品名称、厂商信息。
字符串描述符的结构很灵活:
- bLength: 不固定,由字符串长度决定
- bDescriptorType: 0x03
- wLANGID[0]: 语言ID,如0x0409(英语)
- bString: Unicode编码的字符串
主机怎么获取字符串?它会先请求一个0号字符串描述符,里面放的是支持的语言列表。然后根据语言ID请求具体的字符串。
4.7 描述符的排列顺序——必须严格
主机希望看到这样的顺序:
- 设备描述符(唯一)
- 配置描述符(第一个)
- 该配置下所有接口描述符,按编号递增
- 每个接口下的所有端点描述符,按地址递增
- 如果有备用设置,需要重复3-4步骤
- 字符串描述符(在主机主动请求时才返回)
我曾经见过一个设备,把端点描述符放到了所有接口描述符之后。主机解析到一半就卡住了。记住:接口描述符和它的端点描述符必须连续出现。
用代码表示一个描述符集合:
// 一个典型的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枚举的骨架。设备描述符定身份,配置描述符定供电,接口描述符定功能,端点描述符定通道,字符串描述符定名字。每个字节都不能错,不然主机会直接拒绝。
嗯,这个体系清楚了,后面讲故障注入时你就能精准定位——哪个描述符出了错,影响了哪一层,怎么恢复。这个基础扎实了,后面的调试会顺手很多。