USB描述符基础:设备描述符、配置描述符、接口描述符、端点描述符
做USB一致性测试这么些年,我最常被问到的一个问题就是:“枚举到底是怎么跑起来的?”你想想看,一个USB设备插上主机,什么驱动都没装,主机凭啥就知道你是谁、你能干啥?
说白了,答案就藏在描述符里。描述符就是USB设备的身份证、简历、和功能说明书。主机通过读取这些描述符,才能决定怎么跟你打交道。今天我就带你把四个核心描述符彻底理一遍。
1. 描述符的树形结构
USB描述符是典型的树形层级结构。你可能看过一些文档,什么标准描述符、类描述符、厂商特定描述符……一团乱麻。其实没那么复杂。
按我的理解,你只需要抓住一条主线:描述符构成了一棵从顶层到底层的树。设备描述符是树根,然后是配置描述符,再到接口描述符,最后是端点描述符。
嗯,这里要注意:同一个设备可以有多个配置,但同一时刻只能激活一个。同一个配置下又能挂多个接口,接口下面才是端点。就是这个关系。
核心逻辑:枚举过程就是主机从树根开始,一层一层往下读描述符的过程。哪一层读错了,设备就起不来。
2. 设备描述符 —— 设备的身份证
设备描述符是所有描述符的老大哥。主机在枚举时,首先发的就是GET_DESCRIPTOR请求获取设备描述符(请求索引为0)。主机从里面读到什么?我上个表给你看:
| 字段 | 字节偏移 | 长度(字节) | 说明 |
|---|---|---|---|
| bLength | 0 | 1 | 描述符长度,固定为18 |
| bDescriptorType | 1 | 1 | 类型码,设备描述符为1 |
| bcdUSB | 2 | 2 | USB规格版本号,如0x0200表示USB 2.0 |
| bDeviceClass | 4 | 1 | 类代码,0表示在接口描述符里定义 |
| bDeviceSubClass | 5 | 1 | 子类代码 |
| bDeviceProtocol | 6 | 1 | 协议代码 |
| bMaxPacketSize0 | 7 | 1 | 端点0的最大包长 |
| idVendor | 8 | 2 | 厂商ID,由USB-IF分配 |
| idProduct | 10 | 2 | 产品ID,厂商自定义 |
| bcdDevice | 12 | 2 | 设备版本号,BCD格式 |
| iManufacturer | 14 | 1 | 厂商字符串索引 |
| iProduct | 15 | 1 | 产品字符串索引 |
| iSerialNumber | 16 | 1 | 序列号字符串索引 |
| bNumConfigurations | 17 | 1 | 支持的配置个数 |
个人经验:我在项目中遇到过设备描述符的bMaxPacketSize0写错了。USB 2.0的全速设备应该是64字节,结果有人写成了8字节。主机读到后直接认为这个设备是低速设备,后面枚举全乱套了。这个东西必须和硬件实际支持的包大小一致,千万不能瞎填。
3. 配置描述符 —— 设备的配置文件
读完了身份证,主机接着读配置描述符。配置描述符告诉主机这个设备能工作在什么功耗下、需要多少电流、支持多少接口。
你想想看,为什么好多USB设备插上去不用额外的驱动也能用?就是因为配置描述符里把功能描述清楚了。主机靠它来加载合适的驱动。
| 字段 | 大小(字节) | 说明 |
|---|---|---|
| bLength | 1 | 固定为9字节 |
| bDescriptorType | 1 | 配置描述符类型码,值为2 |
| wTotalLength | 2 | 整个配置描述符族的总长度,包含所有子描述符 |
| bNumInterfaces | 1 | 这个配置下的接口数量 |
| bConfigurationValue | 1 | 用于SET_CONFIGURATION请求的配置编号 |
| iConfiguration | 1 | 配置字符串索引 |
| bmAttributes | 1 | 供电与远程唤醒属性 |
| bMaxPower | 1 | 最大电流,以2mA为单位 |
避坑指南:我曾经遇到一个设备,配置描述符的wTotalLength写小了。主机读到一半发现数据不够,直接判定设备故障,设备管理器里显示“无法识别的USB设备”。后来发现是把接口描述符的字节数漏了。切记:wTotalLength必须包含所有子描述符的字节数总和。
4. 接口描述符 —— 设备的功能模块
每个配置下可以有多个接口。每个接口代表一个功能模块。比如一个USB摄像头,它有视频采集接口,还有音频输入接口,这两个就是不同的接口。
接口描述符里最关键的是 bInterfaceClass、bInterfaceSubClass、bInterfaceProtocol 这三个字段。主机通过它们匹配驱动。你看Windows的设备管理器里, “USB视频设备” 这种设备类型就是从这里读出来的。
| 字段 | 大小(字节) | 说明 |
|---|---|---|
| bLength | 1 | 固定为9字节 |
| bDescriptorType | 1 | 接口描述符类型码,值为4 |
| bInterfaceNumber | 1 | 接口编号,从0开始 |
| bAlternateSetting | 1 | 备用设置编号,用于切换接口属性 |
| bNumEndpoints | 1 | 接口使用的端点数量(除端点0外) |
| bInterfaceClass | 1 | 类代码 |
| bInterfaceSubClass | 1 | 子类代码 |
| bInterfaceProtocol | 1 | 协议代码 |
| iInterface | 1 | 接口字符串索引 |
实际案例:我调试过一个复合设备,它有HID接口和 bulk接口。它的接口描述符里bInterfaceClass分别写了 HID(0x03)和 大容量存储类(0x08)。主机读到HID接口就加载HID驱动,读到mass storage接口就加载磁盘驱动。这就是接口描述符的威力 —— 一个设备能同时扮演多个角色。
5. 端点描述符 —— 数据的通道
端点描述符是最底层的描述符。它描述的是实际数据流的方向、类型、大小。每个接口下面可以有多个端点,但端点0是公用的控制端点,不需要用端点描述符描述。
端点描述符里最让我头疼的是 bmAttributes 字段。它定义了端点的传输类型。会有传输、控制传输、中断传输和同步传输。同步传输又分同步和异步……嗯,这块容易搞混。
| 字段 | 大小(字节) | 说明 |
|---|---|---|
| bLength | 1 | 固定为7字节 |
| bDescriptorType | 1 | 端点描述符类型码,值为5 |
| bEndpointAddress | 1 | 端点地址+方向,bit7为1表示IN |
| bmAttributes | 1 | 传输类型:0控制、1等时、2批量、3中断 |
| wMaxPacketSize | 2 | 端点支持的最大包大小 |
| bInterval | 1 | 轮询时间间隔,以毫秒或帧为单位 |
个人习惯:我习惯用 Bus Hound 抓取枚举时的描述符数据。抓下来后直接看十六进制,对着表一对比就能发现哪里写错了。比如端点描述符里的wMaxPacketSize,USB 2.0 全速下批量端点最大是64字节,高速下是512字节。写大了,主机拒绝接收;写小了,数据吞吐又不够。
6. 描述符之间的强关联
最后说一个很多人忽略的点:描述符之间的字段是 互相绑定的。比如:
- 设备描述符中的bNumConfigurations 必须等于设备实际支持的配置数量。你写了2,但后面只有一个配置描述符,主机就会等到死。
- 配置描述符中的wTotalLength 必须包含该配置下所有接口描述符 + 所有端点描述符 + 可能的类描述符的字节总数。写过的人都知道,这个值算错了会出大问题。
- 接口描述符中的bNumEndpoints 要等于该接口下挂载的端点个数(除端点0外)。我曾经见过一个设备,这个字段写的3,但实际只提供了2个端点描述符。结果主机在读枚举数据时,发现数据长度对不上,直接放弃这个设备。
特别提醒:USB-IF一致性测试里,描述符校验是一个标准测试项目。测试工具会逐字段检查合法性,包括长度、类型、字段间的逻辑一致性。如果你遇到枚举失败,80%的可能是描述符有问题。嗯,剩下20%就是硬件电路的事了。
这就是四个核心描述符的全部内容。你掌握了这个树形结构,再去看USB枚举日志,会清晰很多。描述符是USB设备通信的基础,也是后面一致性测试里反复绕不开的东西。