第三章 USB描述符体系:设备、配置、接口、端点、字符串
描述符,说白了就是USB设备的「身份证」加「简历」。主机插上你的设备后,第一件事就是通过控制传输把这一堆描述符读走。读对了,设备正常枚举;读错了,直接不认——我见过太多项目卡在这一步。
USB描述符是分层的,像一棵树。根是设备描述符,往下是配置描述符,再往下是接口描述符,最底层是端点描述符。字符串描述符比较特殊,它是可选的,用来给人看一些友好名字。
3.1 设备描述符(Device Descriptor)
设备描述符是整个描述符树的根。主机枚举时,第一个控制请求就是获取设备描述符(GET_DESCRIPTOR(DEVICE))。它只有18个字节,但决定了设备的基本身份。
| 偏移 | 字段 | 长度(字节) | 说明 |
|---|---|---|---|
| 0 | bLength | 1 | 描述符长度,固定0x12(18) |
| 1 | bDescriptorType | 1 | 设备描述符类型码,固定0x01 |
| 2-3 | bcdUSB | 2 | USB规范版本号,BCD码,如0x0200表示USB 2.0 |
| 4 | bDeviceClass | 1 | 设备类代码,0x00表示在接口描述符中定义 |
| 5 | bDeviceSubClass | 1 | 设备子类代码 |
| 6 | bDeviceProtocol | 1 | 设备协议代码 |
| 7 | bMaxPacketSize0 | 1 | 端点0的最大包大小,USB 2.0下通常是64 |
| 8-9 | idVendor | 2 | 厂商ID(VID),由USB-IF分配 |
| 10-11 | idProduct | 2 | 产品ID(PID),厂商自行定义 |
| 12-13 | bcdDevice | 2 | 设备版本号,BCD码 |
| 14 | iManufacturer | 1 | 厂商字符串描述符的索引 |
| 15 | iProduct | 1 | 产品字符串描述符的索引 |
| 16 | iSerialNumber | 1 | 序列号字符串描述符的索引 |
| 17 | bNumConfigurations | 1 | 支持的配置数量,通常为1 |
3.2 配置描述符(Configuration Descriptor)
一个设备可以有多个配置,但同一时间只能激活一个。配置描述符告诉主机:这个配置下有多少个接口、需要多少电流、是否支持远程唤醒等。
配置描述符本身只有9个字节,但它后面会跟着一串接口描述符和端点描述符。主机读配置描述符时,实际上是读整个配置描述符集合——包括所有子描述符。
| 偏移 | 字段 | 长度 | 说明 |
|---|---|---|---|
| 0 | bLength | 1 | 固定0x09 |
| 1 | bDescriptorType | 1 | 配置描述符类型码,固定0x02 |
| 2-3 | wTotalLength | 2 | 整个配置描述符集合的总长度(字节) |
| 4 | bNumInterfaces | 1 | 该配置下的接口数量 |
| 5 | bConfigurationValue | 1 | 配置编号,用于SET_CONFIGURATION请求 |
| 6 | iConfiguration | 1 | 配置字符串描述符索引 |
| 7 | bmAttributes | 1 | 配置特性:D7=总线供电,D6=自供电,D5=远程唤醒 |
| 8 | bMaxPower | 1 | 最大电流,单位2mA,如0x32表示100mA |
3.3 接口描述符(Interface Descriptor)
接口描述符定义了一个功能单元。比如一个U盘可能有「Mass Storage」接口和一个「Human Interface Device」接口。每个接口可以包含多个端点(端点0除外)。
接口描述符也是9个字节。注意:一个配置下可以有多个接口,一个接口也可以有多个备用设置(Alternate Setting)。
| 偏移 | 字段 | 长度 | 说明 |
|---|---|---|---|
| 0 | bLength | 1 | 固定0x09 |
| 1 | bDescriptorType | 1 | 接口描述符类型码,固定0x04 |
| 2 | bInterfaceNumber | 1 | 接口编号,从0开始 |
| 3 | bAlternateSetting | 1 | 备用设置编号 |
| 4 | bNumEndpoints | 1 | 该接口使用的端点数量(不包括端点0) |
| 5 | bInterfaceClass | 1 | 接口类代码,如0x08表示Mass Storage |
| 6 | bInterfaceSubClass | 1 | 接口子类代码 |
| 7 | bInterfaceProtocol | 1 | 接口协议代码 |
| 8 | iInterface | 1 | 接口字符串描述符索引 |
3.4 端点描述符(Endpoint Descriptor)
端点是USB通信的实体。每个端点描述符7个字节,定义了端点的地址、方向、传输类型和最大包大小。
| 偏移 | 字段 | 长度 | 说明 |
|---|---|---|---|
| 0 | bLength | 1 | 固定0x07 |
| 1 | bDescriptorType | 1 | 端点描述符类型码,固定0x05 |
| 2 | bEndpointAddress | 1 | 端点地址:D7=方向(0 OUT, 1 IN),D3-D0=端点号 |
| 3 | bmAttributes | 1 | 传输类型:0=控制,1=等时,2=批量,3=中断 |
| 4-5 | wMaxPacketSize | 2 | 最大包大小,USB 2.0高速批量端点固定512 |
| 6 | bInterval | 1 | 轮询间隔,中断/等时端点有效,单位ms或125μs |
3.5 字符串描述符(String Descriptor)
字符串描述符是可选的,但建议加上。它让设备在主机上显示一个友好的名字,比如「USB Flash Drive」而不是一串VID/PID。
字符串描述符的索引0比较特殊——它返回的是语言ID列表。比如0x0409表示美式英语。主机先读索引0拿到支持的语言列表,再用对应的语言ID去读具体的字符串。
// 字符串描述符示例:语言ID列表(索引0)
// 0x04, 0x03, 0x09, 0x04
// 长度=4,类型=0x03(字符串),语言ID=0x0409(美式英语)
// 字符串描述符示例:产品名称(索引2)
// 0x16, 0x03, 'U', 0x00, 'S', 0x00, 'B', 0x00, ' ', 0x00,
// 'K', 0x00, 'e', 0x00, 'y', 0x00, ' ', 0x00, '1', 0x00, '.', 0x00, '0', 0x00
// 长度=0x16(22),类型=0x03,内容="USB Key 1.0"(Unicode UTF-16LE)
3.6 描述符层级关系与枚举流程
下面这张图展示了描述符的树形结构,以及主机枚举时的读取顺序。你想想看,主机其实就像个侦探,一层一层往下挖。
3.7 描述符常见错误与调试方法
枚举失败,十有八九是描述符出了问题。我总结了几类高频错误:
- 长度错误: bLength写错,比如端点描述符写成8字节(应该是7)。主机解析时直接偏移错位。
- 类型码错误: bDescriptorType写错,比如配置描述符的类型码应该是0x02,写成0x01就变成设备描述符了。
- wTotalLength不匹配: 实际描述符总长度和声明的长度不一致。主机读到一半发现数据不够或超出,直接放弃。
- 端点地址冲突: 两个端点用了相同的地址和方向。比如两个IN端点都叫0x81。
- bInterval超出范围: 中断端点的轮询间隔,全速设备是1-255ms,高速设备是1-16(单位125μs)。写错了主机可能不认。