4. USB 设备枚举过程:描述符解析
USB 设备枚举是主机(Host)识别并配置新接入 USB 设备的完整流程。在枚举过程中,主机通过控制传输(Control Transfer)逐级获取设备的描述符(Descriptor),从而了解设备的类型、能力、供电需求以及端点(Endpoint)配置。对于驱动开发者而言,理解枚举过程是正确解析设备拓扑、匹配驱动以及配置 URB(USB Request Block)回调的基础。
4.1 枚举流程概览
当 USB 设备插入端口后,主机控制器检测到电平变化,触发以下标准枚举步骤:
- 设备连接与复位:主机检测到设备插入,向端口发送复位信号(Reset),设备地址恢复为默认地址 0。
- 获取设备描述符(前 8 字节):主机通过默认地址 0 发送
GET_DESCRIPTOR请求,获取设备描述符的前 8 字节,以确定端点 0 的最大包长度(bMaxPacketSize0)。 - 再次复位:主机根据获取到的最大包长度重新配置端口,并再次复位设备。
- 设置地址:主机发送
SET_ADDRESS请求,为设备分配一个唯一的地址(1~127)。 - 获取完整设备描述符:主机使用新地址再次获取完整的设备描述符(18 字节)。
- 获取配置描述符:主机发送
GET_DESCRIPTOR请求获取配置描述符(9 字节),随后获取完整的配置描述符集合(包括接口描述符和端点描述符)。 - 选择配置:主机发送
SET_CONFIGURATION请求,激活设备的某个配置(通常为第一个配置)。
4.2 设备描述符(Device Descriptor)
设备描述符是 USB 设备的第一级描述符,描述了设备的整体信息。主机在枚举初期首先获取该描述符。其结构由 struct usb_device_descriptor 定义(include/uapi/linux/usb/ch9.h)。
| 字段 | 偏移(字节) | 大小(字节) | 说明 |
|---|---|---|---|
bLength |
0 | 1 | 描述符长度(固定 18) |
bDescriptorType |
1 | 1 | 描述符类型(设备描述符为 0x01) |
bcdUSB |
2 | 2 | USB 规范版本号(BCD 码,如 0x0200 表示 USB 2.0) |
bDeviceClass |
4 | 1 | 设备类代码(0x00 表示在接口描述符中定义) |
bDeviceSubClass |
5 | 1 | 设备子类代码 |
bDeviceProtocol |
6 | 1 | 设备协议代码 |
bMaxPacketSize0 |
7 | 1 | 端点 0 的最大包长度(8/16/32/64) |
idVendor |
8 | 2 | 厂商 ID(VID) |
idProduct |
10 | 2 | 产品 ID(PID) |
bcdDevice |
12 | 2 | 设备版本号(BCD 码) |
iManufacturer |
14 | 1 | 厂商字符串描述符索引 |
iProduct |
15 | 1 | 产品字符串描述符索引 |
iSerialNumber |
16 | 1 | 序列号字符串描述符索引 |
bNumConfigurations |
17 | 1 | 支持的配置描述符数量 |
idVendor 和 idProduct 来匹配设备。在 Linux 内核中,USB 驱动使用 usb_device_id 结构体声明支持的 VID/PID 组合。
4.3 配置描述符(Configuration Descriptor)
配置描述符描述设备的供电模式、接口数量以及整体功耗。一个设备可以有多个配置(Configuration),但同一时刻只能激活一个。配置描述符的结构由 struct usb_config_descriptor 定义。
| 字段 | 偏移(字节) | 大小(字节) | 说明 |
|---|---|---|---|
bLength |
0 | 1 | 描述符长度(固定 9) |
bDescriptorType |
1 | 1 | 描述符类型(配置描述符为 0x02) |
wTotalLength |
2 | 2 | 配置描述符集合的总长度(包括接口和端点描述符) |
bNumInterfaces |
4 | 1 | 该配置下包含的接口数量 |
bConfigurationValue |
5 | 1 | 用于 SET_CONFIGURATION 请求的配置值 |
iConfiguration |
6 | 1 | 配置字符串描述符索引 |
bmAttributes |
7 | 1 | 配置特性(自供电/远程唤醒等) |
bMaxPower |
8 | 1 | 最大电流(单位 2mA,如 50 表示 100mA) |
wTotalLength 字段非常重要。主机在获取配置描述符时,先读取前 9 字节得到 wTotalLength,然后根据该长度读取完整的配置描述符集合。驱动开发者在解析配置时,必须遍历该集合以提取所有接口和端点描述符。
4.4 接口描述符(Interface Descriptor)
接口描述符描述一个功能接口(Interface),一个配置可以包含多个接口。接口是驱动绑定的基本单位——一个 USB 驱动通常绑定到一个接口上。接口描述符由 struct usb_interface_descriptor 定义。
| 字段 | 偏移(字节) | 大小(字节) | 说明 |
|---|---|---|---|
bLength |
0 | 1 | 描述符长度(固定 9) |
bDescriptorType |
1 | 1 | 描述符类型(接口描述符为 0x04) |
bInterfaceNumber |
2 | 1 | 接口编号(从 0 开始) |
bAlternateSetting |
3 | 1 | 备用设置编号(用于切换接口配置) |
bNumEndpoints |
4 | 1 | 该接口使用的端点数量(不包括端点 0) |
bInterfaceClass |
5 | 1 | 接口类代码 |
bInterfaceSubClass |
6 | 1 | 接口子类代码 |
bInterfaceProtocol |
7 | 1 | 接口协议代码 |
iInterface |
8 | 1 | 接口字符串描述符索引 |
bInterfaceClass、bInterfaceSubClass、bInterfaceProtocol 以及 VID/PID 来匹配已注册的 usb_driver。匹配成功后,驱动核心调用驱动的 probe() 函数,并将接口描述符信息传递给驱动。
4.5 端点描述符(Endpoint Descriptor)
端点描述符是驱动开发者最关注的描述符,它定义了除默认端点(端点 0)之外的所有端点的属性。对于中断端点(Interrupt Endpoint)的 URB 回调配置,端点描述符中的 bInterval 字段直接决定了 URB 的轮询间隔。端点描述符由 struct usb_endpoint_descriptor 定义。
| 字段 | 偏移(字节) | 大小(字节) | 说明 |
|---|---|---|---|
bLength |
0 | 1 | 描述符长度(固定 7) |
bDescriptorType |
1 | 1 | 描述符类型(端点描述符为 0x05) |
bEndpointAddress |
2 | 1 | 端点地址(位 3-0:端点号,位 7:方向 0=OUT,1=IN) |
bmAttributes |
3 | 1 | 端点属性(位 1-0:传输类型 0=控制,1=等时,2=批量,3=中断) |
wMaxPacketSize |
4 | 2 | 端点支持的最大包长度(字节) |
bInterval |
6 | 1 | 轮询间隔(单位:帧/微帧,中断端点必填) |
bEndpointAddress:驱动通过该字段确定 URB 的目标端点号和传输方向。例如0x81表示端点 1 IN。bmAttributes:位 1-0 必须为0x03(中断传输)。wMaxPacketSize:决定 URB 缓冲区大小。对于高速中断端点,该字段还包含每个微帧的额外事务数。bInterval:对于全速设备,单位为帧(1ms);对于高速设备,单位为微帧(125μs)。该值直接用于设置 URB 的interval字段。
4.6 内核中描述符的解析与使用
在 Linux 内核中,USB 核心在枚举完成后,会将解析好的描述符信息存储在 struct usb_device 和 struct usb_interface 结构体中。驱动开发者可以通过以下方式访问这些信息:
// 在 probe() 函数中获取接口和端点信息
static int my_driver_probe(struct usb_interface *intf,
const struct usb_device_id *id)
{
struct usb_device *udev = interface_to_usbdev(intf);
struct usb_host_interface *altsetting = intf->cur_altsetting;
struct usb_endpoint_descriptor *ep_desc;
int i;
// 打印设备描述符信息
dev_info(&intf->dev, "VID: 0x%04x, PID: 0x%04x\n",
udev->descriptor.idVendor,
udev->descriptor.idProduct);
// 遍历当前备用设置中的所有端点
for (i = 0; i < altsetting->desc.bNumEndpoints; i++) {
ep_desc = &altsetting->endpoint[i].desc;
// 检查是否为中断端点
if (usb_endpoint_is_int_in(ep_desc)) {
dev_info(&intf->dev,
"发现中断 IN 端点: 地址=0x%02x, 最大包长=%d, 间隔=%d\n",
ep_desc->bEndpointAddress,
le16_to_cpu(ep_desc->wMaxPacketSize),
ep_desc->bInterval);
// 保存端点信息供后续 URB 初始化使用
// ...
}
}
return 0;
}
usb_endpoint_is_int_in(epd):检查端点描述符是否为中断 IN 端点。usb_endpoint_is_int_out(epd):检查端点描述符是否为中断 OUT 端点。usb_endpoint_num(epd):从bEndpointAddress中提取端点号。usb_endpoint_dir_in(epd):检查端点方向是否为 IN。usb_endpoint_maxp(epd):获取wMaxPacketSize的值。usb_endpoint_interval(epd):获取bInterval的值(已转换为帧/微帧单位)。
4.7 枚举过程中的常见问题与调试
| 问题现象 | 可能原因 | 调试方法 |
|---|---|---|
| 设备无法被识别 | 设备描述符解析失败,端点 0 最大包长不匹配 | 使用 lsusb -v 查看设备描述符,检查 bMaxPacketSize0 |
| 驱动 probe 不被调用 | VID/PID 或接口类/子类/协议不匹配 | 检查 usb_device_id 表,使用 modprobe usbmon 抓取 URB 日志 |
| 中断 URB 提交失败 | 端点地址错误或端点类型不匹配 | 确认 bEndpointAddress 和 bmAttributes 字段 |
| 中断传输间隔异常 | bInterval 值未正确转换 |
使用 usb_endpoint_interval() 获取正确间隔值 |
lsusb -v:以详细模式列出所有 USB 设备的描述符信息。usbmon:内核 USB 监控工具,可以捕获所有 USB 传输的 URB 数据。/sys/kernel/debug/usb/devices:提供内核中 USB 设备树的详细描述符信息。