4. USB描述符家族:设备描述符、配置描述符、接口描述符、端点描述符、字符串描述符、HID报告描述符
描述符,说白了就是USB设备的「身份证」和「说明书」。主机插上你的设备后,第一件事就是通过枚举过程,把这些描述符一个个读走。读明白了,才知道你是什么设备、该怎么驱动。
我刚开始做USB开发时,总觉得描述符就是填几个表格,随便写写就行。结果有一次设备插到Windows上,系统弹了个「未知设备」,折腾了两天才发现是配置描述符里的bMaxPower写错了。嗯,从那以后我再也不敢小看这些字节了。
4.1 描述符的层级结构
USB描述符是树形结构,从上到下依次是:设备 → 配置 → 接口 → 端点。字符串描述符和HID报告描述符是「挂」在接口上的附属信息。
主机枚举时,先读设备描述符,再读配置描述符,然后根据配置里的接口数量,逐个读接口描述符和端点描述符。这个过程是递归的,主机自己会解析,你只需要把数据准备好就行。
4.2 设备描述符
设备描述符是USB设备的「根节点」。主机第一个读的就是它,长度固定为18字节。
| 偏移 | 字段 | 长度 | 说明 |
|---|---|---|---|
| 0 | bLength | 1 | 描述符长度,固定0x12(18) |
| 1 | bDescriptorType | 1 | 类型码,设备描述符为0x01 |
| 2 | bcdUSB | 2 | USB规范版本号,如0x0200(USB 2.0) |
| 4 | bDeviceClass | 1 | 设备类代码,0xFF表示厂商自定义 |
| 5 | bDeviceSubClass | 1 | 子类代码 |
| 6 | bDeviceProtocol | 1 | 协议代码 |
| 7 | bMaxPacketSize0 | 1 | 端点0最大包长,USB 2.0一般为64 |
| 8 | idVendor | 2 | 厂商ID(VID),需向USB-IF申请 |
| 10 | idProduct | 2 | 产品ID(PID),厂商自行分配 |
| 12 | bcdDevice | 2 | 设备版本号 |
| 14 | iManufacturer | 1 | 厂商字符串描述符索引 |
| 15 | iProduct | 1 | 产品字符串描述符索引 |
| 16 | iSerialNumber | 1 | 序列号字符串描述符索引 |
| 17 | bNumConfigurations | 1 | 支持的配置数量,通常为1 |
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=自供电,D5=远程唤醒 |
| 8 | bMaxPower | 1 | 最大电流,单位2mA。如0x32=100mA |
4.4 接口描述符
接口描述符描述一个「功能单元」。比如一个USB耳机,可能有音频控制接口和音频流接口。长度固定为9字节。
| 偏移 | 字段 | 长度 | 说明 |
|---|---|---|---|
| 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 | 接口字符串描述符索引 |
你想想看,为什么要有bAlternateSetting?我遇到过一种场景:一个视频采集设备,在低分辨率时只需要一个等时端点,切换到高分辨率时需要两个。通过备用设置,主机可以在不重新枚举的情况下切换接口配置,非常灵活。
4.5 端点描述符
端点描述符描述数据传输的「管道」。每个端点(除了默认端点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-255ms,等时端点为1-16ms |
4.6 字符串描述符
字符串描述符是可选的,用于提供人类可读的文本信息,比如厂商名、产品名、序列号。它的结构比较特殊:第一个字节是长度,第二个字节是类型码0x03,后面是Unicode(UTF-16LE)编码的字符串。
// 字符串描述符示例:厂商名 "MyUSB"
// 语言ID描述符(索引0)
0x04, 0x03, 0x09, 0x04 // 长度4,类型3,语言ID 0x0409(美式英语)
// 厂商字符串(索引1)
0x0E, 0x03, // 长度14,类型3
'M', 0x00, 'y', 0x00, 'U', 0x00, 'S', 0x00, 'B', 0x00 // "MyUSB" 的UTF-16LE编码
我个人习惯把字符串描述符的索引0(语言ID)放在最前面。主机读字符串时,会先读索引0获取支持的语言列表,然后再根据语言ID读具体的字符串。如果你只支持一种语言,那就只填一个语言ID。
4.7 HID报告描述符
HID报告描述符是HID设备的「灵魂」。它不描述硬件,而是描述数据的「含义」和「格式」。比如鼠标的移动数据、按键数据,都是通过报告描述符告诉主机的。
HID报告描述符的语法比较复杂,我挑一个鼠标的例子来说明:
// 鼠标报告描述符(简化版)
0x05, 0x01, // Usage Page (Generic Desktop)
0x09, 0x02, // Usage (Mouse)
0xA1, 0x01, // Collection (Application)
0x09, 0x01, // Usage (Pointer)
0xA1, 0x00, // Collection (Physical)
0x05, 0x09, // Usage Page (Button)
0x19, 0x01, // Usage Minimum (1)
0x29, 0x03, // Usage Maximum (3)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x95, 0x03, // Report Count (3)
0x75, 0x01, // Report Size (1)
0x81, 0x02, // Input (Data,Var,Abs)
0x95, 0x01, // Report Count (1)
0x75, 0x05, // Report Size (5)
0x81, 0x01, // Input (Const) ; 填充位
0x05, 0x01, // Usage Page (Generic Desktop)
0x09, 0x30, // Usage (X)
0x09, 0x31, // Usage (Y)
0x15, 0x81, // Logical Minimum (-127)
0x25, 0x7F, // Logical Maximum (127)
0x75, 0x08, // Report Size (8)
0x95, 0x02, // Report Count (2)
0x81, 0x06, // Input (Data,Var,Rel)
0xC0, // End Collection
0xC0 // End Collection
4.8 描述符的「组装」顺序
主机枚举时,描述符的返回顺序是固定的。你必须在固件中按这个顺序组织数据:
- 设备描述符(18字节)
- 配置描述符(9字节)
- 接口描述符(9字节)
- HID描述符(9字节,仅HID设备)
- 端点描述符(7字节 × N)
- 字符串描述符(按需返回)
注意:HID描述符不是标准USB描述符,它是HID类特有的,放在接口描述符之后、端点描述符之前。这个顺序不能乱,否则主机解析会出错。