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;

帧列表的遍历流程:

  1. 软件在内存中构建 1024 个帧列表条目(每个条目 4 字节,共 4KB)。
  2. 将帧列表基址写入 FLBASEADD 寄存器。
  3. 硬件每 1ms 递增 FRNUM,以 FRNUM 的低 10 位作为索引访问帧列表。
  4. 从帧列表条目中取出指针,开始遍历 TD 或 QH 链表,执行 USB 事务。
  5. 遍历完成后,硬件等待下一个 SOF 帧,重复步骤 3。
调试陷阱: 帧列表条目中的 终止位 必须正确设置。如果软件未正确初始化帧列表(例如所有条目 terminate=1),硬件将不会执行任何 USB 事务,导致总线静默。调试时应首先检查帧列表条目的有效性。
UHCI 帧列表与 TD 链表遍历架构 帧列表 (1024 条目) Frame 0 Frame 1 Frame 2 ... Frame 1023 指针 TD 1 低速 OUT 事务 链接 TD 2 低速 IN 事务 链接 TD 3 低速 SETUP 终止位 = 1 (结束) FRNUM 寄存器 → 索引帧列表

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 链表执行以下步骤:

  1. 读取 TD: 从帧列表条目或前一个 TD 的链接指针获取当前 TD 物理地址。
  2. 检查 Active 位: 如果 Active=0,跳过该 TD,继续遍历下一个。
  3. 检查低速标志: 如果 low_speed=1,硬件在发送 Token 前先发送 preamble 包。
  4. 发送 Token 包: 根据 PID 字段发送 OUT/IN/SETUP 令牌。
  5. 数据阶段: 对于 OUT/SETUP,从缓冲区读取数据发送;对于 IN,接收数据写入缓冲区。
  6. 握手阶段: 等待设备返回 ACK/NAK/STALL,更新 TD 状态位。
  7. 更新 TD: 硬件写回 TD 的状态位(Active 清 0、错误计数、NAK 等)。
  8. 遍历下一个: 读取链接指针,如果 terminate=0,继续步骤 1。
UHCI TD 链表遍历与低速事务执行流程 帧开始 (SOF) 读取帧列表条目 检查 TD Active 位 低速标志判断 低速=1 发送 Preamble 包 低速=0 (全速) 执行 USB 事务

3.6 调试端口视角:帧列表与 TD 的捕获方法

UHCI 调试端口通过 PCI 配置空间暴露主机控制器的 MMIO 基址。调试软件通过以下步骤捕获帧列表与 TD:

  1. 获取基址: 读取 PCI 配置空间中的 BAR0(Base Address Register 0),得到 UHCI 寄存器 I/O 基址。
  2. 读取 FLBASEADD: 从 I/O 基址 +0x08 读取 32 位帧列表基址。
  3. 读取 FRNUM: 从 I/O 基址 +0x06 读取当前帧号,确定当前正在处理的帧列表条目。
  4. 转储帧列表: 从 FLBASEADD 指向的物理地址读取 1024 个帧列表条目(4KB)。
  5. 遍历 TD 链: 对每个有效的帧列表条目,跟随链接指针遍历 TD 链表,读取每个 TD 的 32 字节结构。
  6. 解析 TD 字段: 提取低速标志、PID、设备地址、端点号、数据切换位、缓冲区指针等关键信息。
  7. 捕获数据: 根据缓冲区指针读取实际 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),确保读取到的是硬件写回的最新值。