4. USB描述符体系:设备描述符、配置描述符、接口描述符、端点描述符、字符串描述符的结构与解析
描述符,说白了就是USB设备的“身份证”和“说明书”。
我刚开始接触USB开发时,总觉得描述符这东西很抽象。后来调了一个U盘的枚举失败问题,才发现是配置描述符里的bMaxPower写错了——主机认为设备功耗超标,直接拒绝连接。从那以后,我对描述符的每个字节都格外敏感。
4.1 描述符的层级关系
USB描述符是一个树形结构。主机通过标准请求,一层一层往下读。你想想看,主机怎么知道你的设备是什么?能干什么?怎么通信?全靠这套描述符体系。
层级关系是这样的:
- 设备描述符:根节点,描述设备整体信息
- 配置描述符:每个设备可以有多个配置,但同一时刻只能激活一个
- 接口描述符:每个配置下可以有多个接口,接口代表功能
- 端点描述符:每个接口下可以有多个端点,端点是通信管道
- 字符串描述符:可选,提供可读的文字信息
核心要点:主机枚举设备时,先读设备描述符(18字节),再读配置描述符(9字节)+ 所有接口和端点描述符。字符串描述符是最后才读的,而且不是必须的。
我画了一张结构图,帮你理解这个层级关系:
4.2 设备描述符
设备描述符是所有描述符的起点。主机发的第一个标准请求就是获取设备描述符。固定18字节,不能多也不能少。
| 偏移 | 字段 | 大小 | 说明 |
|---|---|---|---|
| 0 | bLength | 1 | 描述符长度,固定0x12(18) |
| 1 | bDescriptorType | 1 | 类型码,设备描述符为0x01 |
| 2 | bcdUSB | 2 | USB规范版本号,如0x0200表示USB 2.0 |
| 4 | bDeviceClass | 1 | 设备类代码,0x00表示在接口描述符中定义 |
| 5 | bDeviceSubClass | 1 | 子类代码 |
| 6 | bDeviceProtocol | 1 | 协议代码 |
| 7 | bMaxPacketSize0 | 1 | 端点0的最大包长,USB 2.0一般为64 |
| 8 | idVendor | 2 | 厂商ID,由USB-IF分配 |
| 10 | idProduct | 2 | 产品ID,厂商自行定义 |
| 12 | bcdDevice | 2 | 设备版本号 |
| 14 | iManufacturer | 1 | 厂商字符串索引,0表示无 |
| 15 | iProduct | 1 | 产品字符串索引 |
| 16 | iSerialNumber | 1 | 序列号字符串索引 |
| 17 | bNumConfigurations | 1 | 支持的配置数量 |
实战经验:bMaxPacketSize0这个字段,我见过很多人填错。USB 2.0全速设备必须是8、16、32、64之一。高速设备固定64。我曾经接手一个项目,设备枚举时断时续,查了两天才发现这里填了128——主机直接不认。
4.3 配置描述符
配置描述符只有9字节,但它后面跟着一堆接口和端点描述符。主机读配置描述符时,会一次性把整个配置全集都读走。
| 偏移 | 字段 | 大小 | 说明 |
|---|---|---|---|
| 0 | bLength | 1 | 固定0x09 |
| 1 | bDescriptorType | 1 | 配置描述符类型码0x02 |
| 2 | wTotalLength | 2 | 配置描述符全集的总长度(包括接口和端点) |
| 4 | bNumInterfaces | 1 | 该配置支持的接口数量 |
| 5 | bConfigurationValue | 1 | 配置编号,用于SetConfiguration请求 |
| 6 | iConfiguration | 1 | 配置字符串索引 |
| 7 | bmAttributes | 1 | 配置特性,D7=1必须,D6=1自供电,D5=1支持远程唤醒 |
| 8 | bMaxPower | 1 | 最大功耗,单位2mA,值×2就是实际mA数 |
避坑指南:wTotalLength这个字段,我见过有人只算了配置描述符本身的9字节,没算接口和端点。结果主机读不全,枚举失败。记住:wTotalLength = 9 + 所有接口描述符长度 + 所有端点描述符长度。
4.4 接口描述符
接口描述符描述了一个功能单元。比如一个U盘可能有Mass Storage接口,也可能有额外的HID接口。每个接口可以有多个设置(Alternate Setting),但默认使用Setting 0。
| 偏移 | 字段 | 大小 | 说明 |
|---|---|---|---|
| 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 | 接口字符串索引 |
我个人习惯把接口描述符看作“功能声明”。比如做一个USB转串口设备,接口类设为0xFF(厂商自定义),子类和协议也设成0xFF。主机看到这个就知道需要装驱动了。
4.5 端点描述符
端点是USB通信的实体管道。除了端点0(控制端点),每个端点都需要一个描述符。端点描述符固定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 | wMaxPacketSize | 2 | 最大包长,高速批量端点固定512 |
| 6 | bInterval | 1 | 轮询间隔,中断端点为1~255,单位ms(全速)或125μs(高速) |
关键点:bEndpointAddress的D7位决定了方向。0x81表示端点1 IN(设备到主机),0x01表示端点1 OUT(主机到设备)。我见过有人把方向搞反,结果数据死活发不出去。
4.6 字符串描述符
字符串描述符不是必须的,但有了它,设备在主机上显示的就是“我的U盘”而不是“Unknown Device”。字符串描述符的索引从1开始,索引0是特殊的语言ID描述符。
字符串描述符的结构很简单:
- bLength:长度,包括自身
- bDescriptorType:类型码0x03
- bString:Unicode字符串(UTF-16LE编码)
举个例子,一个厂商字符串"USB-DEV"的描述符数据:
// 字符串描述符示例:厂商字符串 "USB-DEV"
// 语言ID描述符(索引0)
0x04, 0x03, 0x09, 0x04 // 长度4,类型3,语言ID=0x0409(美式英语)
// 厂商字符串(索引1)
0x10, 0x03, // 长度16,类型3
0x55, 0x00, // U
0x53, 0x00, // S
0x42, 0x00, // B
0x2D, 0x00, // -
0x44, 0x00, // D
0x45, 0x00, // E
0x56, 0x00 // V
小技巧:字符串描述符的bLength必须是偶数,因为每个字符占2字节。我习惯在代码里用宏定义来生成字符串描述符,避免手动计算长度出错。
4.7 描述符的解析流程
主机解析描述符的流程,说白了就是“先读个头,再读全身”。
- 获取设备描述符:主机发GetDescriptor(DEVICE),只读前8字节,拿到bMaxPacketSize0
- 设置地址:主机发SetAddress,设备进入Address状态
- 再次获取设备描述符:这次读全部18字节
- 获取配置描述符:先读9字节,从wTotalLength知道总长度
- 获取配置全集:按wTotalLength读全部数据,然后解析出接口和端点
- 获取字符串描述符:如果有字符串索引,主机再发请求读取
- 选择配置:主机发SetConfiguration,设备开始正常工作
重要提醒:配置描述符的解析不能只按固定偏移来。因为接口描述符和端点描述符的长度可能变化(比如某些类描述符会嵌入额外信息)。正确的做法是:从配置描述符开始,按bLength字段逐个跳转解析。
4.8 实战:用代码构造描述符
下面是一个典型的USB鼠标设备描述符集合。我把它写成一个C语言结构体数组,方便你直接移植到项目中。
// USB鼠标设备描述符集合
__packed const uint8_t USB_DeviceDescriptor[] = {
// 设备描述符
0x12, 0x01, // bLength, bDescriptorType
0x00, 0x02, // bcdUSB = 2.00
0x00, 0x00, 0x00, // bDeviceClass, SubClass, Protocol
0x40, // bMaxPacketSize0 = 64
0x83, 0x04, // idVendor = 0x0483 (ST)
0x10, 0x57, // idProduct = 0x5710
0x00, 0x01, // bcdDevice = 1.00
0x01, // iManufacturer = 1
0x02, // iProduct = 2
0x00, // iSerialNumber = 0
0x01, // bNumConfigurations = 1
// 配置描述符
0x09, 0x02, // bLength, bDescriptorType
0x22, 0x00, // wTotalLength = 34
0x01, // bNumInterfaces = 1
0x01, // bConfigurationValue = 1
0x00, // iConfiguration = 0
0xA0, // bmAttributes = 自供电 + 远程唤醒
0x32, // bMaxPower = 100mA
// 接口描述符
0x09, 0x04, // bLength, bDescriptorType
0x00, // bInterfaceNumber = 0
0x00, // bAlternateSetting = 0
0x01, // bNumEndpoints = 1
0x03, // bInterfaceClass = HID
0x01, // bInterfaceSubClass = Boot
0x02, // bInterfaceProtocol = Mouse
0x00, // iInterface = 0
// HID描述符(类特定描述符)
0x09, 0x21, // bLength, bDescriptorType (HID)
0x10, 0x01, // bcdHID = 1.10
0x00, // bCountryCode
0x01, // bNumDescriptors
0x22, // bDescriptorType (Report)
0x34, 0x00, // wDescriptorLength = 52
// 端点描述符(中断IN)
0x07, 0x05, // bLength, bDescriptorType
0x81, // bEndpointAddress = EP1 IN
0x03, // bmAttributes = Interrupt
0x08, 0x00, // wMaxPacketSize = 8
0x0A // bInterval = 10ms
};
注意:HID描述符是类特定描述符,它插在接口描述符和端点描述符之间。wTotalLength必须包含它。我刚开始写HID设备时,就忘了算这个,结果主机枚举到一半卡住了。
描述符体系是USB协议的基础。你只要把每个字段的含义搞清楚,再结合逻辑分析仪抓包验证,很快就能上手。嗯,这里要注意一点:不同USB类(HID、Mass Storage、CDC等)可能有额外的类特定描述符,但标准描述符的结构是固定的。