USB设备枚举过程:描述符家族详解
枚举,说白了就是USB主机和设备之间的「握手」过程。设备插上去,主机得知道你是谁、需要多少带宽、用什么协议通信。我刚开始做USB驱动时,总觉得枚举是硬件自动完成的,直到有一次设备死活枚举失败,才老老实实把描述符从头啃了一遍。
枚举的核心,就是主机通过控制传输,一条一条地读取设备的各种描述符。这些描述符就像设备的身份证和简历,一层套一层。咱们今天就把这五类描述符拆开揉碎了讲。
2.1 设备描述符 —— 第一张名片
主机枚举的第一步,就是发送GET_DESCRIPTOR(DEVICE)请求。设备必须返回18字节的设备描述符。这是所有描述符的根,后面所有的配置、接口、端点都挂在它下面。
我印象很深,有一次客户反馈设备在Windows上能识别,在Linux上却不行。查了半天,发现是bcdUSB字段填了0x0110,但实际硬件只支持全速。Linux对版本号检查更严格,直接不认。嗯,这里要注意:版本号必须如实反映硬件能力。
| 偏移 | 字段 | 长度 | 说明 |
|---|---|---|---|
| 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最大包长,全速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 | 支持的配置数量,通常为1 |
bMaxPacketSize0填了8,但实际硬件支持64字节。结果主机用8字节包通信,效率极低。这个字段必须如实填写,全速设备固定64,低速设备固定8。
2.2 配置描述符 —— 设备的「功能清单」
读完设备描述符,主机就知道该用多大的包来继续通信了。接下来,主机请求配置描述符。配置描述符本身只有9字节,但它后面会跟着一串接口描述符和端点描述符。
你想想看,一个设备可能有多种工作模式。比如一个USB采集卡,插上电脑可以是音频设备,也可以是MIDI设备。每种模式就是一个配置。主机通过SET_CONFIGURATION请求来激活其中一个配置。
// 配置描述符结构(9字节)
typedef struct {
uint8_t bLength; // 0x09
uint8_t bDescriptorType; // 0x02
uint16_t wTotalLength; // 配置描述符+所有接口和端点描述符的总长度
uint8_t bNumInterfaces; // 该配置下的接口数量
uint8_t bConfigurationValue;// 配置值,用于SET_CONFIGURATION
uint8_t iConfiguration; // 配置字符串索引
uint8_t bmAttributes; // 属性:自供电/总线供电/远程唤醒
uint8_t bMaxPower; // 最大电流,单位2mA
} usb_config_descriptor_t;
wTotalLength这个字段特别容易出错。它包含的是整个配置描述符集合的总长度,包括配置描述符本身、所有接口描述符、所有端点描述符,以及可能存在的类特定描述符。填少了,主机读不全;填多了,主机多读可能超时。
2.3 接口描述符 —— 功能的「分组」
一个配置下面可以有多个接口。接口描述符描述的是某个功能单元的属性。比如一个USB摄像头,它可能有视频接口和音频接口,每个接口对应一个独立的逻辑功能。
我个人习惯把接口描述符理解为「功能分组」。主机通过SET_INTERFACE请求来选择使用哪个备选接口(Alternate Setting)。这个机制在流式传输中特别有用——你可以切换备选接口来改变带宽。
| 字段 | 说明 |
|---|---|
| bInterfaceNumber | 接口编号,从0开始 |
| bAlternateSetting | 备选接口编号,用于切换不同带宽配置 |
| bNumEndpoints | 该接口使用的端点数量(不包括端点0) |
| bInterfaceClass | 接口类代码,如HID类为0x03 |
| bInterfaceSubClass | 接口子类 |
| bInterfaceProtocol | 接口协议 |
| iInterface | 接口字符串索引 |
2.4 端点描述符 —— 数据的「管道」
端点描述符是描述符家族里最底层的成员。每个端点描述符7字节,定义了数据传输的通道属性。端点0是默认控制端点,不需要描述符。其他端点必须通过描述符向主机声明。
说白了,端点就是设备内部的一个数据缓冲区。主机通过端点地址来读写数据。端点描述符告诉主机:这个端点是输入还是输出?传输类型是什么?包大小是多少?
// 端点描述符结构(7字节)
typedef struct {
uint8_t bLength; // 0x07
uint8_t bDescriptorType; // 0x05
uint8_t bEndpointAddress; // 端点地址:bit7方向(0=OUT,1=IN),bit3-0端点号
uint8_t bmAttributes; // 传输类型:0=控制,1=等时,2=批量,3=中断
uint16_t wMaxPacketSize; // 最大包长
uint8_t bInterval; // 轮询间隔(中断/等时端点有效)
} usb_endpoint_descriptor_t;
这里有个容易踩的坑:bInterval字段。对于全速中断端点,单位是毫秒;对于高速中断端点,单位是125微秒(微帧)。我见过有人把全速设备的间隔写成1,结果主机以1ms轮询,CPU占用率飙升。嗯,要根据速度等级正确换算。
2.5 字符串描述符 —— 给人看的名字
字符串描述符是可选的。它提供人类可读的厂商名、产品名和序列号。主机通过索引值来请求对应的字符串。字符串描述符使用Unicode编码(UTF-16LE)。
我记得有一次,设备在Windows上显示乱码。查了半天,发现是字符串描述符的bLength字段算错了。字符串描述符的第一个字节是长度,第二个字节是类型码(0x03),后面才是Unicode字符串。长度必须包含这两个字节。
// 字符串描述符示例:"USB" 的Unicode表示
// 长度 = 2(头) + 3*2(三个字符) = 8
0x08, 0x03, 'U', 0x00, 'S', 0x00, 'B', 0x00
2.6 描述符的层级关系
说了这么多,咱们用一张图来理清关系。描述符的层级就像一棵树:设备描述符是根,下面挂着配置描述符,配置下面挂着接口描述符,接口下面挂着端点描述符。字符串描述符是独立的,通过索引引用。
2.7 枚举流程实战
了解了每个描述符,咱们来看看枚举的完整流程。主机枚举设备,大致分这么几步:
- 复位设备:主机检测到设备插入,对总线复位,设备地址恢复为0。
- 获取设备描述符:主机发送
GET_DESCRIPTOR(DEVICE),读取前8字节(先读前8字节是为了确定端点0的最大包长)。 - 再次复位:主机根据获取到的
bMaxPacketSize0重新配置,然后再次复位设备。 - 设置地址:主机通过
SET_ADDRESS请求给设备分配唯一地址。 - 获取完整设备描述符:主机用新地址再次请求完整的18字节设备描述符。
- 获取配置描述符集合:主机先请求9字节的配置描述符,得到
wTotalLength后,再请求完整集合。 - 解析描述符:主机解析配置描述符集合,提取所有接口和端点描述符。
- 加载驱动:主机根据VID/PID或接口类代码,加载对应的设备驱动。
- 设置配置:驱动发送
SET_CONFIGURATION,激活设备。
bMaxPacketSize0后,后续通信才能用正确的包长。
枚举过程中,任何一个描述符出错,都可能导致设备无法被识别。我建议你在开发时,用USB分析仪抓包看看枚举过程。亲眼看到主机发了什么请求、设备回了什么数据,比看任何文档都直观。
嗯,描述符这块内容比较多,但它是USB开发的基石。你把这些描述符的结构和关系记牢了,后面做设备驱动、调试枚举问题,心里就有底了。