3. USB 设备枚举过程:描述符体系详解
USB 设备枚举是主机(Host)识别并配置 USB 设备的核心过程。在枚举阶段,主机通过默认控制管道(Endpoint 0)向设备发送标准请求,设备则以 描述符(Descriptor) 的形式返回自身的能力与属性。描述符是 USB 协议中定义的数据结构,采用层级嵌套的方式组织,从设备级到功能级逐层细化。
3.1 枚举流程总览
USB 设备插入后,主机控制器(Host Controller)检测到端口状态变化,启动枚举过程。枚举并非一次性读取所有描述符,而是分阶段、按需获取。标准枚举流程如下:
- 复位(Reset): 主机向设备发送复位信号,设备地址恢复为默认地址 0。
- 获取设备描述符(前 8 字节): 主机读取设备描述符的前 8 字节,获取端点 0 的最大包长度(bMaxPacketSize0)。
- 分配地址(Set Address): 主机为设备分配唯一地址,设备切换到新地址通信。
- 获取完整设备描述符: 主机使用新地址读取完整的设备描述符(18 字节)。
- 获取配置描述符(含接口和端点): 主机读取配置描述符,并连带读取该配置下的所有接口描述符和端点描述符。
- 获取字符串描述符(可选): 如果描述符中指定了字符串索引,主机可请求对应的字符串描述符(如厂商名、产品名、序列号)。
- 选择配置(Set Configuration): 主机选择一个配置,设备进入配置状态,功能可用。
3.2 设备描述符 (Device Descriptor)
设备描述符是 USB 设备的根描述符,每个设备有且仅有一个。它描述了设备的通用属性,包括 USB 规范版本、设备类、厂商 ID(VID)、产品 ID(PID)等。主机通过 GET_DESCRIPTOR 请求(bmRequestType = 0x80, bRequest = 0x06, wValue = 0x0100)读取。
| 偏移 | 字段 | 大小 | 说明 |
|---|---|---|---|
| 0 | bLength | 1 | 描述符长度,固定为 18 |
| 1 | bDescriptorType | 1 | 描述符类型,设备描述符为 0x01 |
| 2-3 | bcdUSB | 2 | USB 规范版本号(BCD 码),如 0x0200 表示 USB 2.0 |
| 4 | bDeviceClass | 1 | 设备类代码(0x00 表示在接口描述符中定义) |
| 5 | bDeviceSubClass | 1 | 设备子类代码 |
| 6 | bDeviceProtocol | 1 | 设备协议代码 |
| 7 | bMaxPacketSize0 | 1 | 端点 0 的最大包长度(8/16/32/64) |
| 8-9 | idVendor | 2 | 厂商 ID(VID),由 USB-IF 分配 |
| 10-11 | idProduct | 2 | 产品 ID(PID),由厂商自行分配 |
| 12-13 | bcdDevice | 2 | 设备版本号(BCD 码) |
| 14 | iManufacturer | 1 | 厂商字符串描述符的索引 |
| 15 | iProduct | 1 | 产品字符串描述符的索引 |
| 16 | iSerialNumber | 1 | 序列号字符串描述符的索引 |
| 17 | bNumConfigurations | 1 | 支持的配置描述符数量 |
3.3 配置描述符 (Configuration Descriptor)
配置描述符描述了一个 USB 设备的供电模式、最大电流消耗以及该配置下包含的接口数量。一个设备可以有多个配置(如低功耗模式和高性能模式),但同一时刻只能有一个配置被激活。配置描述符的 wTotalLength 字段指明了整个配置描述符集(包括配置描述符本身 + 所有接口描述符 + 所有端点描述符)的总长度。
| 偏移 | 字段 | 大小 | 说明 |
|---|---|---|---|
| 0 | bLength | 1 | 描述符长度,固定为 9 |
| 1 | bDescriptorType | 1 | 描述符类型,配置描述符为 0x02 |
| 2-3 | wTotalLength | 2 | 整个配置描述符集的总字节数 |
| 4 | bNumInterfaces | 1 | 该配置支持的接口数量 |
| 5 | bConfigurationValue | 1 | 用于 Set Configuration 请求的配置值 |
| 6 | iConfiguration | 1 | 配置字符串描述符的索引 |
| 7 | bmAttributes | 1 | 供电模式标志(Bit7: 总线供电, Bit6: 自供电, Bit5: 远程唤醒) |
| 8 | bMaxPower | 1 | 最大电流消耗,单位 2mA(值 × 2 = mA) |
3.4 接口描述符 (Interface Descriptor)
接口描述符是驱动开发中最关键的描述符。它描述了一个 USB 设备的功能接口,包括接口的类、子类、协议以及使用的端点数量。一个配置下可以有多个接口,每个接口可以有一个或多个备用设置(Alternate Setting)。接口描述符中的 bInterfaceClass、bInterfaceSubClass、bInterfaceProtocol 是 Linux USB 驱动匹配的核心依据。
| 偏移 | 字段 | 大小 | 说明 |
|---|---|---|---|
| 0 | bLength | 1 | 描述符长度,固定为 9 |
| 1 | bDescriptorType | 1 | 描述符类型,接口描述符为 0x04 |
| 2 | bInterfaceNumber | 1 | 接口编号(从 0 开始) |
| 3 | bAlternateSetting | 1 | 备用设置编号(用于切换接口配置) |
| 4 | bNumEndpoints | 1 | 该接口使用的端点数量(不包括端点 0) |
| 5 | bInterfaceClass | 1 | 接口类代码(如 HID=0x03, Mass Storage=0x08, CDC=0x02) |
| 6 | bInterfaceSubClass | 1 | 接口子类代码 |
| 7 | bInterfaceProtocol | 1 | 接口协议代码 |
| 8 | iInterface | 1 | 接口字符串描述符的索引 |
usb_register() 注册,并在 usb_driver 结构体的 id_table 中指定匹配的 USB_DEVICE_INFO(class, subclass, protocol) 宏。当枚举完成且主机选择配置后,USB 核心会遍历已注册的驱动,匹配接口描述符中的类/子类/协议。
3.5 端点描述符 (Endpoint Descriptor)
端点描述符描述了 USB 设备中除默认控制端点(Endpoint 0)以外的每个端点的属性,包括端点地址、传输类型、最大包大小和轮询间隔。每个接口描述符后面跟随若干个端点描述符(由 bNumEndpoints 指定)。
| 偏移 | 字段 | 大小 | 说明 |
|---|---|---|---|
| 0 | bLength | 1 | 描述符长度,固定为 7 |
| 1 | bDescriptorType | 1 | 描述符类型,端点描述符为 0x05 |
| 2 | bEndpointAddress | 1 | 端点地址(Bit3-0: 端点号, Bit7: 方向 0=OUT, 1=IN) |
| 3 | bmAttributes | 1 | 传输类型(Bit1-0: 0=控制, 1=等时, 2=批量, 3=中断) |
| 4-5 | wMaxPacketSize | 2 | 最大包大小(Bit10-0: 最大包字节数, Bit12-11: 附加事务数) |
| 6 | bInterval | 1 | 轮询间隔(单位:帧/微帧,取决于传输类型和速度) |
3.6 字符串描述符 (String Descriptor)
字符串描述符用于提供人类可读的文本信息,如厂商名称、产品名称和序列号。字符串描述符使用 Unicode(UTF-16LE)编码。设备描述符中的 iManufacturer、iProduct、iSerialNumber 字段以及配置/接口描述符中的 iConfiguration、iInterface 字段存储的是字符串描述符的索引值。主机通过 GET_DESCRIPTOR 请求(wValue 高字节为 0x03,低字节为索引值)读取。
字符串描述符 0(索引 0)是一个特殊的 语言 ID 描述符,它返回设备支持的语言代码列表(LANGID)。主机先读取索引 0 获取支持的语言,再使用指定的 LANGID 请求具体的字符串。
| 偏移 | 字段 | 大小 | 说明 |
|---|---|---|---|
| 0 | bLength | 1 | 描述符长度(可变,取决于字符串长度) |
| 1 | bDescriptorType | 1 | 描述符类型,字符串描述符为 0x03 |
| 2..N | bString / wLANGID | 可变 | 索引 0 时为 LANGID 数组(每个 2 字节);其他索引时为 Unicode 字符串 |
usb_string() 函数从设备获取字符串描述符内容。
3.7 描述符在 Linux 内核中的数据结构
Linux 内核 USB 核心层在枚举完成后,将解析后的描述符信息存储在对应的内核数据结构中。驱动开发者通过访问这些结构体来获取设备信息。
/* 设备描述符内核结构 (include/linux/usb/ch9.h) */
struct usb_device_descriptor {
__u8 bLength;
__u8 bDescriptorType;
__le16 bcdUSB;
__u8 bDeviceClass;
__u8 bDeviceSubClass;
__u8 bDeviceProtocol;
__u8 bMaxPacketSize0;
__le16 idVendor;
__le16 idProduct;
__le16 bcdDevice;
__u8 iManufacturer;
__u8 iProduct;
__u8 iSerialNumber;
__u8 bNumConfigurations;
} __attribute__ ((packed));
/* 配置描述符内核结构 */
struct usb_config_descriptor {
__u8 bLength;
__u8 bDescriptorType;
__le16 wTotalLength;
__u8 bNumInterfaces;
__u8 bConfigurationValue;
__u8 iConfiguration;
__u8 bmAttributes;
__u8 bMaxPower;
} __attribute__ ((packed));
/* 接口描述符内核结构 */
struct usb_interface_descriptor {
__u8 bLength;
__u8 bDescriptorType;
__u8 bInterfaceNumber;
__u8 bAlternateSetting;
__u8 bNumEndpoints;
__u8 bInterfaceClass;
__u8 bInterfaceSubClass;
__u8 bInterfaceProtocol;
__u8 iInterface;
} __attribute__ ((packed));
/* 端点描述符内核结构 */
struct usb_endpoint_descriptor {
__u8 bLength;
__u8 bDescriptorType;
__u8 bEndpointAddress;
__u8 bmAttributes;
__le16 wMaxPacketSize;
__u8 bInterval;
/* 注意:实际内核中还有额外的扩展字段用于 USB 3.0 等 */
} __attribute__ ((packed));
struct usb_interface *interface 参数可以获取 interface->cur_altsetting 指向当前备用设置的接口描述符,进而访问 desc 成员(struct usb_interface_descriptor)。端点描述符则通过 interface->cur_altsetting->endpoint 数组访问。
3.8 枚举过程中的常见问题与调试
- 描述符长度不匹配: 设备返回的描述符长度与请求不符,可能导致枚举失败。使用
usbmon或lsusb -v抓取描述符原始数据对比。 - bMaxPacketSize0 错误: 如果设备在复位后返回的 bMaxPacketSize0 与实际能力不符,后续控制传输可能失败。
- 配置描述符集总长度溢出: wTotalLength 必须精确等于所有描述符字节数之和,否则主机可能截断或解析错误。
- 接口备用设置混淆: 驱动应检查 bAlternateSetting,确保操作的是当前激活的备用设置。
- 字符串描述符索引无效: 如果描述符中的字符串索引为 0,表示没有对应的字符串,主机不应请求。
lsusb -v -d [VID:PID] 可完整打印设备的所有描述符信息。cat /sys/kernel/debug/usb/devices 可查看内核枚举后的设备树信息。usbmon 配合 tcpdump 风格过滤可捕获 USB 总线上的原始描述符请求与响应。