4、USB描述符体系:设备描述符、配置描述符、接口描述符、端点描述符、字符串描述符
描述符,说白了就是USB设备的「身份证」和「说明书」。
主机枚举设备时,第一步就是通过控制传输把这一堆描述符读回来。读懂了,才知道你插上来的到底是个U盘、鼠标,还是个复合设备。我早年调试一个自定义HID设备时,枚举死活过不去,最后发现是配置描述符里的总长度算错了——少算了两个字节。嗯,从那以后我养成了一个习惯:每次写完描述符数组,先拿计算器加一遍。
4.1 描述符的层级关系
USB描述符是树形结构,一层套一层。主机从根节点(设备描述符)开始读,然后一层层往下走。
核心层级(从上到下):
- 设备描述符 —— 整台设备的全局信息,一个设备只有一个。
- 配置描述符 —— 一种工作模式下的资源分配,一个设备可以有多个配置。
- 接口描述符 —— 某个功能单元的属性,一个配置下可以有多个接口。
- 端点描述符 —— 数据传输的管道属性,一个接口下可以有多个端点。
- 字符串描述符 —— 给人看的文字信息,可选,但建议加上。
我习惯把这种关系想象成「酒店 → 楼层 → 房间 → 房门」。设备是整栋酒店,配置是某一层的布局,接口是具体的房间,端点是房间的门——数据就从这些门里进出。
4.2 设备描述符
设备描述符是主机读到的第一份数据,固定18字节。它告诉主机:你是谁家的芯片?遵循哪个USB版本?有多少种配置?
| 偏移 | 字段 | 长度(字节) | 说明 |
|---|---|---|---|
| 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-8 | idVendor | 2 | 厂商ID (VID),USB-IF分配 |
| 9-10 | idProduct | 2 | 产品ID (PID),厂商自定义 |
| 11-12 | bcdDevice | 2 | 设备版本号,BCD码 |
| 13 | iManufacturer | 1 | 厂商字符串描述符索引 |
| 14 | iProduct | 1 | 产品字符串描述符索引 |
| 15 | iSerialNumber | 1 | 序列号字符串描述符索引 |
| 16 | bNumConfigurations | 1 | 支持的配置数量 |
避坑指南:我曾经遇到一个坑——bcdUSB字段填了0x0110(USB 1.1),结果主机以全速12Mbps枚举,但我的硬件其实只支持全速。后来改成0x0200,主机才正确识别为全速设备。记住:bcdUSB填的是你希望主机用什么协议版本跟你通信,不是你的硬件版本。
4.3 配置描述符
配置描述符固定9字节,但它后面会跟着一串子描述符(接口、端点、甚至其他)。主机通过一次控制传输,把整个配置描述符集合(配置描述符 + 所有子描述符)一口气读走。
这里有个关键字段:wTotalLength。它表示整个配置描述符集合的总字节数。我见过不少新手在这里栽跟头——只算了配置描述符本身的9字节,忘了加上接口和端点的长度。结果主机读到一半发现长度对不上,直接复位设备。
| 偏移 | 字段 | 长度(字节) | 说明 |
|---|---|---|---|
| 0 | bLength | 1 | 固定0x09 |
| 1 | bDescriptorType | 1 | 配置描述符类型码0x02 |
| 2-3 | wTotalLength | 2 | 整个配置描述符集合的总长度 |
| 4 | bNumInterfaces | 1 | 该配置下的接口数量 |
| 5 | bConfigurationValue | 1 | 主机用这个值来选择配置 |
| 6 | iConfiguration | 1 | 配置字符串描述符索引 |
| 7 | bmAttributes | 1 | 电源属性:D7=总线供电,D6=自供电,D5=远程唤醒 |
| 8 | bMaxPower | 1 | 最大电流,单位2mA,如0x32表示100mA |
注意:bmAttributes的D7位必须为1。如果写成0,主机直接拒绝该配置。我调试一个自供电设备时就犯过这个错——D7没置位,Windows弹了个「未知USB设备」的提示,折腾了半天才发现是这一个比特的问题。
4.4 接口描述符
接口描述符固定9字节,描述一个功能单元的属性。一个配置下可以有多个接口,比如一个USB摄像头可以有视频接口和音频接口。
接口描述符里有个字段叫bInterfaceNumber,这是接口的编号。另一个字段bAlternateSetting用于切换接口的备用设置。我做过一个音频设备,用备用设置切换采样率——主机发一个Set Interface请求,接口描述符里的端点描述符就跟着变了。
| 偏移 | 字段 | 长度(字节) | 说明 |
|---|---|---|---|
| 0 | bLength | 1 | 固定0x09 |
| 1 | bDescriptorType | 1 | 接口描述符类型码0x04 |
| 2 | bInterfaceNumber | 1 | 接口编号,从0开始 |
| 3 | bAlternateSetting | 1 | 备用设置编号 |
| 4 | bNumEndpoints | 1 | 该接口使用的端点数量(不包括端点0) |
| 5 | bInterfaceClass | 1 | 接口类代码,如HID为0x03 |
| 6 | bInterfaceSubClass | 1 | 接口子类代码 |
| 7 | bInterfaceProtocol | 1 | 接口协议代码 |
| 8 | iInterface | 1 | 接口字符串描述符索引 |
4.5 端点描述符
端点描述符固定7字节,描述一个端点的传输属性。端点0(控制端点)没有描述符,其他端点都需要。
端点描述符里最重要的字段是bEndpointAddress和bmAttributes。前者决定方向(IN还是OUT)和端点号,后者决定传输类型(控制、批量、中断、同步)。
| 偏移 | 字段 | 长度(字节) | 说明 |
|---|---|---|---|
| 0 | bLength | 1 | 固定0x07 |
| 1 | bDescriptorType | 1 | 端点描述符类型码0x05 |
| 2 | bEndpointAddress | 1 | D7: 方向(0=OUT,1=IN),D3-D0: 端点号 |
| 3 | bmAttributes | 1 | D1-D0: 传输类型(00=控制,01=等时,10=批量,11=中断) |
| 4-5 | wMaxPacketSize | 2 | 最大包大小,全速批量端点最大64字节 |
| 6 | bInterval | 1 | 中断/等时端点的轮询间隔,单位ms或125μs |
实战经验:wMaxPacketSize这个字段,全速批量端点最大只能填64,高速批量端点可以填512。我见过有人给全速设备填了128,结果主机直接忽略这个端点,数据发不出去也收不回来。另外bInterval对于中断端点,全速设备范围是1~255ms,高速设备是1~16(单位125μs)。
4.6 字符串描述符
字符串描述符是可选的,但建议加上。没有字符串描述符,设备管理器里显示的就是「未知USB设备」,有了之后才能看到厂商名、产品名、序列号。
字符串描述符的格式比较特殊:第一个字节是长度,第二个字节是类型码0x03,后面是Unicode(UTF-16LE)编码的字符串。注意,不是ASCII,是Unicode。每个字符占两个字节。
// 字符串描述符示例:厂商名 "MyUSB"
// 语言ID: 0x0409 (英语-美国)
// 字符串: 'M'=0x004D, 'y'=0x0079, 'U'=0x0055, 'S'=0x0053, 'B'=0x0042
// 语言ID描述符 (索引0)
const uint8_t LangID[] = {
0x04, // bLength
0x03, // bDescriptorType
0x09, 0x04 // wLANGID: 英语-美国
};
// 厂商字符串描述符 (索引1)
const uint8_t Manufacturer[] = {
0x0C, // bLength: 12字节 (2字节头 + 5个字符×2字节)
0x03, // bDescriptorType
'M', 0x00, // M
'y', 0x00, // y
'U', 0x00, // U
'S', 0x00, // S
'B', 0x00 // B
};
小技巧:字符串描述符索引0是语言ID描述符,主机必须先读索引0获取支持的语言列表,再根据语言ID读具体的字符串。我习惯只支持英语-美国(0x0409),省事。如果你要支持多语言,记得在语言ID描述符里列出所有支持的LANGID。
4.7 描述符的组装顺序
主机通过Get Descriptor请求读取描述符时,返回的数据是连续排列的。配置描述符在前,后面紧跟着该配置下的所有接口描述符,每个接口描述符后面跟着该接口的所有端点描述符。
举个例子,一个简单的HID设备(1个接口,1个中断IN端点)的描述符排列顺序:
- 配置描述符 (9字节)
- 接口描述符 (9字节)
- HID描述符 (9字节,HID类特有,不是标准USB描述符)
- 端点描述符 (7字节,中断IN端点)
总长度 = 9 + 9 + 9 + 7 = 34字节。这个值要填在配置描述符的wTotalLength字段里。
常见错误:描述符的顺序不能乱。主机解析时是按类型码来识别的,但顺序必须符合规范:配置 → 接口 → 端点。如果把端点描述符放在接口描述符前面,主机解析到端点描述符时还不知道它属于哪个接口,直接报错。我调试复合设备时遇到过这个问题——两个接口的描述符交叉排列,结果主机只认了第一个接口。
4.8 描述符的调试方法
枚举失败时,怎么查描述符的问题?我一般用这几招:
- USB分析仪:抓取主机发起的Get Descriptor请求和设备返回的数据,逐字节核对。这是最可靠的方法。
- 逻辑分析仪:没有分析仪时,用逻辑分析仪抓D+和D-上的数据,虽然解析起来费劲,但能看出主机是否收到了NAK或STALL。
- 软件工具:Windows下用USBView或Bus Hound,Linux下用lsusb -v,可以直接看到主机解析后的描述符内容。
- 打印调试:在固件里加打印,每次发送描述符数据前把长度和内容打印出来,跟预期值对比。
我个人最常用的是USB分析仪加USBView组合。分析仪看底层数据对不对,USBView看主机解析结果对不对。两相对比,问题出在哪一层一目了然。
嗯,描述符这部分就讲到这里。记住一句话:描述符是主机和设备的沟通语言,写错一个字节,沟通就断了。写描述符时多检查几遍wTotalLength和bLength,这两个字段是枚举成败的关键。