4. USB协议核心概念(下):USB描述符与枚举过程详解

上一讲我们聊了USB的电气特性和传输类型。这一讲,我们来啃USB协议里最绕、但也最重要的部分——描述符和枚举。

说实话,我刚接触USB时,最头疼的就是这一块。一堆嵌套的结构体,各种bLength、bDescriptorType,看得人眼花。但后来我发现,只要搞懂了描述符的“套娃”关系,USB开发就通了50%。

4.1 描述符:USB设备的“身份证”

USB描述符是什么?说白了,就是一套标准化的数据结构。设备插上主机后,主机啥也不知道,得靠描述符来告诉它:“我是谁、我有什么功能、我怎么用”。

描述符是分层的,像俄罗斯套娃一样。最外层是设备描述符,里面包着配置描述符,配置里又有接口描述符,接口里再包端点描述符。还有个字符串描述符,专门给人看的。

核心层级关系:

设备描述符 → 配置描述符 → 接口描述符 → 端点描述符

字符串描述符(可选,用于提供可读文本)

4.1.1 设备描述符(Device Descriptor)

这是最顶层的描述符,每个USB设备只有一个。它描述了设备的整体信息,比如USB版本、厂商ID(VID)、产品ID(PID)等。

我记得第一次做USB项目时,VID/PID搞反了,结果设备死活枚举不上。后来养成习惯,每次写描述符都对着USB-IF的规范核对一遍。

字段 长度(字节) 说明
bLength 1 描述符长度,固定18字节
bDescriptorType 1 描述符类型,设备描述符为0x01
bcdUSB 2 USB规范版本号,如0x0200表示USB 2.0
bDeviceClass 1 设备类代码,0x00表示在接口描述符中定义
bDeviceSubClass 1 设备子类代码
bDeviceProtocol 1 设备协议代码
bMaxPacketSize0 1 端点0的最大包大小
idVendor 2 厂商ID,由USB-IF分配
idProduct 2 产品ID,厂商自行定义
bcdDevice 2 设备版本号
iManufacturer 1 厂商字符串描述符的索引
iProduct 1 产品字符串描述符的索引
iSerialNumber 1 序列号字符串描述符的索引
bNumConfigurations 1 支持的配置数量

避坑指南:bMaxPacketSize0这个字段很关键。对于全速设备,它必须是8、16、32或64;高速设备固定为64。我曾经设了个8,结果高速枚举时主机直接报错,查了半天才发现是这里的问题。

4.1.2 配置描述符(Configuration Descriptor)

一个设备可以有多个配置,但同一时间只能激活一个。配置描述符描述了设备的功耗、供电方式等信息。

这里有个容易忽略的点:配置描述符的wTotalLength字段,它包含了整个配置下所有描述符的总长度。主机就是靠这个字段来一次性读取完整的配置信息。

字段 长度(字节) 说明
bLength 1 描述符长度,固定9字节
bDescriptorType 1 配置描述符类型,0x02
wTotalLength 2 整个配置的总长度(含所有子描述符)
bNumInterfaces 1 该配置支持的接口数量
bConfigurationValue 1 配置编号,用于SetConfiguration请求
iConfiguration 1 配置字符串描述符索引
bmAttributes 1 供电属性,D7=1表示总线供电,D6=1表示自供电
bMaxPower 1 最大功耗,单位2mA

4.1.3 接口描述符(Interface Descriptor)

接口描述符描述了一个功能单元。比如一个U盘可能有“大容量存储接口”和“人机交互接口”。每个接口可以包含多个端点。

这里有个概念容易混淆:接口交替设置(Alternate Setting)。同一个接口可以有多个交替设置,用于切换不同的工作模式。比如音频设备,可以用交替设置来切换采样率。

注意:接口描述符的bInterfaceNumber和bAlternateSetting组合起来唯一标识一个接口。如果bAlternateSetting为0,表示默认设置。主机通过SetInterface请求来切换交替设置。

4.1.4 端点描述符(Endpoint Descriptor)

端点是USB通信的终点。每个端点都有一个地址,由端点号和方向组成。端点0是默认的控制端点,所有设备都必须有。

端点描述符里有个字段叫bInterval,它表示端点的轮询间隔。对于中断传输和同步传输,这个字段特别重要。我做过一个HID设备,bInterval设得太小,导致CPU占用率飙升;设得太大,又感觉响应迟钝。后来调了个平衡值,才解决问题。

字段 长度(字节) 说明
bLength 1 描述符长度,固定7字节
bDescriptorType 1 端点描述符类型,0x05
bEndpointAddress 1 端点地址,Bit7=方向(0:OUT, 1:IN),Bit3-0=端点号
bmAttributes 1 传输类型:0=控制,1=等时,2=批量,3=中断
wMaxPacketSize 2 最大包大小
bInterval 1 轮询间隔,单位:帧(全速)或微帧(高速)

4.1.5 字符串描述符(String Descriptor)

字符串描述符是可选的,用于提供人类可读的文本信息,比如厂商名、产品名、序列号。它使用Unicode编码(UTF-16LE)。

字符串描述符的索引0比较特殊,它返回的是设备支持的语言ID列表。比如0x0409表示美式英语。

小技巧:如果设备不需要字符串描述符,可以把iManufacturer、iProduct等字段设为0。这样主机就不会去请求字符串了,能省点枚举时间。

4.2 枚举过程:设备与主机的“握手”

枚举,就是USB设备插入后,主机通过一系列标准请求来获取设备信息、分配地址、加载驱动的过程。这个过程完全由主机主导,设备被动响应。

我刚开始做USB开发时,总觉得枚举很神秘。后来用USB分析仪抓了一次包,才发现其实就是一问一答,挺简单的。

下面我用SVG画个枚举流程图,方便大家理解。

USB枚举过程流程图 USB枚举过程流程图 设备插入 主机复位设备 获取设备描述符(前8字节) 分配地址(SetAddress) 获取完整设备描述符(18字节) 获取配置描述符(含所有子描述符) 获取字符串描述符(可选) 加载驱动,枚举完成 说明 • 所有请求通过端点0发送 • 设备地址默认为0 • 主机按顺序执行

4.3 枚举中的关键请求

枚举过程中,主机通过控制传输发送一系列标准请求。这些请求的格式是固定的,设备必须正确响应。

标准请求的结构如下:

// USB标准请求结构
typedef struct {
    uint8_t  bmRequestType;  // 请求类型:方向、类型、接收者
    uint8_t  bRequest;       // 请求码
    uint16_t wValue;         // 值字段
    uint16_t wIndex;         // 索引字段
    uint16_t wLength;        // 数据阶段长度
} USB_SetupPacket;

常用的标准请求有:

  • GET_DESCRIPTOR(0x06):获取描述符,枚举中最常用的请求
  • SET_ADDRESS(0x05):分配设备地址
  • SET_CONFIGURATION(0x09):激活配置
  • GET_CONFIGURATION(0x08):获取当前配置
  • GET_STATUS(0x00):获取设备状态
  • CLEAR_FEATURE(0x01):清除特性
  • SET_FEATURE(0x03):设置特性

枚举过程中的关键时序:

1. 设备插入后,主机检测到D+/D-线上的电平变化

2. 主机发送复位信号(SE0状态持续至少10ms)

3. 设备在复位后,默认地址为0,端点0可用

4. 主机发送GET_DESCRIPTOR请求,获取设备描述符的前8字节

5. 主机根据bMaxPacketSize0确定端点0的最大包大小

6. 主机发送SET_ADDRESS请求,分配唯一地址

7. 主机使用新地址,获取完整的设备描述符

8. 主机获取配置描述符及所有子描述符

9. 主机根据描述符信息加载合适的驱动程序

10. 主机发送SET_CONFIGURATION请求,激活配置

4.4 枚举失败排查思路

枚举失败是USB开发中最常见的问题。我总结了几种典型情况:

现象 可能原因 排查方法
设备无响应 硬件连接问题、电源不足 检查D+/D-信号、测量VBUS电压
获取描述符失败 描述符数据错误、端点0包大小不匹配 用USB分析仪抓包,核对描述符内容
地址分配失败 设备未正确响应SET_ADDRESS 检查固件中地址处理逻辑
驱动加载失败 VID/PID不匹配、描述符不符合规范 检查设备描述符中的类/子类/协议字段

曾经踩过的坑:有一次我做的设备在Windows上枚举正常,但在Mac上就是不行。后来发现是配置描述符的wTotalLength算错了,少算了接口描述符的长度。Windows比较宽容,能自动修正;Mac比较严格,直接拒绝。从那以后,我每次写完描述符都会用USB验证工具检查一遍。

4.5 描述符在CubeMX中的配置

在CubeMX中生成USB设备工程时,描述符的配置主要在USB_DEVICE选项卡里。你可以直接填写VID、PID、厂商字符串等参数,CubeMX会自动生成对应的描述符数组。

但要注意,CubeMX生成的描述符是模板,很多时候需要手动调整。比如:

  • 修改端点数量或类型
  • 调整接口交替设置
  • 自定义字符串描述符内容

生成的描述符代码通常在usbd_desc.c文件中。我建议你打开看看,理解每一行代码的含义,而不是直接拿来就用。

个人习惯:我会在usbd_desc.c里加注释,标注每个字段的含义和取值范围。这样以后改起来方便,也方便团队其他人理解。

好了,这一讲的内容就到这里。描述符和枚举是USB开发的基础,理解了它们,后面的HID、CDC、MSC等设备类开发就会轻松很多。