4、USB描述符体系:设备描述符、配置描述符、接口描述符、端点描述符、字符串描述符

描述符,说白了就是USB设备的「身份证」和「说明书」。

主机枚举设备时,第一步就是通过控制传输把这一堆描述符读回来。读懂了,才知道你插上来的到底是个U盘、鼠标,还是个复合设备。我早年调试一个自定义HID设备时,枚举死活过不去,最后发现是配置描述符里的总长度算错了——少算了两个字节。嗯,从那以后我养成了一个习惯:每次写完描述符数组,先拿计算器加一遍。

4.1 描述符的层级关系

USB描述符是树形结构,一层套一层。主机从根节点(设备描述符)开始读,然后一层层往下走。

核心层级(从上到下):

  • 设备描述符 —— 整台设备的全局信息,一个设备只有一个。
  • 配置描述符 —— 一种工作模式下的资源分配,一个设备可以有多个配置。
  • 接口描述符 —— 某个功能单元的属性,一个配置下可以有多个接口。
  • 端点描述符 —— 数据传输的管道属性,一个接口下可以有多个端点。
  • 字符串描述符 —— 给人看的文字信息,可选,但建议加上。

我习惯把这种关系想象成「酒店 → 楼层 → 房间 → 房门」。设备是整栋酒店,配置是某一层的布局,接口是具体的房间,端点是房间的门——数据就从这些门里进出。

USB描述符层级结构图 USB描述符层级结构 设备描述符 (Device Descriptor) 配置描述符 (Configuration Descriptor) 接口描述符 (Interface Descriptor) 端点描述符 (Endpoint Descriptor) 说明 1个设备 1~N个配置 1配置下N个接口 1接口下N个端点 端点0无描述符 字符串可选 配置描述符包含 其下所有子描述符 的总长度

4.2 设备描述符

设备描述符是主机读到的第一份数据,固定18字节。它告诉主机:你是谁家的芯片?遵循哪个USB版本?有多少种配置?

偏移 字段 长度(字节) 说明
0bLength1描述符长度,固定0x12 (18)
1bDescriptorType1类型码,设备描述符为0x01
2-3bcdUSB2USB规范版本号,BCD码,如0x0200表示USB 2.0
4bDeviceClass1设备类代码,0x00表示在接口描述符中定义
5bDeviceSubClass1子类代码
6bDeviceProtocol1协议代码
7-8idVendor2厂商ID (VID),USB-IF分配
9-10idProduct2产品ID (PID),厂商自定义
11-12bcdDevice2设备版本号,BCD码
13iManufacturer1厂商字符串描述符索引
14iProduct1产品字符串描述符索引
15iSerialNumber1序列号字符串描述符索引
16bNumConfigurations1支持的配置数量

避坑指南:我曾经遇到一个坑——bcdUSB字段填了0x0110(USB 1.1),结果主机以全速12Mbps枚举,但我的硬件其实只支持全速。后来改成0x0200,主机才正确识别为全速设备。记住:bcdUSB填的是你希望主机用什么协议版本跟你通信,不是你的硬件版本。

4.3 配置描述符

配置描述符固定9字节,但它后面会跟着一串子描述符(接口、端点、甚至其他)。主机通过一次控制传输,把整个配置描述符集合(配置描述符 + 所有子描述符)一口气读走。

这里有个关键字段:wTotalLength。它表示整个配置描述符集合的总字节数。我见过不少新手在这里栽跟头——只算了配置描述符本身的9字节,忘了加上接口和端点的长度。结果主机读到一半发现长度对不上,直接复位设备。

偏移 字段 长度(字节) 说明
0bLength1固定0x09
1bDescriptorType1配置描述符类型码0x02
2-3wTotalLength2整个配置描述符集合的总长度
4bNumInterfaces1该配置下的接口数量
5bConfigurationValue1主机用这个值来选择配置
6iConfiguration1配置字符串描述符索引
7bmAttributes1电源属性:D7=总线供电,D6=自供电,D5=远程唤醒
8bMaxPower1最大电流,单位2mA,如0x32表示100mA

注意:bmAttributes的D7位必须为1。如果写成0,主机直接拒绝该配置。我调试一个自供电设备时就犯过这个错——D7没置位,Windows弹了个「未知USB设备」的提示,折腾了半天才发现是这一个比特的问题。

4.4 接口描述符

接口描述符固定9字节,描述一个功能单元的属性。一个配置下可以有多个接口,比如一个USB摄像头可以有视频接口和音频接口。

接口描述符里有个字段叫bInterfaceNumber,这是接口的编号。另一个字段bAlternateSetting用于切换接口的备用设置。我做过一个音频设备,用备用设置切换采样率——主机发一个Set Interface请求,接口描述符里的端点描述符就跟着变了。

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

4.5 端点描述符

端点描述符固定7字节,描述一个端点的传输属性。端点0(控制端点)没有描述符,其他端点都需要。

端点描述符里最重要的字段是bEndpointAddress和bmAttributes。前者决定方向(IN还是OUT)和端点号,后者决定传输类型(控制、批量、中断、同步)。

偏移 字段 长度(字节) 说明
0bLength1固定0x07
1bDescriptorType1端点描述符类型码0x05
2bEndpointAddress1D7: 方向(0=OUT,1=IN),D3-D0: 端点号
3bmAttributes1D1-D0: 传输类型(00=控制,01=等时,10=批量,11=中断)
4-5wMaxPacketSize2最大包大小,全速批量端点最大64字节
6bInterval1中断/等时端点的轮询间隔,单位ms或125μs

实战经验:wMaxPacketSize这个字段,全速批量端点最大只能填64,高速批量端点可以填512。我见过有人给全速设备填了128,结果主机直接忽略这个端点,数据发不出去也收不回来。另外bInterval对于中断端点,全速设备范围是1~255ms,高速设备是1~16(单位125μs)。

4.6 字符串描述符

字符串描述符是可选的,但建议加上。没有字符串描述符,设备管理器里显示的就是「未知USB设备」,有了之后才能看到厂商名、产品名、序列号。

字符串描述符的格式比较特殊:第一个字节是长度,第二个字节是类型码0x03,后面是Unicode(UTF-16LE)编码的字符串。注意,不是ASCII,是Unicode。每个字符占两个字节。

// 字符串描述符示例:厂商名 "MyUSB"
// 语言ID: 0x0409 (英语-美国)
// 字符串: 'M'=0x004D, 'y'=0x0079, 'U'=0x0055, 'S'=0x0053, 'B'=0x0042

// 语言ID描述符 (索引0)
const uint8_t LangID[] = {
    0x04,       // bLength
    0x03,       // bDescriptorType
    0x09, 0x04  // wLANGID: 英语-美国
};

// 厂商字符串描述符 (索引1)
const uint8_t Manufacturer[] = {
    0x0C,       // bLength: 12字节 (2字节头 + 5个字符×2字节)
    0x03,       // bDescriptorType
    'M', 0x00,  // M
    'y', 0x00,  // y
    'U', 0x00,  // U
    'S', 0x00,  // S
    'B', 0x00   // B
};

小技巧:字符串描述符索引0是语言ID描述符,主机必须先读索引0获取支持的语言列表,再根据语言ID读具体的字符串。我习惯只支持英语-美国(0x0409),省事。如果你要支持多语言,记得在语言ID描述符里列出所有支持的LANGID。

4.7 描述符的组装顺序

主机通过Get Descriptor请求读取描述符时,返回的数据是连续排列的。配置描述符在前,后面紧跟着该配置下的所有接口描述符,每个接口描述符后面跟着该接口的所有端点描述符。

举个例子,一个简单的HID设备(1个接口,1个中断IN端点)的描述符排列顺序:

  1. 配置描述符 (9字节)
  2. 接口描述符 (9字节)
  3. HID描述符 (9字节,HID类特有,不是标准USB描述符)
  4. 端点描述符 (7字节,中断IN端点)

总长度 = 9 + 9 + 9 + 7 = 34字节。这个值要填在配置描述符的wTotalLength字段里。

常见错误:描述符的顺序不能乱。主机解析时是按类型码来识别的,但顺序必须符合规范:配置 → 接口 → 端点。如果把端点描述符放在接口描述符前面,主机解析到端点描述符时还不知道它属于哪个接口,直接报错。我调试复合设备时遇到过这个问题——两个接口的描述符交叉排列,结果主机只认了第一个接口。

4.8 描述符的调试方法

枚举失败时,怎么查描述符的问题?我一般用这几招:

  • USB分析仪:抓取主机发起的Get Descriptor请求和设备返回的数据,逐字节核对。这是最可靠的方法。
  • 逻辑分析仪:没有分析仪时,用逻辑分析仪抓D+和D-上的数据,虽然解析起来费劲,但能看出主机是否收到了NAK或STALL。
  • 软件工具:Windows下用USBView或Bus Hound,Linux下用lsusb -v,可以直接看到主机解析后的描述符内容。
  • 打印调试:在固件里加打印,每次发送描述符数据前把长度和内容打印出来,跟预期值对比。

我个人最常用的是USB分析仪加USBView组合。分析仪看底层数据对不对,USBView看主机解析结果对不对。两相对比,问题出在哪一层一目了然。

嗯,描述符这部分就讲到这里。记住一句话:描述符是主机和设备的沟通语言,写错一个字节,沟通就断了。写描述符时多检查几遍wTotalLength和bLength,这两个字段是枚举成败的关键。