3. UHCI 主机控制器架构:UHCI 与 OHCI 的区别、寄存器模型、帧列表与 TD 结构
3.1 UHCI 与 OHCI 架构对比
USB 1.x 时代存在两种主流主机控制器接口标准:UHCI(Universal Host Controller Interface,Intel 主导)与 OHCI(Open Host Controller Interface,Microsoft/Compaq 主导)。两者在功能上等价,但设计哲学与硬件/软件分工截然不同。理解这些差异是调试 USB 1.1 低速事务的基础。
| 对比维度 | UHCI(Intel) | OHCI(Microsoft/Compaq) |
|---|---|---|
| 设计哲学 | 软件主导(Software-Centric),硬件尽量简单 | 硬件主导(Hardware-Centric),硬件承担更多调度 |
| 帧列表管理 | 软件维护帧列表(Frame List),硬件逐帧遍历 | 硬件内部维护帧周期,软件只需提交端点描述符 |
| 传输描述符 | TD(Transfer Descriptor)由软件在内存中构建 | ED(Endpoint Descriptor)+ TD 链表,硬件自动遍历 |
| 中断延迟 | 较高,依赖软件及时更新帧列表 | 较低,硬件可主动处理中断事务 |
| 低速事务支持 | 通过 TD 中的低速标志位 + 分频机制实现 | 通过 ED 中的低速标志位 + 硬件自动插入 preamble |
| 调试复杂度 | 调试端口可直接观察帧列表与 TD 队列,适合协议分析 | 硬件内部状态机复杂,调试端口暴露信息较少 |
调试视角: UHCI 的软件主导架构使得调试端口能够直接捕获内存中的帧列表与 TD 链,非常适合低速事务的逐帧协议分析。OHCI 的硬件调度虽然性能更优,但调试时难以观察内部调度状态。
3.2 UHCI 寄存器模型
UHCI 主机控制器通过一组 I/O 映射的寄存器与软件交互。调试端口通过读取这些寄存器获取控制器状态、帧计数器、中断状态等信息。核心寄存器如下:
| 偏移地址 | 寄存器名称 | 位宽 | 功能描述 |
|---|---|---|---|
| 0x00 | USBCMD | 16-bit | 主机控制器命令寄存器(运行/停止、全局复位、帧列表基址更新等) |
| 0x02 | USBSTS | 16-bit | 主机控制器状态寄存器(HCHalted、HCPE、Fatal Error 等) |
| 0x04 | USBINTR | 16-bit | 中断使能寄存器(控制哪些状态位能触发中断) |
| 0x06 | FRNUM | 16-bit | 帧编号寄存器(当前帧号,每 1ms 递增) |
| 0x08 | FLBASEADD | 32-bit | 帧列表基址寄存器(指向 4KB 对齐的帧列表物理地址) |
| 0x0C | SOFMOD | 8-bit | SOF 帧间隔调整寄存器(用于微调帧周期) |
| 0x10 | PORTSC1 | 16-bit | 端口状态/控制寄存器(连接检测、使能、复位、低速/全速指示) |
| 0x12 | PORTSC2 | 16-bit | 端口 2 状态/控制寄存器(UHCI 通常支持 2 个端口) |
调试技巧: 在低速事务捕获前,应首先读取 PORTSCx 寄存器的位 8(低速设备标志位),确认设备被正确识别为低速模式。同时读取 FRNUM 确认帧计数器是否正常递增(每 1ms +1)。
3.3 帧列表(Frame List)结构
帧列表是 UHCI 调度体系的核心。它是一个由 1024 个 32-bit 指针组成的数组,每个指针指向一个传输描述符(TD)链表或队列头(QH)。帧列表基址由 FLBASEADD 寄存器指定,必须 4KB 对齐。
// 帧列表条目格式(32-bit)
// 位 [31:4] : TD 或 QH 的物理地址(16 字节对齐)
// 位 [3:2] : 保留
// 位 [1] : QH 标志(0 = TD 指针,1 = QH 指针)
// 位 [0] : 终止位(1 = 链表结束,无后续节点)
typedef struct {
uint32_t pointer : 28; // 物理地址高 28 位
uint32_t reserved : 2;
uint32_t qh_flag : 1; // 0: TD, 1: QH
uint32_t terminate : 1; // 0: 有效指针, 1: 链表结束
} FrameListEntry;
帧列表的遍历流程:
- 软件在内存中构建 1024 个帧列表条目(每个条目 4 字节,共 4KB)。
- 将帧列表基址写入 FLBASEADD 寄存器。
- 硬件每 1ms 递增 FRNUM,以 FRNUM 的低 10 位作为索引访问帧列表。
- 从帧列表条目中取出指针,开始遍历 TD 或 QH 链表,执行 USB 事务。
- 遍历完成后,硬件等待下一个 SOF 帧,重复步骤 3。
调试陷阱: 帧列表条目中的 终止位 必须正确设置。如果软件未正确初始化帧列表(例如所有条目 terminate=1),硬件将不会执行任何 USB 事务,导致总线静默。调试时应首先检查帧列表条目的有效性。
3.4 传输描述符(TD)结构
传输描述符是 UHCI 中最基本的事务单元。每个 TD 描述一个完整的 USB 事务(Token + Data + Handshake)。TD 由软件在内存中构建,硬件通过帧列表遍历 TD 链表并执行事务。
// UHCI 传输描述符结构(32 字节)
typedef struct {
// 字 0: 链接指针
uint32_t link_pointer : 28; // 下一个 TD 或 QH 的物理地址
uint32_t reserved0 : 2;
uint32_t qh_flag : 1; // 0: TD, 1: QH
uint32_t terminate : 1; // 0: 有效, 1: 链表结束
// 字 1: 控制状态
uint32_t reserved1 : 1;
uint32_t error_count : 2; // 错误重试计数
uint32_t low_speed : 1; // 1: 低速设备
uint32_t isochronous : 1; // 1: 等时传输
uint32_t ioc : 1; // 完成中断使能
uint32_t active : 1; // 1: TD 激活
uint32_t stalled : 1; // 1: 设备返回 STALL
uint32_t data_buffer_error : 1;
uint32_t babble : 1; // 1: 设备发送超长数据
uint32_t nak : 1; // 1: 设备返回 NAK
uint32_t crc_timeout : 1; // 1: CRC 超时或位填充错误
uint32_t status : 8; // 保留状态位
uint32_t reserved2 : 12;
// 字 2: 令牌信息
uint32_t pid : 8; // 令牌 PID(OUT=0xE1, IN=0x69, SETUP=0x2D)
uint32_t device_address : 7; // 设备地址
uint32_t endpoint : 4; // 端点号
uint32_t data_toggle : 1; // 数据切换位
uint32_t reserved3 : 12;
// 字 3: 缓冲区指针
uint32_t buffer_pointer; // 数据缓冲区物理地址(4 字节对齐)
// 字 4-7: 保留(扩展用途)
uint32_t reserved4[4];
} UHCI_TD;
低速事务关键字段: TD 控制状态字中的 low_speed 位(位 2)必须置 1,硬件才会在事务前自动插入 preamble(SYNC + PID)并以 1.5 Mbps 速率驱动总线。如果该位为 0,硬件将以全速 12 Mbps 发送,导致低速设备无法响应。
3.5 TD 链表与事务执行流程
UHCI 硬件逐帧遍历帧列表,对每个帧列表条目指向的 TD 链表执行以下步骤:
- 读取 TD: 从帧列表条目或前一个 TD 的链接指针获取当前 TD 物理地址。
- 检查 Active 位: 如果 Active=0,跳过该 TD,继续遍历下一个。
- 检查低速标志: 如果 low_speed=1,硬件在发送 Token 前先发送 preamble 包。
- 发送 Token 包: 根据 PID 字段发送 OUT/IN/SETUP 令牌。
- 数据阶段: 对于 OUT/SETUP,从缓冲区读取数据发送;对于 IN,接收数据写入缓冲区。
- 握手阶段: 等待设备返回 ACK/NAK/STALL,更新 TD 状态位。
- 更新 TD: 硬件写回 TD 的状态位(Active 清 0、错误计数、NAK 等)。
- 遍历下一个: 读取链接指针,如果 terminate=0,继续步骤 1。
3.6 调试端口视角:帧列表与 TD 的捕获方法
UHCI 调试端口通过 PCI 配置空间暴露主机控制器的 MMIO 基址。调试软件通过以下步骤捕获帧列表与 TD:
- 获取基址: 读取 PCI 配置空间中的 BAR0(Base Address Register 0),得到 UHCI 寄存器 I/O 基址。
- 读取 FLBASEADD: 从 I/O 基址 +0x08 读取 32 位帧列表基址。
- 读取 FRNUM: 从 I/O 基址 +0x06 读取当前帧号,确定当前正在处理的帧列表条目。
- 转储帧列表: 从 FLBASEADD 指向的物理地址读取 1024 个帧列表条目(4KB)。
- 遍历 TD 链: 对每个有效的帧列表条目,跟随链接指针遍历 TD 链表,读取每个 TD 的 32 字节结构。
- 解析 TD 字段: 提取低速标志、PID、设备地址、端点号、数据切换位、缓冲区指针等关键信息。
- 捕获数据: 根据缓冲区指针读取实际 USB 数据包内容。
实战建议: 在调试低速 HID 设备(如键盘、鼠标)时,重点关注 TD 中的 low_speed 位和 device_address 字段。如果发现低速设备的事务 TD 中 low_speed=0,说明软件配置错误,硬件将以全速速率发送,导致设备无响应。
3.7 常见调试场景与问题定位
| 调试现象 | 可能原因 | 检查方法 |
|---|---|---|
| 总线无任何活动 | 帧列表所有条目 terminate=1 | 检查 FLBASEADD 指向的内存区域,确认帧列表条目有效 |
| 低速设备无响应 | TD 中 low_speed 位未置 1 | 遍历 TD 链表,检查控制状态字的位 2 |
| 设备返回 STALL | 端点不支持请求的 PID 或数据阶段错误 | 检查 TD 中的 PID 和 endpoint 字段是否正确 |
| 数据缓冲区溢出 | TD 缓冲区指针未 4 字节对齐 | 检查 buffer_pointer 的低 2 位是否为 0 |
| 帧编号不递增 | USBCMD 中的 Run/Stop 位为 0 | 读取 USBCMD 寄存器,确认位 0 为 1 |
关键调试约束: UHCI 调试端口在捕获 TD 时,必须注意 内存一致性 问题。硬件在更新 TD 状态位时可能使用写组合(Write-Combining)或缓存,调试软件在读取 TD 前应执行内存屏障指令(如
mfence 或 clflush),确保读取到的是硬件写回的最新值。