4. 枚举流程总览:从连接复位到配置完成的完整状态机

USB 枚举是主机与设备之间建立通信会话的标准化握手过程。本章将枚举过程抽象为一个有限状态机(FSM),从设备物理连接开始,经过复位、地址分配、描述符获取,最终到达配置完成状态。理解这个状态机是定位枚举故障的基础。

核心观点: 枚举不是一次性的“请求-应答”,而是主机驱动的多阶段状态迁移。任何阶段超时、错误响应或协议违规都会导致状态机回退或挂起。

4.1 枚举状态机总览

下图展示了从设备插入到配置完成的完整状态迁移路径。每个椭圆代表一个状态,箭头上的标签代表触发迁移的事件或条件。

USB 枚举状态机总览 USB 枚举状态机总览 连接检测 总线复位 默认地址(0) 获取设备描述符 分配地址 获取配置描述符 配置设备 配置完成 VBUS 检测 复位结束 GET_DESCRIPTOR 解析成功 SET_ADDRESS 描述符完整 SET_CONFIG 失败/超时 → 复位 ⚠ 关键观察: 状态机是单向前进的,但任何阶段的失败(超时、STALL、CRC 错误)都会触发主机重新复位设备,回到“总线复位”状态。

4.2 状态机各阶段详解

枚举状态机包含 8 个核心阶段。下表列出了每个阶段的主机动作、设备响应要求以及典型故障模式。

阶段 主机动作 设备响应 典型故障
1. 连接检测 检测 D+/D- 电平变化,VBUS 上电 上拉电阻激活,信号线状态指示速度 上拉电阻缺失、电平冲突
2. 总线复位 驱动 D+/D- 为 SE0 状态 ≥ 10ms 复位内部状态机,地址恢复为 0 复位信号不足 10ms、设备未响应
3. 默认地址 设备在地址 0 等待第一个令牌包 在地址 0 监听并响应控制传输 地址 0 冲突、设备未就绪
4. 获取设备描述符 发送 GET_DESCRIPTOR (设备, 0) 返回 18 字节设备描述符 STALL、数据长度错误、描述符损坏
5. 分配地址 发送 SET_ADDRESS (新地址) 在状态阶段后切换到新地址 地址冲突、设备未切换地址
6. 获取配置描述符 发送 GET_DESCRIPTOR (配置, 0) 返回配置描述符 + 接口/端点描述符 描述符长度不匹配、缺少端点
7. 配置设备 发送 SET_CONFIGURATION (配置值) 激活指定配置,端点准备就绪 配置值无效、端点初始化失败
8. 配置完成 设备进入就绪状态,可进行数据传输 端点处于空闲或数据状态 配置未生效、驱动加载失败

4.3 关键时序与超时约束

USB 规范对枚举过程中的时序有严格定义。违反这些时序是导致枚举失败的最常见原因之一。

  • 复位持续时间: 主机必须驱动 SE0 至少 10ms,设备必须在 10ms 内完成复位。复位结束后,设备有 10ms 的恢复时间(TRSTRCY)。
  • 控制传输超时: 主机等待设备响应 SETUP 包的时间通常为 5s(USB 2.0 规范建议)。设备必须在 5s 内完成状态阶段。
  • 描述符请求间隔: 主机在获取设备描述符后,通常会在 100ms 内发送 SET_ADDRESS。设备必须在此窗口内保持地址 0 的监听状态。
  • 配置超时: 从 SET_CONFIGURATION 到设备端点就绪,主机等待时间通常为 50ms。超过此时间主机可能认为配置失败。
⚠ 故障排查提示: 使用逻辑分析仪抓取 D+/D- 信号时,重点测量复位脉冲宽度是否 ≥ 10ms。许多低成本主机控制器或劣质 USB HUB 会输出不足 10ms 的复位信号,导致设备无法正确复位。

4.4 状态机中的关键决策点

在枚举过程中,主机在每个阶段结束后都会进行决策。下图展示了主机在关键决策点的判断逻辑。

枚举关键决策点 枚举关键决策点流程图 收到设备响应 ACK? 是 否 (NAK/STALL) 校验数据合法性 数据有效? 是 → 进入下一状态 否 → 复位设备 重试/复位

4.5 枚举失败后的状态机回退

当枚举过程中出现不可恢复的错误时,主机不会无限等待,而是执行以下回退策略:

  1. 重试机制: 主机对每个请求最多重试 3 次。如果连续 3 次收到 STALL 或超时,则判定设备无响应。
  2. 总线复位: 判定设备无响应后,主机驱动总线复位,将设备重置到地址 0,重新开始枚举。
  3. 端口禁用: 如果连续 3 次完整枚举尝试均失败,主机将禁用该端口,并在系统日志中记录“枚举失败”事件。
  4. 电源循环: 某些主机控制器在端口禁用后,会尝试关闭 VBUS 再重新上电,以强制设备硬件复位。
💡 调试技巧: 在 Linux 系统中,使用 usbmon 或 lsusb -v 可以观察枚举过程中的 URB 请求序列。如果看到连续的 URB_NOTSHORT 或 -EPROTO 错误,说明设备在数据阶段出现了协议错误。

4.6 枚举流程的代码级观察

以下是一个简化的主机枚举状态机实现伪代码,展示了状态迁移的核心逻辑:

// 主机枚举状态机伪代码
enum usb_enum_state {
    ENUM_STATE_CONNECT,
    ENUM_STATE_RESET,
    ENUM_STATE_DEFAULT_ADDR,
    ENUM_STATE_GET_DEV_DESC,
    ENUM_STATE_SET_ADDR,
    ENUM_STATE_GET_CFG_DESC,
    ENUM_STATE_SET_CFG,
    ENUM_STATE_CONFIGURED,
    ENUM_STATE_FAILED
};

int usb_enum_device(struct usb_device *udev) {
    int retry = 3;
    enum usb_enum_state state = ENUM_STATE_CONNECT;

    while (state != ENUM_STATE_CONFIGURED && retry > 0) {
        switch (state) {
        case ENUM_STATE_CONNECT:
            if (detect_connection(udev) < 0) {
                retry--;
                break;
            }
            state = ENUM_STATE_RESET;
            break;

        case ENUM_STATE_RESET:
            if (usb_reset_device(udev) < 0) {
                retry--;
                state = ENUM_STATE_CONNECT;
                break;
            }
            state = ENUM_STATE_DEFAULT_ADDR;
            break;

        case ENUM_STATE_GET_DEV_DESC:
            if (usb_get_device_descriptor(udev) < 0) {
                retry--;
                state = ENUM_STATE_RESET;
                break;
            }
            state = ENUM_STATE_SET_ADDR;
            break;

        case ENUM_STATE_SET_ADDR:
            if (usb_set_address(udev, udev->devnum) < 0) {
                retry--;
                state = ENUM_STATE_RESET;
                break;
            }
            state = ENUM_STATE_GET_CFG_DESC;
            break;

        case ENUM_STATE_GET_CFG_DESC:
            if (usb_get_config_descriptor(udev) < 0) {
                retry--;
                state = ENUM_STATE_RESET;
                break;
            }
            state = ENUM_STATE_SET_CFG;
            break;

        case ENUM_STATE_SET_CFG:
            if (usb_set_configuration(udev, udev->config->desc.bConfigurationValue) < 0) {
                retry--;
                state = ENUM_STATE_RESET;
                break;
            }
            state = ENUM_STATE_CONFIGURED;
            break;

        default:
            break;
        }
    }
    return (state == ENUM_STATE_CONFIGURED) ? 0 : -ENODEV;
}
代码要点: 注意每个失败分支都将状态回退到 ENUM_STATE_RESET,而不是直接回到 ENUM_STATE_CONNECT。这是因为复位操作会重新初始化设备,而连接检测阶段已经完成。这种设计减少了不必要的 VBUS 电源循环。

4.7 常见枚举故障与状态机关联

将实际故障映射到状态机阶段,可以快速定位问题根源:

故障现象 卡住的状态 可能原因
设备不识别,无任何响应 连接检测 上拉电阻缺失、VBUS 未供电、D+/D- 短路
设备反复复位(听到 USB 重连音) 总线复位 → 默认地址 复位信号不足 10ms、设备内部电源不稳
设备描述符读取失败 获取设备描述符 描述符长度错误、端点 0 未初始化、STALL
地址分配后设备消失 分配地址 设备未在状态阶段后切换地址、地址冲突
配置描述符不完整 获取配置描述符 描述符总长度字段错误、缺少接口/端点描述符
配置后设备无数据通信 配置设备 端点未使能、配置值不匹配、驱动未加载

通过将故障现象与状态机阶段对应,工程师可以精确选择需要抓取的 USB 信号段,避免盲目排查。