4. USB枚举过程详解
USB 枚举是主机(Host)识别并配置新接入设备的核心流程。对于嵌入式 Linux 驱动开发者而言,枚举失败是导致设备无法工作的最常见原因,而理解枚举的每一个阶段是定位问题的前提。本章将深入剖析枚举的五个关键步骤,并结合 Linux 内核的驱动模型,解释每一步的底层机制与调试要点。
4.1 枚举全流程概览
当 USB 设备插入主机后,主机控制器(Host Controller)检测到端口状态变化,触发一系列由内核 USB 核心(USB Core)驱动的状态机转换。完整的枚举流程可归纳为以下五个阶段:
| 阶段 | 核心动作 | 关键数据结构/函数 | 常见失败原因 |
|---|---|---|---|
| 1. 复位 | 主机向设备发送复位信号(SE0),设备复位后进入默认地址 0 | hub_port_reset() |
硬件电气问题、复位时序不满足规范 |
| 2. 获取描述符 | 主机在地址 0 下请求设备描述符(前 8 字节),获取端点 0 最大包长 | usb_get_device_descriptor() |
设备无响应、描述符数据错误 |
| 3. 地址分配 | 主机分配唯一地址,设备切换到新地址 | usb_set_address() |
地址冲突、设备未正确切换地址 |
| 4. 配置选择 | 主机读取完整配置描述符,选择设备的工作配置 | usb_set_configuration() |
配置描述符解析失败、供电不足 |
| 5. 驱动绑定 | 内核根据接口描述符匹配驱动,调用驱动的 probe() 函数 |
usb_device_match() |
驱动未注册、匹配失败、probe 返回错误 |
调试提示: 在嵌入式系统中,枚举失败通常表现为 dmesg 中出现 device descriptor read/64, error -71 或 unable to enumerate USB device。此时应首先确认硬件电气连接是否正常,再逐阶段分析内核日志。
4.2 阶段一:复位(Reset)
复位是枚举的起点。主机控制器通过驱动 D+ 和 D- 线为低电平(SE0)并持续至少 10ms,强制设备进入默认状态。复位完成后,设备必须满足以下条件:
- 设备地址恢复为 0
- 设备处于默认状态(Default State)
- 端点 0 的最大包长(MaxPacketSize)为 8/16/32/64 字节(取决于设备速度)
在 Linux 内核中,复位操作由 hub_port_reset() 函数实现,该函数会调用主机控制器驱动的复位回调。复位成功后,内核会读取端口状态寄存器,确认设备已处于 USB_PORT_STAT_ENABLE 状态。
硬件排查要点: 如果复位后端口状态始终为 USB_PORT_STAT_RESET 或 USB_PORT_STAT_SUSPEND,请检查:
- VBUS 供电是否稳定(5V ±5%)
- D+/D- 信号线是否短路或断路
- 设备端的上拉电阻是否正确(全速设备在 D+ 上拉 1.5kΩ,低速设备在 D- 上拉)
4.3 阶段二:获取描述符(Get Descriptor)
复位成功后,主机在默认地址 0 下发送 GET_DESCRIPTOR 请求,读取设备描述符的前 8 个字节。这 8 个字节包含了端点 0 的最大包长(bMaxPacketSize0),主机据此调整后续通信的数据包大小。
Linux 内核中,此过程由 usb_get_device_descriptor() 函数完成。关键代码路径如下:
/* drivers/usb/core/hub.c */
static int hub_port_init(struct usb_hub *hub, struct usb_device *udev,
int port1, int delay)
{
// 1. 发送 GET_DESCRIPTOR 请求,读取前 8 字节
retval = usb_get_device_descriptor(udev, 8);
if (retval < 8) {
dev_err(&udev->dev, "device descriptor read/8, error %d\n", retval);
goto fail;
}
// 2. 根据 bMaxPacketSize0 更新端点 0 的通信参数
udev->ep0.desc.wMaxPacketSize = le16_to_cpu(udev->descriptor.bMaxPacketSize0);
// 3. 重新读取完整的设备描述符(18 字节)
retval = usb_get_device_descriptor(udev, USB_DT_DEVICE_SIZE);
...
}
调试技巧: 如果 dmesg 中出现 device descriptor read/64, error -71(-71 对应 EPROTO 协议错误),通常意味着设备在复位后未能正确响应。可能的原因包括:
- 设备固件未正确初始化端点 0
- USB 信号质量差(线缆过长、干扰大)
- 设备供电不足导致逻辑电路工作不稳定
4.4 阶段三:地址分配(Set Address)
主机通过 SET_ADDRESS 请求为设备分配一个唯一的 7 位地址(1~127)。设备收到请求后,必须在状态阶段完成后立即切换到新地址。这是枚举过程中最容易出现时序问题的环节。
Linux 内核中,地址分配由 usb_set_address() 函数实现:
/* drivers/usb/core/hub.c */
static int hub_set_address(struct usb_device *udev, int devnum)
{
int retval;
// 发送 SET_ADDRESS 请求
retval = usb_control_msg(udev, usb_sndaddr0pipe(),
USB_REQ_SET_ADDRESS, 0, devnum, 0,
NULL, 0, USB_CTRL_SET_TIMEOUT);
if (retval < 0) {
dev_err(&udev->dev, "device not accepting address %d, error %d\n",
devnum, retval);
return retval;
}
// 等待设备完成地址切换(USB 规范要求至少 2ms)
msleep(2);
// 更新内核中的设备地址
udev->devnum = devnum;
return 0;
}
常见失败场景: 地址分配失败时,内核日志通常显示 device not accepting address X, error -32(-32 对应 EPIPE 端点停止)。这往往是因为设备在收到 SET_ADDRESS 后,未能正确完成状态阶段(Status Stage)的握手。请检查:
- 设备固件是否在状态阶段正确返回 0 长度数据包
- 设备是否在 2ms 内完成了地址切换
- 是否存在地址冲突(罕见,但多设备同时枚举时可能发生)
4.5 阶段四:配置选择(Set Configuration)
地址分配完成后,主机读取完整的设备描述符、配置描述符、接口描述符和端点描述符。然后,主机根据设备支持的配置,选择一个合适的配置并发送 SET_CONFIGURATION 请求。设备收到请求后,必须按照所选配置初始化所有端点。
Linux 内核中,配置选择由 usb_set_configuration() 函数处理:
/* drivers/usb/core/message.c */
int usb_set_configuration(struct usb_device *dev, int configuration)
{
int retval;
// 1. 发送 SET_CONFIGURATION 请求
retval = usb_control_msg(dev, usb_sndctrlpipe(dev, 0),
USB_REQ_SET_CONFIGURATION, 0, configuration, 0,
NULL, 0, USB_CTRL_SET_TIMEOUT);
if (retval < 0) {
dev_err(&dev->dev, "can't set config #%d, error %d\n",
configuration, retval);
return retval;
}
// 2. 更新设备配置值
dev->actconfig = configuration;
// 3. 初始化所有端点(分配端点队列、设置端点类型等)
for (i = 0; i < dev->actconfig->desc.bNumInterfaces; i++) {
// 遍历接口和端点,调用 usb_enable_endpoint()
...
}
return 0;
}
配置选择失败分析: 如果 SET_CONFIGURATION 失败,最常见的原因是设备请求的供电电流超过主机端口的供电能力。USB 2.0 规范规定,一个端口最大供电为 500mA(高功率设备需协商)。此外,配置描述符中的 bNumInterfaces 和 bNumEndpoints 必须与实际硬件一致,否则设备可能无法正确初始化端点。
4.6 阶段五:驱动绑定(Driver Binding)
配置选择完成后,设备进入已配置状态(Configured State)。此时,Linux 内核的 USB 核心会根据设备接口描述符中的 bInterfaceClass、bInterfaceSubClass、bInterfaceProtocol 以及 idVendor/idProduct 等信息,在已注册的 USB 驱动列表中查找匹配项。
驱动匹配由 usb_device_match() 函数实现,匹配顺序如下:
- VID/PID 精确匹配: 驱动通过
usb_device_id表指定支持的 VID/PID - 类/子类/协议匹配: 驱动通过
bInterfaceClass等字段匹配通用类驱动 - 通用驱动匹配: 如
usb-storage、usbhid等
匹配成功后,内核调用驱动的 probe() 函数:
/* drivers/usb/core/driver.c */
static int usb_probe_device(struct device *dev)
{
struct usb_device *udev = to_usb_device(dev);
struct usb_driver *driver = to_usb_driver(dev->driver);
int error;
// 调用驱动的 probe 函数
error = driver->probe(udev);
if (error) {
dev_err(dev, "probe failed with error %d\n", error);
return error;
}
// 绑定成功,设备进入可用状态
return 0;
}
驱动绑定调试命令:
lsusb -t:查看 USB 设备树和驱动绑定情况cat /sys/kernel/debug/usb/devices:查看所有 USB 设备的描述符和驱动状态echo -n "driver_name" > /sys/bus/usb/devices/.../driver_override:强制绑定指定驱动
4.7 枚举失败的综合排查清单
当枚举失败时,建议按照以下清单逐项排查:
| 步骤 | 检查项 | 内核日志关键词 | 可能原因 |
|---|---|---|---|
| 1 | 硬件连接 | over-current |
VBUS 短路、端口过流保护 |
| 2 | 复位信号 | reset failed |
D+/D- 信号异常、设备未正确上拉 |
| 3 | 描述符读取 | device descriptor read/64, error -71 |
端点 0 未就绪、信号质量差 |
| 4 | 地址分配 | device not accepting address |
设备未正确切换地址、状态阶段失败 |
| 5 | 配置选择 | can't set config |
供电不足、配置描述符错误 |
| 6 | 驱动绑定 | probe failed |
驱动未注册、匹配失败、probe 初始化错误 |