4. USB描述符体系:设备描述符、配置描述符、接口描述符、端点描述符、字符串描述符的结构与解析

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

我刚开始接触USB开发时,总觉得描述符这东西很抽象。后来调了一个U盘的枚举失败问题,才发现是配置描述符里的bMaxPower写错了——主机认为设备功耗超标,直接拒绝连接。从那以后,我对描述符的每个字节都格外敏感。

4.1 描述符的层级关系

USB描述符是一个树形结构。主机通过标准请求,一层一层往下读。你想想看,主机怎么知道你的设备是什么?能干什么?怎么通信?全靠这套描述符体系。

层级关系是这样的:

  • 设备描述符:根节点,描述设备整体信息
  • 配置描述符:每个设备可以有多个配置,但同一时刻只能激活一个
  • 接口描述符:每个配置下可以有多个接口,接口代表功能
  • 端点描述符:每个接口下可以有多个端点,端点是通信管道
  • 字符串描述符:可选,提供可读的文字信息

核心要点:主机枚举设备时,先读设备描述符(18字节),再读配置描述符(9字节)+ 所有接口和端点描述符。字符串描述符是最后才读的,而且不是必须的。

我画了一张结构图,帮你理解这个层级关系:

USB 描述符层级结构 设备描述符 配置描述符 接口描述符 端点描述符 字符串描述符 说明: 1. 每个描述符都有bLength和 bDescriptorType字段 2. 字符串描述符通过索引引用 3. 端点0没有描述符 枚举顺序: 1. 获取设备描述符(18字节) 2. 设置地址 3. 获取配置描述符全集 4. 选择配置

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 描述符的解析流程

主机解析描述符的流程,说白了就是“先读个头,再读全身”。

  1. 获取设备描述符:主机发GetDescriptor(DEVICE),只读前8字节,拿到bMaxPacketSize0
  2. 设置地址:主机发SetAddress,设备进入Address状态
  3. 再次获取设备描述符:这次读全部18字节
  4. 获取配置描述符:先读9字节,从wTotalLength知道总长度
  5. 获取配置全集:按wTotalLength读全部数据,然后解析出接口和端点
  6. 获取字符串描述符:如果有字符串索引,主机再发请求读取
  7. 选择配置:主机发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等)可能有额外的类特定描述符,但标准描述符的结构是固定的。