4. 描述符家族:从设备到端点的完整画像

描述符是什么?说白了,就是 USB 设备的「身份证」加「简历」。主机插上你的设备后,第一件事就是通过标准请求把这一整套描述符读走,然后才知道:你是谁?你能干什么?该怎么跟你通信?

我个人习惯把描述符家族比作俄罗斯套娃——最外层是设备描述符,打开里面是配置描述符,再打开是接口描述符,最里面是端点描述符。字符串描述符和 HID 描述符则是额外的「附件说明」。今天我们就一个一个拆开看。

核心要点:描述符的层级关系是 USB 枚举的基石。主机通过 Get_Descriptor 请求逐层读取,任何一层填错,设备都可能无法正常工作。
USB 描述符层级结构 设备描述符 配置描述符 接口描述符 端点描述符 1 端点描述符 2 说明: 一个设备只有一个设备描述符,但可以有多个配置描述符。 每个配置下可以有多个接口,每个接口下可以有多个端点。

4.1 设备描述符:设备的「户口本」

设备描述符是所有描述符的「老大」。主机枚举时,第一个标准请求就是获取设备描述符(长度 18 字节)。它告诉主机:这个设备用了什么 USB 版本、厂商是谁、产品是什么、需要多大电流。

偏移 字段 长度 说明
0 bLength 1 描述符长度,固定 0x12 (18)
1 bDescriptorType 1 描述符类型,设备描述符为 0x01
2-3 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-9 idVendor 2 厂商 ID (VID),由 USB-IF 分配
10-11 idProduct 2 产品 ID (PID),厂商自定义
12-13 bcdDevice 2 设备版本号
14 iManufacturer 1 厂商字符串描述符索引
15 iProduct 1 产品字符串描述符索引
16 iSerialNumber 1 序列号字符串描述符索引
17 bNumConfigurations 1 支持的配置数量
实战经验:bMaxPacketSize0 这个字段我踩过坑。USB 2.0 全速设备必须填 8、16、32 或 64,高速设备固定 64。有一次我填了 128,结果主机枚举直接失败,折腾了半天才发现是这里的问题。

4.2 配置描述符:设备的「功能清单」

配置描述符描述了一个配置的整体信息。一个设备可以有多个配置(比如一个配置用电池供电,另一个用总线供电),但同一时刻只能激活一个。

配置描述符本身只有 9 字节,但它后面会跟着接口描述符和端点描述符。主机通过读取配置描述符的 wTotalLength 字段,就知道整个配置「家族」的总长度。

偏移 字段 长度 说明
0 bLength 1 固定 0x09 (9)
1 bDescriptorType 1 配置描述符类型,0x02
2-3 wTotalLength 2 整个配置描述符家族的总长度
4 bNumInterfaces 1 该配置下的接口数量
5 bConfigurationValue 1 配置编号,用于 Set_Configuration 请求
6 iConfiguration 1 配置字符串描述符索引
7 bmAttributes 1 供电属性:D7=1 总线供电,D6=1 自供电,D5=1 支持远程唤醒
8 bMaxPower 1 最大电流,单位 2mA,如 0x32 表示 100mA
注意:bmAttributes 的 D7 位必须为 1。我曾经见过有人把 D7 写成 0,结果主机认为配置无效,设备直接不被识别。另外 bMaxPower 填 0x32 表示 100mA,不是 50mA,单位是 2mA 不是 1mA。

4.3 接口描述符:设备的「功能模块」

接口描述符定义了一个功能接口。比如一个复合设备可能有 HID 接口和音频接口,每个接口都有自己的接口描述符。

接口描述符也是 9 字节。注意它的 bInterfaceNumber 和 bAlternateSetting 字段——前者是接口编号,后者是备用设置。同一个接口可以有多个备用设置,用于切换不同的带宽需求。

偏移 字段 长度 说明
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 接口字符串描述符索引
避坑指南:我曾经做过一个复合设备,接口 0 是 HID,接口 1 是 CDC。结果 CDC 接口的 bInterfaceClass 填错了,主机加载了错误的驱动。记住:接口类代码决定了主机用什么驱动来跟你通信,填错了就是「设备无法识别」。

4.4 端点描述符:数据的「出入口」

端点是 USB 通信的最小单元。每个端点描述符 7 字节,描述了端点的地址、方向、传输类型和最大包长。

端点 0 是默认控制端点,不需要描述符。其他端点(1~15)都需要在接口描述符后面跟着对应的端点描述符。

偏移 字段 长度 说明
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-5 wMaxPacketSize 2 最大包长,USB 2.0 高速批量端点最大 512
6 bInterval 1 轮询间隔,中断端点单位是 ms(全速)或 125μs(高速)
关键点:bEndpointAddress 的 D7 位决定了方向。0x81 表示端点 1 IN(设备到主机),0x01 表示端点 1 OUT(主机到设备)。我见过有人把 IN 和 OUT 搞反,结果主机发数据设备收不到,设备发数据主机也不理。

4.5 字符串描述符:设备的「名字牌」

字符串描述符是可选的,但强烈建议加上。它让设备在主机上显示一个友好的名字,而不是「Unknown Device」。

字符串描述符使用 Unicode 编码(UTF-16LE)。第一个字节是长度,第二个字节是类型(0x03),后面是 Unicode 字符串。注意:字符串描述符的索引 0 是特殊的,它返回的是语言 ID 列表。

// 字符串描述符示例:语言 ID 列表
// 索引 0:支持的语言(英语美国)
0x04, 0x03, 0x09, 0x04

// 字符串描述符示例:厂商字符串 "MyUSB"
// 索引 1
0x0C, 0x03, 'M', 0x00, 'y', 0x00, 'U', 0x00, 'S', 0x00, 'B', 0x00
个人经验:字符串描述符的长度计算容易出错。每个字符占 2 字节,加上头部的 2 字节。比如 "MyUSB" 是 4 个字符,总长度 = 2 + 4*2 = 10 = 0x0A。我刚开始写的时候经常算错,后来干脆写了个宏自动计算。

4.6 HID 描述符:人机交互设备的「说明书」

HID 描述符是 HID 类设备特有的。它告诉主机:这个设备用什么协议、报告描述符有多长。HID 描述符本身很短,但它指向的报告描述符才是真正的「重头戏」。

偏移 字段 长度 说明
0 bLength 1 固定 0x09
1 bDescriptorType 1 HID 描述符类型,0x21
2-3 bcdHID 2 HID 规范版本号,通常 0x0111
4 bCountryCode 1 国家代码,0x00 表示不指定
5 bNumDescriptors 1 下级描述符数量,通常为 1
6 bDescriptorType 1 下级描述符类型,报告描述符为 0x22
7-8 wDescriptorLength 2 报告描述符的总长度
注意:HID 描述符必须放在接口描述符之后、端点描述符之前。我见过有人把顺序搞反了,结果主机解析失败。另外,报告描述符的长度一定要算准,填短了主机读不全,填长了主机可能读超时。

4.7 实战填充:一个完整的 HID 键盘描述符集合

说了这么多理论,我们来填一个真实的例子。下面是一个 USB 全速 HID 键盘的描述符集合,包含设备、配置、接口、HID、端点和字符串描述符。

// 设备描述符 (18 字节)
0x12,                   // bLength
0x01,                   // bDescriptorType (Device)
0x00, 0x02,             // bcdUSB (2.00)
0x00,                   // bDeviceClass (在接口中定义)
0x00,                   // bDeviceSubClass
0x00,                   // bDeviceProtocol
0x40,                   // bMaxPacketSize0 (64)
0x34, 0x12,             // idVendor (0x1234)
0x56, 0x78,             // idProduct (0x7856)
0x00, 0x01,             // bcdDevice (1.00)
0x01,                   // iManufacturer (字符串索引 1)
0x02,                   // iProduct (字符串索引 2)
0x03,                   // iSerialNumber (字符串索引 3)
0x01,                   // bNumConfigurations (1 个配置)

// 配置描述符 (9 字节)
0x09,                   // bLength
0x02,                   // bDescriptorType (Configuration)
0x29, 0x00,             // wTotalLength (41 字节,整个家族)
0x01,                   // bNumInterfaces (1 个接口)
0x01,                   // bConfigurationValue (配置编号 1)
0x00,                   // iConfiguration (无字符串)
0x80,                   // bmAttributes (总线供电)
0x32,                   // bMaxPower (100mA)

// 接口描述符 (9 字节)
0x09,                   // bLength
0x04,                   // bDescriptorType (Interface)
0x00,                   // bInterfaceNumber (接口 0)
0x00,                   // bAlternateSetting
0x01,                   // bNumEndpoints (1 个端点,不包括端点 0)
0x03,                   // bInterfaceClass (HID)
0x01,                   // bInterfaceSubClass (Boot 子类)
0x01,                   // bInterfaceProtocol (键盘)
0x00,                   // iInterface (无字符串)

// HID 描述符 (9 字节)
0x09,                   // bLength
0x21,                   // bDescriptorType (HID)
0x11, 0x01,             // bcdHID (1.11)
0x00,                   // bCountryCode
0x01,                   // bNumDescriptors
0x22,                   // bDescriptorType (Report)
0x41, 0x00,             // wDescriptorLength (65 字节)

// 端点描述符 (7 字节)
0x07,                   // bLength
0x05,                   // bDescriptorType (Endpoint)
0x81,                   // bEndpointAddress (端点 1 IN)
0x03,                   // bmAttributes (中断传输)
0x08, 0x00,             // wMaxPacketSize (8 字节)
0x0A,                   // bInterval (10ms)
验证方法:填完描述符后,我建议用 USB 分析仪抓包验证。看主机请求了什么、设备返回了什么。如果 wTotalLength 算错了,主机可能只读到一半描述符就卡住了。我每次都会用 USBTrace 或 Wireshark 抓一次枚举过程,确保每个字节都对。