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 |
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);
/* 清理设备资源 */
}
}
}
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);
}
usb_set_device_state() 会同步更新设备状态。
4.5 获取描述符
地址分配完成后,主机通过 GET_DESCRIPTOR 请求获取设备的描述符信息。描述符是 USB 设备的“身份证”,包含设备类型、端点数量、供电方式等关键信息。
获取描述符的典型顺序:
- 设备描述符(Device Descriptor):先读取前 8 字节,获取最大包长度(bMaxPacketSize0)。
- 配置描述符(Configuration Descriptor):读取完整配置描述符,包括接口和端点描述符。
- 字符串描述符(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);
}
4.7 枚举状态机与 EHCI 交互
整个枚举过程可以抽象为一个有限状态机(FSM)。下面的 SVG 图展示了从设备插入到配置完成的完整状态转换:
4.8 枚举过程中的常见问题与调试
在嵌入式系统中,枚举失败是 USB 驱动调试中最常见的问题。下表总结了典型故障现象及其根因:
| 故障现象 | 可能原因 | 排查方法 |
|---|---|---|
| 设备连接后无中断 | EHCI 中断未使能;PORTSC 中断屏蔽 | 检查 USBINTR 寄存器;确认端口中断使能位 |
| 复位超时 | PR 位未正确清除;硬件复位信号异常 | 用示波器测量 D+/D- 复位波形;检查 PR 位轮询逻辑 |
| SET_ADDRESS 无响应 | 控制传输 TD 链表错误;设备地址 0 通信失败 | 检查 QH 和 qTD 描述符;确认端点 0 最大包长度 |
| 描述符读取不全 | bMaxPacketSize0 与实际不符;DMA 缓冲区对齐问题 | 先读 8 字节获取正确包长度;确保缓冲区 32 字节对齐 |
| SET_CONFIGURATION 失败 | 供电不足;端点配置超出主机能力 | 检查 bMaxPower 值;确认端点类型和方向支持 |
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)的管理。