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画个枚举流程图,方便大家理解。
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等设备类开发就会轻松很多。