4. USB 枚举过程:设备插入检测、复位、地址分配、读取描述符、配置设备、加载驱动、设备就绪的完整流程

USB 枚举是主机(Host)识别并配置一个新接入 USB 设备的完整过程。它是 USB 子系统中最核心的协议流程,也是驱动开发者必须透彻理解的第一道关卡。枚举过程由主机端的 USB 核心(USB Core)自动完成,设备端只需被动响应标准请求。本章将按时间顺序拆解枚举的 7 个关键阶段,并深入每个阶段背后的数据交互与状态机变迁。

理解要点: 枚举过程是主机主导的“一问一答”式交互。设备在枚举完成前处于默认地址(0)状态,主机通过控制传输(Control Transfer)逐级获取设备信息并分配资源。

4.1 枚举全流程总览

下图展示了从设备插入到驱动加载完成的完整状态机与数据流。每个阶段都对应一个特定的 USB 设备状态。

USB 枚举完整流程状态机 USB 枚举完整流程状态机 ① 设备插入 ② 总线复位 ③ 分配地址 ④ 读取描述符 ⑤ 配置设备 ⑥ 加载驱动 ⑦ 设备就绪 关键说明: • 所有阶段均通过默认管道(Endpoint 0)进行控制传输。 • 地址分配后,设备从默认地址(0)切换到新地址通信。 • 读取描述符阶段会依次获取设备描述符、配置描述符、接口描述符、端点描述符。 • 配置设备阶段主机发送 SET_CONFIGURATION 请求,设备进入 Configured 状态。 • 驱动加载由内核根据 bInterfaceClass/SubClass/Protocol 或 VID/PID 匹配。

4.2 阶段一:设备插入检测

当 USB 设备物理插入端口时,主机控制器(Host Controller)通过检测 D+ 或 D- 数据线上的电平变化来感知设备连接。对于全速/高速设备,D+ 被拉高;对于低速设备,D- 被拉高。

  • 硬件检测: 主机控制器轮询端口状态寄存器,发现端口状态变化位(Port Change)置位。
  • 内核通知: 主机控制器驱动调用 hub_irq() 或 hub_events(),触发 Hub 驱动处理端口事件。
  • 去抖动(Debounce): 等待 100ms 以消除物理接触抖动,确保连接稳定。
内核关键路径: hub_events() → hub_port_connect_change() → usb_alloc_dev() → usb_new_device()

4.3 阶段二:总线复位

主机确认设备连接后,立即向该端口发送复位信号(Reset)。复位信号将 D+ 和 D- 同时拉低至少 10ms,使设备回到初始状态。

复位参数 规范要求 内核实现
复位持续时间 ≥ 10ms 通常 10ms ~ 20ms
复位后设备状态 Default 状态,地址为 0 设备等待主机发送 SETUP 包
速度协商 高速设备通过 Chirp 握手 主机控制器检测设备速度

复位完成后,设备处于 Default 状态,使用默认地址 0 与主机通信。此时设备仅能响应控制传输,且只能接收来自主机的标准请求。

4.4 阶段三:分配地址

主机通过控制传输发送 SET_ADDRESS 请求,为设备分配一个唯一的 7 位地址(1~127)。这是枚举过程中第一个必须成功的关键步骤。

// 主机发送的 SET_ADDRESS 请求格式(控制传输 Setup 包)
// bmRequestType: 0x00 (主机到设备,标准请求,接收端为设备)
// bRequest:      0x05 (SET_ADDRESS)
// wValue:        新地址值 (例如 0x0002)
// wIndex:        0x0000
// wLength:       0x0000

// 内核中分配地址的核心函数 (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_snddefctrl(udev),
                             USB_REQ_SET_ADDRESS, 0,
                             devnum, 0, NULL, 0, USB_CTRL_SET_TIMEOUT);
    if (retval == 0) {
        // 地址分配成功后,设备切换到新地址
        udev->devnum = devnum;
        usb_set_device_state(udev, USB_STATE_ADDRESS);
    }
    return retval;
}
注意事项: SET_ADDRESS 请求发送后,主机必须等待至少 2ms 才能使用新地址与设备通信。设备在收到请求后立即切换到新地址,但主机侧需要延迟切换。

4.5 阶段四:读取描述符

地址分配完成后,主机开始通过新地址读取设备的描述符信息。描述符是 USB 设备的“身份证”,包含了设备类型、厂商信息、配置能力、接口功能、端点特性等全部关键数据。

4.5.1 描述符读取顺序

  1. 设备描述符(Device Descriptor): 固定 18 字节,包含 VID、PID、bcdUSB、bMaxPacketSize0 等。
  2. 配置描述符(Configuration Descriptor): 包含配置的总长度、接口数量、属性等。主机先读取前 9 字节获取总长度,再读取完整描述符。
  3. 接口描述符(Interface Descriptor): 描述一个功能接口的类、子类、协议、备用设置等。
  4. 端点描述符(Endpoint Descriptor): 描述每个端点的地址、方向、传输类型、最大包大小。
// 设备描述符结构 (include/uapi/linux/usb/ch9.h)
struct usb_device_descriptor {
    __u8  bLength;            // 描述符长度 (18)
    __u8  bDescriptorType;    // 描述符类型 (0x01)
    __le16 bcdUSB;            // USB 规范版本号 (BCD)
    __u8  bDeviceClass;       // 设备类
    __u8  bDeviceSubClass;    // 设备子类
    __u8  bDeviceProtocol;    // 设备协议
    __u8  bMaxPacketSize0;    // 端点 0 最大包大小
    __le16 idVendor;          // 厂商 ID (VID)
    __le16 idProduct;         // 产品 ID (PID)
    __le16 bcdDevice;         // 设备版本号
    __u8  iManufacturer;      // 厂商字符串索引
    __u8  iProduct;           // 产品字符串索引
    __u8  iSerialNumber;      // 序列号字符串索引
    __u8  bNumConfigurations; // 配置描述符数量
} __attribute__ ((packed));

4.5.2 描述符读取流程

描述符读取顺序与依赖关系 描述符读取顺序与依赖关系 1. 设备描述符 (18B) 2. 配置描述符 (9B) 3. 配置描述符 (完整) 4. 接口描述符 5. 端点描述符 (N个) 读取策略说明: • 主机先读设备描述符,获取 bMaxPacketSize0,确定端点 0 的最大包长。 • 再读配置描述符前 9 字节,获取 wTotalLength,确定完整配置大小。 • 最后读取完整配置描述符,其中包含接口和端点描述符。 • 字符串描述符按需读取,非必须。 • 每个配置下可以有多个接口,每个接口下可以有多个端点。

4.6 阶段五:配置设备

主机解析完所有描述符后,根据设备的能力和系统策略选择一个合适的配置(Configuration),发送 SET_CONFIGURATION 请求激活该配置。

  • 请求格式: SET_CONFIGURATION (bRequest = 0x09),wValue 为配置编号(bConfigurationValue)。
  • 设备响应: 设备进入 Configured 状态,所有被选中的接口和端点进入可用状态。
  • 多配置选择: 主机通常选择第一个配置(bConfigurationValue = 1),除非驱动有特殊要求。
内核实现: usb_set_configuration() 函数负责发送 SET_CONFIGURATION 请求,并解析配置描述符中的接口和端点信息,创建 usb_interface 和 usb_host_endpoint 数据结构。

4.7 阶段六:加载驱动

设备配置完成后,USB 核心根据设备的 接口信息 寻找匹配的驱动程序。匹配依据包括:

匹配方式 匹配依据 优先级 示例
VID/PID 精确匹配 idVendor + idProduct 最高 特定厂商的特定设备
类/子类/协议匹配 bInterfaceClass + bInterfaceSubClass + bInterfaceProtocol 中 HID 类、Mass Storage 类
通用驱动匹配 usb_generic_driver 最低 无专用驱动时的兜底
// USB 设备 ID 表示例 (驱动声明支持的设备)
static const struct usb_device_id my_usb_table[] = {
    // VID/PID 精确匹配
    { USB_DEVICE(0x1234, 0x5678) },
    // 类匹配 (HID 类)
    { USB_INTERFACE_INFO(USB_CLASS_HID, 0, 0) },
    // 结束标记
    { }
};
MODULE_DEVICE_TABLE(usb, my_usb_table);

驱动匹配成功后,内核调用驱动的 probe() 函数,驱动在此函数中完成初始化、分配资源、注册设备节点等操作。

4.8 阶段七:设备就绪

驱动加载并初始化完成后,设备进入 就绪状态。此时:

  • 用户空间可以通过 /dev/bus/usb/ 或驱动创建的字符设备节点访问设备。
  • 设备可以正常进行数据传输(中断传输、批量传输、等时传输)。
  • USB 核心将设备标记为 USB_STATE_CONFIGURED,并通知用户空间(udev)生成设备事件。
调试技巧: 使用 lsusb -v 可以查看枚举完成后设备的所有描述符信息。使用 usbmon 或 usbhid-dump 可以捕获枚举过程中的 USB 数据包,是分析枚举失败问题的利器。

4.9 枚举失败常见原因与排查

失败阶段 典型现象 可能原因 排查方法
设备插入检测 内核无任何日志 硬件连接问题、Hub 驱动未加载 检查 dmesg,确认 Hub 驱动正常
总线复位 复位超时 设备未正确响应复位信号 用示波器测量 D+/D- 电平
地址分配 SET_ADDRESS 失败 设备端点 0 异常、时序问题 检查 usbmon 捕获的 SETUP 包
读取描述符 描述符校验失败 bMaxPacketSize0 错误、描述符格式错误 对比 USB 规范检查描述符内容
配置设备 SET_CONFIGURATION 失败 设备不支持所选配置 检查 bNumConfigurations 和配置值
加载驱动 无驱动匹配 VID/PID 或类信息未注册 检查 modprobe 和 lsusb -t
关键提醒: 枚举过程中任何一步失败,设备都不会进入就绪状态。驱动开发者应首先确保设备在裸机(无驱动)情况下能够被主机正确枚举,然后再进行驱动开发。使用 dmesg -w 实时监控内核日志是排查枚举问题的最直接手段。