4. USB描述符家族:设备描述符、配置描述符、接口描述符、端点描述符、字符串描述符、HID报告描述符

描述符,说白了就是USB设备的「身份证」和「说明书」。主机插上你的设备后,第一件事就是通过枚举过程,把这些描述符一个个读走。读明白了,才知道你是什么设备、该怎么驱动。

我刚开始做USB开发时,总觉得描述符就是填几个表格,随便写写就行。结果有一次设备插到Windows上,系统弹了个「未知设备」,折腾了两天才发现是配置描述符里的bMaxPower写错了。嗯,从那以后我再也不敢小看这些字节了。

核心认知:描述符是USB协议栈的「元数据」。主机不读你的代码,只读你的描述符。描述符写错了,设备就「哑巴」了。

4.1 描述符的层级结构

USB描述符是树形结构,从上到下依次是:设备 → 配置 → 接口 → 端点。字符串描述符和HID报告描述符是「挂」在接口上的附属信息。

USB 描述符层级结构 设备描述符 配置描述符 接口描述符 端点描述符 1 端点描述符 2 字符串描述符 HID 报告描述符 注意 字符串和HID报告描述符 不是每个设备都必须有

主机枚举时,先读设备描述符,再读配置描述符,然后根据配置里的接口数量,逐个读接口描述符和端点描述符。这个过程是递归的,主机自己会解析,你只需要把数据准备好就行。

4.2 设备描述符

设备描述符是USB设备的「根节点」。主机第一个读的就是它,长度固定为18字节。

偏移字段长度说明
0bLength1描述符长度,固定0x12(18)
1bDescriptorType1类型码,设备描述符为0x01
2bcdUSB2USB规范版本号,如0x0200(USB 2.0)
4bDeviceClass1设备类代码,0xFF表示厂商自定义
5bDeviceSubClass1子类代码
6bDeviceProtocol1协议代码
7bMaxPacketSize01端点0最大包长,USB 2.0一般为64
8idVendor2厂商ID(VID),需向USB-IF申请
10idProduct2产品ID(PID),厂商自行分配
12bcdDevice2设备版本号
14iManufacturer1厂商字符串描述符索引
15iProduct1产品字符串描述符索引
16iSerialNumber1序列号字符串描述符索引
17bNumConfigurations1支持的配置数量,通常为1
我的经验:bMaxPacketSize0这个字段,很多人填0x40(64字节)就完事了。但如果你做的是低速设备(如鼠标),这里必须填8。我见过一个同事把低速设备填了64,结果主机枚举时直接超时,设备根本认不出来。

4.3 配置描述符

配置描述符描述设备的「供电模式」和「接口数量」。一个设备可以有多个配置,但同一时刻只能激活一个。长度固定为9字节。

偏移字段长度说明
0bLength1固定0x09
1bDescriptorType1配置描述符类型码0x02
2wTotalLength2整个配置的总长度(含接口、端点等)
4bNumInterfaces1该配置下的接口数量
5bConfigurationValue1配置编号,用于SetConfiguration命令
6iConfiguration1配置字符串描述符索引
7bmAttributes1供电属性:D7=1(必须),D6=自供电,D5=远程唤醒
8bMaxPower1最大电流,单位2mA。如0x32=100mA
避坑指南:wTotalLength这个字段,很多人只算了配置描述符自己的9字节,忘了加上接口和端点的长度。我曾经调试一个复合设备,主机总是枚举到一半就停了,最后发现wTotalLength少算了20多个字节。记住:这个长度是「配置描述符 + 所有接口描述符 + 所有端点描述符 + 所有附属描述符」的总和。

4.4 接口描述符

接口描述符描述一个「功能单元」。比如一个USB耳机,可能有音频控制接口和音频流接口。长度固定为9字节。

偏移字段长度说明
0bLength1固定0x09
1bDescriptorType1接口描述符类型码0x04
2bInterfaceNumber1接口编号,从0开始
3bAlternateSetting1备用设置编号,用于切换接口配置
4bNumEndpoints1该接口使用的端点数量(不含端点0)
5bInterfaceClass1接口类代码,如HID=0x03
6bInterfaceSubClass1接口子类代码
7bInterfaceProtocol1接口协议代码
8iInterface1接口字符串描述符索引

你想想看,为什么要有bAlternateSetting?我遇到过一种场景:一个视频采集设备,在低分辨率时只需要一个等时端点,切换到高分辨率时需要两个。通过备用设置,主机可以在不重新枚举的情况下切换接口配置,非常灵活。

4.5 端点描述符

端点描述符描述数据传输的「管道」。每个端点(除了默认端点0)都需要一个描述符。长度固定为7字节。

偏移字段长度说明
0bLength1固定0x07
1bDescriptorType1端点描述符类型码0x05
2bEndpointAddress1端点地址:D7=方向(0:OUT,1:IN),D3-D0=端点号
3bmAttributes1传输类型:0=控制,1=等时,2=批量,3=中断
4wMaxPacketSize2最大包长,高速批量端点可达512字节
6bInterval1查询间隔,中断端点为1-255ms,等时端点为1-16ms
关键点:bEndpointAddress的D7位决定了方向。0x81表示端点1 IN(设备到主机),0x01表示端点1 OUT(主机到设备)。很多人搞反了方向,导致数据发不出去也收不回来。

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
我的经验:写HID报告描述符最容易犯的错是Report Size和Report Count算错了。比如上面这个鼠标,3个按键各占1位,加上5位填充,凑成一个字节。然后X和Y各占8位。总共3个字节。如果你算错了,主机解析数据时就会错位,鼠标动起来乱跳。我建议你写完后,手动算一遍总位数,确保是8的倍数。

4.8 描述符的「组装」顺序

主机枚举时,描述符的返回顺序是固定的。你必须在固件中按这个顺序组织数据:

  1. 设备描述符(18字节)
  2. 配置描述符(9字节)
  3. 接口描述符(9字节)
  4. HID描述符(9字节,仅HID设备)
  5. 端点描述符(7字节 × N)
  6. 字符串描述符(按需返回)

注意:HID描述符不是标准USB描述符,它是HID类特有的,放在接口描述符之后、端点描述符之前。这个顺序不能乱,否则主机解析会出错。

避坑指南:我曾经帮一个客户调试HID设备,设备枚举总是失败。抓包一看,他把HID描述符放在了端点描述符后面。主机读到端点描述符后,以为接口描述符已经结束了,结果后面的HID描述符被当成了垃圾数据。记住:HID描述符必须紧跟在接口描述符后面。