第三章 USB描述符体系:设备、配置、接口、端点、字符串

描述符,说白了就是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
我的经验: bMaxPacketSize0这个字段特别容易踩坑。USB 2.0全速设备必须是8、16、32、64之一,高速设备固定64。我曾经把一个全速设备的这个字段写成32,结果主机枚举到一半就断了——因为主机默认用64去读,设备却只能回32,直接超时。

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
注意: wTotalLength这个字段必须精确计算。如果你漏算了一个端点描述符的长度,主机读到的数据就会错位,后面的所有描述符都会解析错误。我调试时习惯先手算一遍总长度,再和代码里生成的值比对。

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 接口字符串描述符索引
避坑指南: 我曾经做过一个复合设备,有两个接口。结果bInterfaceNumber写成了0和1,但bNumEndpoints没算对——第一个接口写了2个端点,第二个接口写了0个端点。主机枚举时直接报错「配置描述符无效」。后来发现是第二个接口的端点描述符忘记加了。

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
关键点: bEndpointAddress的D7位决定了方向。0x81表示端点1 IN(设备到主机),0x01表示端点1 OUT(主机到设备)。我见过有人把IN和OUT搞反,结果主机发数据设备收不到,设备发数据主机也不认。

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)
我的习惯: 字符串描述符用UTF-16LE编码,每个字符占2字节。我一般把厂商名放在索引1,产品名放在索引2,序列号放在索引3。这样主机读起来清晰,调试时也容易定位。

3.6 描述符层级关系与枚举流程

下面这张图展示了描述符的树形结构,以及主机枚举时的读取顺序。你想想看,主机其实就像个侦探,一层一层往下挖。

USB描述符层级结构与枚举流程 USB描述符层级结构与枚举流程 设备描述符 配置描述符(含wTotalLength) 接口描述符 0 接口描述符 1 端点1 IN (0x81) 端点2 OUT (0x02) 端点1 IN (0x81) 端点3 OUT (0x03) 字符串描述符(可选) 枚举顺序:主机先读设备描述符 → 再读配置描述符集合 → 解析出接口和端点 字符串描述符在需要时单独请求读取

3.7 描述符常见错误与调试方法

枚举失败,十有八九是描述符出了问题。我总结了几类高频错误:

  • 长度错误: bLength写错,比如端点描述符写成8字节(应该是7)。主机解析时直接偏移错位。
  • 类型码错误: bDescriptorType写错,比如配置描述符的类型码应该是0x02,写成0x01就变成设备描述符了。
  • wTotalLength不匹配: 实际描述符总长度和声明的长度不一致。主机读到一半发现数据不够或超出,直接放弃。
  • 端点地址冲突: 两个端点用了相同的地址和方向。比如两个IN端点都叫0x81。
  • bInterval超出范围: 中断端点的轮询间隔,全速设备是1-255ms,高速设备是1-16(单位125μs)。写错了主机可能不认。
调试建议: 用USB分析仪(比如Beagle USB或Ellisys)抓取枚举阶段的控制传输。看主机发了什么请求,设备回了什么数据。我一般先看设备描述符的前两个字节——长度和类型,如果这两个对了,后面才有戏。