4. USB枚举流程概述

USB 枚举是主机(Host)与设备(Device)建立通信前必须完成的初始化握手过程。枚举的本质是主机通过默认地址(Address 0)向设备请求一系列标准描述符,并根据描述符信息为设备分配唯一地址、选择工作配置。本章将按时间顺序拆解枚举的 6 个核心阶段,并给出完整的时序关系。

4.1 枚举全流程阶段划分

一次完整的 USB 枚举包含以下 6 个阶段,每个阶段都依赖前一个阶段的结果:

阶段 触发条件 主机动作 设备响应 关键协议要素
1. 设备插入检测 D+/D- 电平变化 检测到 J/K 状态,启动轮询 上拉电阻使能,信号线状态切换 Hub 中断 + 端口状态变化
2. 复位(Reset) 检测到设备存在 驱动 D+/D- 为 SE0 持续 ≥10ms 复位后进入默认状态,地址为 0 SE0 信号、复位恢复时间
3. 地址分配(Set Address) 复位完成 发送 SET_ADDRESS 请求 存储新地址,返回 0 长度状态包 控制传输、状态阶段握手
4. 设备描述符请求 地址生效 发送 GET_DESCRIPTOR (Device) 返回 18 字节设备描述符 bMaxPacketSize0 关键字段
5. 配置描述符请求 获取设备描述符后 发送 GET_DESCRIPTOR (Configuration) 返回配置描述符 + 接口/端点描述符 描述符集合长度、端点数量
6. 配置选择(Set Configuration) 解析描述符完成 发送 SET_CONFIGURATION 请求 按配置编号激活内部功能 bConfigurationValue 匹配
关键理解: 枚举的前 3 个阶段(检测→复位→地址分配)是“建立通信基础”,后 3 个阶段(设备描述符→配置描述符→配置选择)是“获取设备能力并激活”。地址分配是分水岭:在此之前设备使用默认地址 0,在此之后使用主机分配的唯一地址。

4.2 设备插入检测与速度识别

USB 总线上的设备插入检测由 Hub 或 Root Hub 的端口状态机完成。当设备插入时,设备端的上拉电阻会将 D+ 或 D- 线拉高,产生可被主机检测到的电平变化。

  • 低速设备(1.5 Mbps): 上拉电阻接在 D- 线。
  • 全速设备(12 Mbps): 上拉电阻接在 D+ 线。
  • 高速设备(480 Mbps): 先以全速模式插入,再通过 Chirp 握手协商切换到高速。

主机侧的 Hub 驱动通过轮询端口状态寄存器(如 PORTSC 寄存器中的 Port Connect Status Change 位)来感知插入事件。

驱动开发要点: 在 Linux 内核中,Hub 驱动通过 hub_events() 工作队列处理端口状态变化。当检测到 USB_PORT_STAT_CONNECTION 置位时,内核会启动 hub_port_connect_change() 函数,进入枚举流程。

4.3 复位(Reset)与默认状态

主机检测到设备插入后,必须对端口执行复位操作。复位信号通过将 D+ 和 D- 同时拉低(SE0)并保持至少 10ms 来实现。复位完成后:

  • 设备地址被重置为 0(默认地址)。
  • 设备进入 Default 状态,准备接收控制传输。
  • 主机通过读取端口状态寄存器确认复位完成(Port Reset Change 位被置位)。
常见陷阱: 复位时间不足 10ms 可能导致设备无法正确进入默认状态。部分低速设备需要更长的复位时间(最多 20ms)。驱动中应使用 msleep(10) 或等待端口状态位确认,而不是硬编码固定延时。

4.4 地址分配(Set Address)

复位完成后,主机通过默认地址 0 向设备发送 SET_ADDRESS 标准请求。这是枚举过程中第一个控制传输,使用 控制写传输(bmRequestType = 0x00,bRequest = 0x05)。

地址分配的控制传输分为三个阶段:

  1. 设置阶段(Setup): 主机发送 8 字节设置包,包含新地址值(1~127)。
  2. 数据阶段(Data): 无数据(wLength = 0)。
  3. 状态阶段(Status): 设备返回 0 长度 IN 包确认。

设备在收到 SET_ADDRESS 后,必须在状态阶段完成后立即切换到新地址。后续所有通信都使用新地址。

/* Linux 内核中 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_sndctrlpipe(udev, 0),
                             USB_REQ_SET_ADDRESS, 0, devnum, 0,
                             NULL, 0, USB_CTRL_SET_TIMEOUT);
    if (retval == 0) {
        /* 等待设备完成地址切换 */
        msleep(2);
        udev->devnum = devnum;
    }
    return retval;
}

4.5 描述符请求:获取设备能力

地址分配完成后,主机开始通过 GET_DESCRIPTOR 请求获取设备的描述符信息。描述符是 USB 设备的“身份证”,以层级结构描述设备的能力。

4.5.1 设备描述符(Device Descriptor)

主机首先请求设备描述符,长度为 18 字节。关键字段包括:

  • bcdUSB: USB 规范版本号(如 0x0200 表示 USB 2.0)。
  • bDeviceClass / bDeviceSubClass / bDeviceProtocol: 设备类、子类、协议代码。
  • bMaxPacketSize0: 端点 0 的最大包大小(8/16/32/64 字节)。
  • idVendor / idProduct: 厂商 ID 和产品 ID。
驱动优化技巧: 第一次请求设备描述符时,主机通常只读取前 8 字节(因为此时还不知道 bMaxPacketSize0)。读取后根据 bMaxPacketSize0 调整端点 0 的最大包大小,再重新读取完整的 18 字节。这样可以避免因包大小不匹配导致的通信失败。

4.5.2 配置描述符集合(Configuration Descriptor Set)

获取设备描述符后,主机请求配置描述符。配置描述符本身长度为 9 字节,但它后面会跟随:

  • 一个或多个 接口描述符(Interface Descriptor)。
  • 每个接口描述符后面跟随零个或多个 端点描述符(Endpoint Descriptor)。

主机通过 wTotalLength 字段知道整个描述符集合的总长度,然后一次性读取完整集合。

描述符类型 长度(字节) 关键字段 作用
配置描述符 9 bConfigurationValue, bNumInterfaces, wTotalLength 描述配置的总体属性
接口描述符 9 bInterfaceNumber, bAlternateSetting, bNumEndpoints 描述功能接口
端点描述符 7 bEndpointAddress, bmAttributes, wMaxPacketSize 描述数据传输端点

4.6 配置选择(Set Configuration)

解析完配置描述符集合后,主机根据设备类驱动或用户空间的配置策略,选择一个合适的配置值,发送 SET_CONFIGURATION 请求(bRequest = 0x09)。

  • 请求格式: bmRequestType = 0x00,wValue = 选中的 bConfigurationValue。
  • 设备响应: 设备根据配置值激活对应的接口和端点,进入 Configured 状态。
  • 枚举完成标志: 配置选择成功后,设备可以开始正常的数据传输(中断传输、批量传输、等时传输)。
实际案例: 一个 USB 摄像头可能提供两个配置:配置 1(视频流 + 音频)和配置 2(仅视频流)。主机驱动根据用户需求选择配置 1 或 2。如果选择配置 1,设备会同时激活视频接口和音频接口,以及它们对应的端点。

4.7 枚举全流程时序图

下面的 SVG 时序图展示了从设备插入到配置完成的完整枚举流程,包含主机与设备之间的交互步骤和时间顺序。

USB 枚举全流程时序图 USB 主机(Host) USB 设备(Device) ① 设备插入:D+/D- 电平变化,Hub 检测到连接 ② 复位:主机驱动 SE0 信号 ≥10ms,设备地址重置为 0 ③ 地址分配:SET_ADDRESS → 设备确认 → 切换到新地址 ④ 设备描述符:GET_DESCRIPTOR → 返回 18 字节描述符 ⑤ 配置描述符:GET_DESCRIPTOR → 返回配置 + 接口 + 端点 ⑥ 配置选择:SET_CONFIGURATION → 设备进入 Configured 状态 枚举完成,设备就绪 时间

4.8 枚举过程中的关键状态机

USB 设备在枚举过程中经历以下状态转换,每个状态对应特定的主机操作:

  1. Attached: 设备物理连接到端口。
  2. Powered: 端口供电(VBUS 有效)。
  3. Default: 复位完成,地址为 0,可响应控制传输。
  4. Address: 收到 SET_ADDRESS 并确认,使用新地址。
  5. Configured: 收到 SET_CONFIGURATION,功能接口激活。
调试经验: 如果枚举在“地址分配”阶段失败,常见原因是设备未正确响应状态阶段握手。检查控制传输的超时设置(通常为 5000ms)和端点 0 的最大包大小是否匹配。如果枚举在“配置描述符”阶段失败,通常是因为描述符长度字段(wTotalLength)与实际返回的数据长度不一致。

4.9 枚举流程的驱动实现要点

在 Linux 内核中,枚举流程主要由 drivers/usb/core/hub.c 和 drivers/usb/core/generic.c 实现。以下是驱动开发者需要关注的关键函数调用链:

/* Linux 内核枚举主路径(简化) */
hub_port_connect_change()          // 检测到端口变化
  └─ hub_port_init()               // 复位 + 地址分配
       ├─ hub_set_address()        // 发送 SET_ADDRESS
       └─ usb_get_device_descriptor() // 获取设备描述符
  └─ usb_new_device()              // 创建 USB 设备对象
       └─ usb_enumerate_device()   // 获取配置描述符
            └─ usb_get_configuration() // 读取配置描述符集合
       └─ usb_choose_configuration()   // 选择最佳配置
       └─ usb_set_configuration()      // 发送 SET_CONFIGURATION
实战建议: 在调试自定义 USB 设备时,建议在主机端使用 usbmon 或 lsusb -v 抓取枚举阶段的控制传输数据。对比标准枚举流程,可以快速定位是哪个阶段出现问题。例如,如果 lsusb -v 显示 bMaxPacketSize0 = 64 但设备实际只支持 8 字节,则枚举会在第一次控制传输后失败。

4.10 本章小结

USB 枚举是一个严格按序执行的 6 阶段过程:

  • 检测 → 复位 → 地址分配 建立通信基础。
  • 设备描述符 → 配置描述符 获取设备能力。
  • 配置选择 激活设备功能。