USB设备枚举过程:描述符家族详解

枚举,说白了就是USB主机和设备之间的「握手」过程。设备插上去,主机得知道你是谁、需要多少带宽、用什么协议通信。我刚开始做USB驱动时,总觉得枚举是硬件自动完成的,直到有一次设备死活枚举失败,才老老实实把描述符从头啃了一遍。

枚举的核心,就是主机通过控制传输,一条一条地读取设备的各种描述符。这些描述符就像设备的身份证和简历,一层套一层。咱们今天就把这五类描述符拆开揉碎了讲。

枚举的本质: 主机通过默认控制管道(端点0)向设备请求描述符数据,设备按标准格式回复。整个过程由主机驱动主导,设备被动响应。

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
特别注意: 字符串描述符索引0是特殊的语言ID描述符。主机必须先请求索引0,获取设备支持的语言ID列表,然后再用具体的语言ID去请求其他字符串。如果设备只支持英语,语言ID通常是0x0409。

2.6 描述符的层级关系

说了这么多,咱们用一张图来理清关系。描述符的层级就像一棵树:设备描述符是根,下面挂着配置描述符,配置下面挂着接口描述符,接口下面挂着端点描述符。字符串描述符是独立的,通过索引引用。

USB描述符层级结构 设备描述符 配置描述符 接口描述符 0 接口描述符 1 端点描述符 IN 端点描述符 OUT 端点描述符 IN 端点描述符 OUT 字符串描述符(索引引用) 说明: 设备描述符是根,一个设备可以有多个配置描述符。每个配置下可以有多个接口描述符,每个接口下可以有多个端点描述符。 字符串描述符通过索引被其他描述符引用,不直接挂在层级中。

2.7 枚举流程实战

了解了每个描述符,咱们来看看枚举的完整流程。主机枚举设备,大致分这么几步:

  1. 复位设备:主机检测到设备插入,对总线复位,设备地址恢复为0。
  2. 获取设备描述符:主机发送GET_DESCRIPTOR(DEVICE),读取前8字节(先读前8字节是为了确定端点0的最大包长)。
  3. 再次复位:主机根据获取到的bMaxPacketSize0重新配置,然后再次复位设备。
  4. 设置地址:主机通过SET_ADDRESS请求给设备分配唯一地址。
  5. 获取完整设备描述符:主机用新地址再次请求完整的18字节设备描述符。
  6. 获取配置描述符集合:主机先请求9字节的配置描述符,得到wTotalLength后,再请求完整集合。
  7. 解析描述符:主机解析配置描述符集合,提取所有接口和端点描述符。
  8. 加载驱动:主机根据VID/PID或接口类代码,加载对应的设备驱动。
  9. 设置配置:驱动发送SET_CONFIGURATION,激活设备。
关键点: 步骤2中只读前8字节,是USB规范的一个优化技巧。因为此时主机还不知道端点0的最大包长,先读8字节确保不会超过任何设备的包长限制。拿到bMaxPacketSize0后,后续通信才能用正确的包长。

枚举过程中,任何一个描述符出错,都可能导致设备无法被识别。我建议你在开发时,用USB分析仪抓包看看枚举过程。亲眼看到主机发了什么请求、设备回了什么数据,比看任何文档都直观。

嗯,描述符这块内容比较多,但它是USB开发的基石。你把这些描述符的结构和关系记牢了,后面做设备驱动、调试枚举问题,心里就有底了。