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- 上拉)
USB 复位信号时序与状态转换 主机控制器 SE0 (≥10ms) 设备复位 地址 → 0 默认状态 端点0 最大包长就绪 复位时序关键参数 • T0: 主机检测到设备插入,等待 100ms 去抖 • T1: 主机驱动 SE0 复位信号,持续 10ms ~ 20ms • T2: 复位结束后,设备必须在 10ms 内进入默认状态 • T3: 主机等待 10ms 后开始获取描述符

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 内完成了地址切换
  • 是否存在地址冲突(罕见,但多设备同时枚举时可能发生)
SET_ADDRESS 事务时序 主机 设备 SETUP: SET_ADDRESS(addr) 无数据阶段 状态阶段: 设备返回 0 长度 ACK 设备切换到新地址 ≥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() 函数实现,匹配顺序如下:

  1. VID/PID 精确匹配: 驱动通过 usb_device_id 表指定支持的 VID/PID
  2. 类/子类/协议匹配: 驱动通过 bInterfaceClass 等字段匹配通用类驱动
  3. 通用驱动匹配: 如 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;
}
Linux USB 驱动绑定流程 USB 设备 接口描述符 usb_device_match() VID/PID / 类匹配 USB 驱动列表 usb_driver 链表 driver->probe() 初始化设备 绑定成功 绑定失败

驱动绑定调试命令:

  • 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 初始化错误