4. USB 枚举过程:设备连接、复位、地址分配、获取描述符、配置选择

USB 枚举是主机(Host)识别并配置一个新接入设备的过程。对于嵌入式 EHCI 控制器驱动而言,枚举过程是软件栈与硬件寄存器交互最密集的阶段。本章将详细拆解枚举的五个核心步骤,并给出对应的驱动实现要点。

4.1 枚举全流程概览

当设备插入 USB 端口后,主机控制器(EHCI)与设备控制器之间通过一系列标准请求完成身份识别与资源分配。下表总结了枚举各阶段的主要动作与数据流向:

步骤 触发条件 主机动作 设备响应 关键数据结构
1. 设备连接 端口状态变化(Port Change) 读取端口状态寄存器,检测连接 上拉电阻使 D+/D- 电平变化 PORTSC(Port Status and Control)
2. 复位 检测到连接 设置端口复位位,等待复位完成 进入默认状态(Default State) PORTSC 复位位
3. 地址分配 复位完成 发送 SET_ADDRESS 请求 分配唯一地址(1~127) 设备请求(Setup Packet)
4. 获取描述符 地址已分配 发送 GET_DESCRIPTOR 请求 返回设备/配置/接口/端点描述符 描述符结构体
5. 配置选择 描述符解析完成 发送 SET_CONFIGURATION 请求 进入配置状态(Configured State) 配置描述符中的 bConfigurationValue
驱动提示: 在嵌入式系统中,枚举过程通常由 USB 核心层(USB Core)驱动框架完成,EHCI 控制器驱动主要负责提供底层硬件访问接口(如端口操作、中断处理、DMA 传输)。理解枚举流程有助于定位硬件初始化失败或设备无法识别的问题。

4.2 设备连接检测

EHCI 控制器通过 PORTSC(Port Status and Control) 寄存器监控每个端口的状态。当设备插入时,D+/D- 信号线上的电平变化会被硬件捕获,并置位 PORTSC 中的 Connect Status Change(CSC) 位。

驱动在中断处理函数中轮询或响应端口状态变化:

/* 伪代码:检测端口连接 */
void ehci_port_connect_handler(struct ehci_hcd *ehci, int port) {
    uint32_t portsc = ehci_readl(&ehci->regs->portsc[port]);

    if (portsc & PORTSC_CSC) {
        /* 清除连接状态变化标志 */
        ehci_writel(&ehci->regs->portsc[port], portsc | PORTSC_CSC);

        if (portsc & PORTSC_CCS) {
            /* 设备已连接 */
            dev_info(ehci->dev, "Port %d: device connected\n", port);
            /* 触发后续复位流程 */
            ehci_port_reset(ehci, port);
        } else {
            /* 设备断开 */
            dev_info(ehci->dev, "Port %d: device disconnected\n", port);
            /* 清理设备资源 */
        }
    }
}
硬件注意: 对于高速(High-Speed)设备,EHCI 控制器直接处理;对于全速(Full-Speed)或低速(Low-Speed)设备,EHCI 会将端口所有权交给配套的 OHCI/UHCI 控制器(companion controller)。驱动需要检查 PORTSC 中的 Port Owner(PO) 位。

4.3 设备复位

复位是枚举中最关键的硬件操作之一。主机通过设置 PORTSC 寄存器的 Port Reset(PR) 位,向设备发送至少 10ms 的复位信号。复位完成后,设备进入默认状态(地址为 0),并准备好接收控制传输。

复位时序必须严格遵循 USB 2.0 规范:

/* 伪代码:端口复位 */
void ehci_port_reset(struct ehci_hcd *ehci, int port) {
    uint32_t portsc;

    /* 置位复位位 */
    portsc = ehci_readl(&ehci->regs->portsc[port]);
    ehci_writel(&ehci->regs->portsc[port], portsc | PORTSC_PR);

    /* 等待至少 10ms(规范要求 10ms ~ 20ms) */
    mdelay(10);

    /* 清除复位位 */
    portsc = ehci_readl(&ehci->regs->portsc[port]);
    ehci_writel(&ehci->regs->portsc[port], portsc & ~PORTSC_PR);

    /* 等待复位完成(硬件自动清除 PR 位) */
    while (ehci_readl(&ehci->regs->portsc[port]) & PORTSC_PR) {
        udelay(1);
    }

    /* 复位完成后,设备速度已确定,读取速度指示位 */
    portsc = ehci_readl(&ehci->regs->portsc[port]);
    if (portsc & PORTSC_HSP) {
        dev_info(ehci->dev, "Port %d: High-Speed device\n", port);
    } else {
        dev_info(ehci->dev, "Port %d: Full-Speed device\n", port);
    }
}
关键寄存器位:
  • PR(Port Reset):写 1 启动复位,硬件复位完成后自动清零。
  • HSP(High-Speed Port):复位完成后指示设备当前速度模式。
  • PE(Port Enable):复位成功后硬件自动置位,表示端口已启用。

4.4 地址分配

复位完成后,设备处于默认地址(0)。主机通过控制传输(端点 0)发送 SET_ADDRESS 标准请求,为设备分配一个唯一的地址(1~127)。

SET_ADDRESS 请求的 Setup 包结构如下:

字段 值 说明
bmRequestType 0x00 主机到设备,标准请求,接收端为设备
bRequest 0x05 SET_ADDRESS
wValue 地址值(1~127) 要分配的设备地址
wIndex 0x0000 未使用
wLength 0x0000 无数据阶段
/* 伪代码:发送 SET_ADDRESS */
int usb_set_address(struct usb_device *dev, int address) {
    struct usb_ctrlrequest req;

    req.bRequestType = 0x00;
    req.bRequest = USB_REQ_SET_ADDRESS;
    req.wValue = cpu_to_le16(address);
    req.wIndex = 0;
    req.wLength = 0;

    /* 通过 EHCI 的控制传输函数发送 */
    return ehci_control_transfer(dev, &req, NULL, 0);
}
驱动注意: SET_ADDRESS 请求必须在设备地址为 0 时发送。发送成功后,驱动需要等待至少 2ms(USB 规范要求),然后才能使用新地址进行后续通信。在 Linux 内核中,usb_set_device_state() 会同步更新设备状态。

4.5 获取描述符

地址分配完成后,主机通过 GET_DESCRIPTOR 请求获取设备的描述符信息。描述符是 USB 设备的“身份证”,包含设备类型、端点数量、供电方式等关键信息。

获取描述符的典型顺序:

  1. 设备描述符(Device Descriptor):先读取前 8 字节,获取最大包长度(bMaxPacketSize0)。
  2. 配置描述符(Configuration Descriptor):读取完整配置描述符,包括接口和端点描述符。
  3. 字符串描述符(String Descriptor):可选,用于获取厂商/产品字符串。

设备描述符结构(USB 2.0 规范):

struct usb_device_descriptor {
    uint8_t  bLength;            /* 描述符长度(18 字节) */
    uint8_t  bDescriptorType;    /* 描述符类型(0x01) */
    uint16_t bcdUSB;             /* USB 规范版本号(BCD 码) */
    uint8_t  bDeviceClass;       /* 设备类代码 */
    uint8_t  bDeviceSubClass;    /* 设备子类代码 */
    uint8_t  bDeviceProtocol;    /* 设备协议代码 */
    uint8_t  bMaxPacketSize0;    /* 端点 0 最大包长度 */
    uint16_t idVendor;           /* 厂商 ID */
    uint16_t idProduct;          /* 产品 ID */
    uint16_t bcdDevice;          /* 设备版本号 */
    uint8_t  iManufacturer;      /* 厂商字符串描述符索引 */
    uint8_t  iProduct;           /* 产品字符串描述符索引 */
    uint8_t  iSerialNumber;      /* 序列号字符串描述符索引 */
    uint8_t  bNumConfigurations; /* 配置描述符数量 */
} __attribute__((packed));
关键字段解析:
  • bMaxPacketSize0:决定控制传输的最大数据负载,常见值为 8、16、32、64。
  • idVendor / idProduct:驱动匹配设备的关键标识。
  • bNumConfigurations:通常为 1,表示设备只有一个配置。

4.6 配置选择

获取并解析配置描述符后,主机选择最合适的配置(通常为第一个配置)。通过发送 SET_CONFIGURATION 请求,设备进入配置状态(Configured State),此时所有端点(Endpoint)才真正可用。

SET_CONFIGURATION 请求的 Setup 包:

字段 值 说明
bmRequestType 0x00 主机到设备,标准请求
bRequest 0x09 SET_CONFIGURATION
wValue bConfigurationValue 配置描述符中的配置值
wIndex 0x0000 未使用
wLength 0x0000 无数据阶段
/* 伪代码:选择配置 */
int usb_set_configuration(struct usb_device *dev, int config_value) {
    struct usb_ctrlrequest req;

    req.bRequestType = 0x00;
    req.bRequest = USB_REQ_SET_CONFIGURATION;
    req.wValue = cpu_to_le16(config_value);
    req.wIndex = 0;
    req.wLength = 0;

    return ehci_control_transfer(dev, &req, NULL, 0);
}
常见问题: 如果 SET_CONFIGURATION 失败,设备将停留在地址状态(Addressed State),无法进行数据传输。常见原因包括:供电不足、描述符解析错误、端点配置超出主机控制器能力(如缓冲区大小不足)。

4.7 枚举状态机与 EHCI 交互

整个枚举过程可以抽象为一个有限状态机(FSM)。下面的 SVG 图展示了从设备插入到配置完成的完整状态转换:

USB 枚举状态机(EHCI 视角) 连接检测 端口复位 地址分配 获取描述符 配置选择 配置完成 CSC 置位 复位完成 SET_ADDRESS 成功 描述符解析完成 SET_CONFIG 成功 复位失败 地址分配失败 状态说明 • 连接检测:EHCI 检测到端口电平变化,置位 CSC 位。 • 端口复位:驱动设置 PR 位,硬件产生复位信号,设备进入默认状态。 • 地址分配:主机发送 SET_ADDRESS,设备获得唯一地址。 • 获取描述符:主机读取设备/配置/接口/端点描述符。 • 配置选择:主机发送 SET_CONFIGURATION,设备进入配置状态。 • 虚线路径表示失败回退,驱动需重新复位或报告错误。

4.8 枚举过程中的常见问题与调试

在嵌入式系统中,枚举失败是 USB 驱动调试中最常见的问题。下表总结了典型故障现象及其根因:

故障现象 可能原因 排查方法
设备连接后无中断 EHCI 中断未使能;PORTSC 中断屏蔽 检查 USBINTR 寄存器;确认端口中断使能位
复位超时 PR 位未正确清除;硬件复位信号异常 用示波器测量 D+/D- 复位波形;检查 PR 位轮询逻辑
SET_ADDRESS 无响应 控制传输 TD 链表错误;设备地址 0 通信失败 检查 QH 和 qTD 描述符;确认端点 0 最大包长度
描述符读取不全 bMaxPacketSize0 与实际不符;DMA 缓冲区对齐问题 先读 8 字节获取正确包长度;确保缓冲区 32 字节对齐
SET_CONFIGURATION 失败 供电不足;端点配置超出主机能力 检查 bMaxPower 值;确认端点类型和方向支持
调试利器: 在 Linux 内核中,可以通过 echo 'file drivers/usb/core/hub.c +p' > /sys/kernel/debug/dynamic_debug/control 打开 USB 核心层的枚举调试日志,实时观察枚举过程中的状态变化和错误码。

4.9 本章小结

USB 枚举是主机与设备建立通信的基础,涉及硬件状态检测、控制传输、描述符解析等多个环节。对于 EHCI 控制器驱动开发者而言,需要重点关注:

  • 端口操作:正确读写 PORTSC 寄存器,处理连接/断开/复位/速度检测。
  • 控制传输:构造 Setup 包,通过 EHCI 的异步列表(Async List)调度传输。
  • 描述符解析:严格按照 USB 规范解析二进制描述符数据,处理字节序对齐。
  • 状态管理:维护设备状态机,处理枚举失败时的回退与重试逻辑。

掌握枚举流程后,下一阶段将深入 EHCI 控制器的传输调度机制,包括周期性列表(Periodic List)与异步列表(Async List)的管理。