4. UHCI 队列头(QH)与 TD 链表

UHCI 主机控制器通过内存中的链表结构来管理 USB 总线上所有的传输任务。这套链表体系由队列头(QH, Queue Head)和传输描述符(TD, Transfer Descriptor)两种核心数据结构构成。QH 作为调度入口,TD 承载具体的传输参数与数据缓冲区指针。理解 QH 与 TD 的链接方式、水平链表与垂直链表的组织逻辑,是掌握 UHCI 帧调度与 DMA 传输机制的基础。

4.1 QH(Queue Head)结构

QH 是 UHCI 调度链表中的基本调度单元,每个 QH 对应一个 USB 端点(Endpoint)或一个控制传输阶段。QH 在内存中固定为 16 字节对齐,其结构定义如下:

偏移(字节) 位域 描述
0-3 Horizontal Link Pointer (HLP) 水平链接指针,指向下一个 QH 或 TD,最低位(bit 0)为 QH/TD 选择位(0=TD, 1=QH)
4-7 Endpoint Number / Device Address / Max Packet Size bit 0-6: Device Address;bit 7-10: Endpoint Number;bit 11-14: Max Packet Size(以字节为单位,实际值为 (N+1)*8);bit 15: 低速设备标志(Low Speed Device);bit 16-31: 保留
8-11 TD Queue Head Pointer (TQHP) 垂直链接指针,指向该 QH 挂载的第一个 TD(TD 链表的头节点)
12-15 TD Queue Tail Pointer (TQTP) 垂直链接尾指针,指向该 QH 挂载的最后一个 TD 的下一个位置(用于硬件回写,软件初始化时指向链表末尾的保留 TD)
关键对齐规则:QH 必须存储在 16 字节对齐的内存地址上(即地址低 4 位为 0x0)。TD 必须存储在 16 字节对齐的地址上(地址低 4 位为 0x0)。水平链接指针(HLP)的最低 4 位用于控制位,实际地址为 HLP & 0xFFFFFFF0。

4.2 QH 与 TD 的链接方式

QH 与 TD 通过水平链接和垂直链接两种方式构成二维链表结构。水平链接用于遍历调度队列,垂直链接用于挂载具体传输任务。

  • 水平链接(Horizontal Link):QH 的 Horizontal Link Pointer(HLP)指向下一个调度节点(可以是另一个 QH 或一个 TD)。所有 QH 通过 HLP 串联成一个水平链表,UHCI 主机控制器按帧遍历该链表。
  • 垂直链接(Vertical Link):QH 的 TD Queue Head Pointer(TQHP)指向该 QH 挂载的第一个 TD。TD 之间通过 Next Link Pointer(每个 TD 的 0-3 字节)串联成垂直链表。QH 的 TD Queue Tail Pointer(TQTP)用于硬件回写,标记链表末尾。
链接方式对比:
  • 水平链表:QH → QH → QH(或 TD),用于调度顺序遍历。
  • 垂直链表:QH → TD → TD → TD,用于挂载同一端点的多个传输描述符。
QH与TD链接方式示意图 QH 与 TD 水平/垂直链接结构 QH 1 Device=1, EP=0 HLP → QH2 QH 2 Device=2, EP=0 HLP → QH3 QH 3 Device=3, EP=0 HLP → NULL TD 1.1 Next → TD 1.2 TQHP TD 1.2 Next → TD 1.3 TD 1.3 Next → NULL TD 2.1 Next → NULL TQHP TD 3.1 Next → TD 3.2 TQHP TD 3.2 Next → NULL 水平链表:QH1 → QH2 → QH3(调度顺序) | 垂直链表:每个 QH 挂载自己的 TD 链(传输任务)

4.3 水平链表与垂直链表

UHCI 的链表体系通过水平链表和垂直链表的二维结构,实现了调度与传输的分离管理。

4.3.1 水平链表(Horizontal List)

水平链表由 QH 通过 HLP 指针串联而成,UHCI 主机控制器在每个帧(Frame)开始时,从帧列表(Frame List)中取出当前帧的指针,然后遍历水平链表。水平链表上的每个 QH 代表一个需要被服务的 USB 端点。水平链表的遍历顺序决定了端点的服务优先级。

  • 遍历规则:UHCI 从帧列表指针开始,依次访问每个 QH。如果 QH 的垂直链表非空(即挂载了 TD),则处理该 QH 的第一个 TD;如果垂直链表为空,则跳过该 QH。
  • 链表终止:水平链表以 HLP 为 NULL(0x00000001,最低位为 1 表示 QH 类型,但地址为 0)结束。
  • 动态插入:软件可以在运行时动态插入或移除 QH,只需修改前一个 QH 的 HLP 指针即可。

4.3.2 垂直链表(Vertical List)

垂直链表由 TD 通过 Next Link Pointer 串联而成,挂载在 QH 的 TQHP 指针下。每个 QH 管理一个垂直链表,链表中包含该端点待处理的所有传输描述符。

  • 链表结构:QH 的 TQHP 指向第一个 TD,每个 TD 的 Next Link Pointer 指向下一个 TD,最后一个 TD 的 Next Link Pointer 为 NULL(0x00000001,最低位为 0 表示 TD 类型,但地址为 0)。
  • 硬件回写:UHCI 硬件在处理完一个 TD 后,会更新 QH 的 TQTP 指针,用于软件判断链表是否处理完毕。
  • 深度优先:UHCI 在遍历水平链表时,遇到挂载了 TD 的 QH,会先处理该 QH 垂直链表上的所有 TD(深度优先),然后再继续遍历下一个 QH。
常见陷阱:垂直链表上的 TD 必须按顺序完成,UHCI 硬件不会乱序执行同一 QH 下的 TD。如果某个 TD 执行失败(如 STALL 或数据溢出),硬件会停止该 QH 的垂直链表处理,并设置 TD 的状态位。软件需要检查 TD 状态并重新初始化链表。

4.4 调度策略

UHCI 的调度策略基于帧(Frame)和链表遍历机制,确保 USB 总线的带宽分配满足等时(Isochronous)、中断(Interrupt)、控制(Control)和批量(Bulk)传输的不同需求。

传输类型 调度优先级 帧列表位置 QH 挂载方式
等时传输 最高 帧列表直接指向 TD(不经过 QH) 无 QH,直接挂载 TD 到帧列表
中断传输 高 帧列表指向中断 QH 链表 每个中断端点对应一个 QH,挂载 TD 链
控制传输 中 帧列表指向控制 QH 链表 每个控制端点对应一个 QH,挂载 TD 链
批量传输 低 帧列表指向批量 QH 链表 每个批量端点对应一个 QH,挂载 TD 链

UHCI 主机控制器在每个帧开始时,按以下顺序遍历链表:

  1. 等时传输:直接从帧列表指针读取等时 TD,立即处理。
  2. 中断传输:遍历中断 QH 链表,处理所有挂载的 TD。
  3. 控制传输:遍历控制 QH 链表,处理所有挂载的 TD。
  4. 批量传输:遍历批量 QH 链表,处理所有挂载的 TD。
调度策略要点:
  • 等时传输具有最高优先级,保证带宽和延迟。
  • 中断传输在等时之后处理,保证周期性数据的及时性。
  • 控制传输在中断之后处理,用于设备枚举和配置。
  • 批量传输优先级最低,仅在总线空闲时处理。
  • 每个帧的时间为 1ms(全速)或 125µs(高速,但 UHCI 仅支持全速/低速)。
UHCI 调度策略流程图 UHCI 帧调度策略流程图 帧开始(SOF) 1. 等时传输(帧列表 → TD) 2. 中断传输(中断 QH 链表) 3. 控制传输(控制 QH 链表) 4. 批量传输(批量 QH 链表)

4.5 链表初始化与维护

在嵌入式平台上初始化 UHCI 链表时,需要遵循以下步骤:

  1. 分配内存:为帧列表(Frame List)分配 1024 个 32 位指针(4KB),必须 4KB 对齐。为每个 QH 和 TD 分配 16 字节对齐的内存。
  2. 初始化帧列表:将所有帧列表指针初始化为 NULL(0x00000001,表示 QH 类型但地址为 0),或指向默认的空闲 QH。
  3. 创建 QH:为每个需要调度的端点创建一个 QH,设置 Device Address、Endpoint Number、Max Packet Size 等字段。将 TQHP 和 TQTP 初始化为指向一个保留的“空 TD”(Dummy TD)。
  4. 链接 QH:将 QH 按优先级顺序通过 HLP 串联成水平链表,并将第一个 QH 的地址写入帧列表的对应位置。
  5. 挂载 TD:当有传输请求时,创建 TD 并挂载到对应 QH 的垂直链表上。更新 TQHP 指向第一个 TD,TQTP 指向最后一个 TD 的下一个位置。
  6. 启动主机控制器:设置 UHCI 基地址寄存器(BASEADDR)指向帧列表,设置控制寄存器(USBCMD)中的 Run/Stop 位为 1,启动调度。
嵌入式平台优化建议:在 DMA 传输场景下,QH 和 TD 应放置在非缓存(Non-cacheable)内存区域,或使用 DMA 一致性内存分配(如 Linux 的 dma_alloc_coherent),避免 CPU 缓存与 DMA 引擎之间的数据不一致问题。

4.6 链表遍历示例

以下是一个简化的链表遍历伪代码,展示了 UHCI 硬件在每个帧中的处理逻辑:

// UHCI 帧处理伪代码
void uhci_frame_process(uint32_t frame_list_ptr, int frame_index) {
    uint32_t current_ptr = frame_list_ptr[frame_index];
    
    while (current_ptr != NULL) {
        if (current_ptr & 0x1) {
            // 当前节点是 QH(最低位为 1)
            QH *qh = (QH *)(current_ptr & 0xFFFFFFF0);
            
            // 检查垂直链表是否非空
            if (qh->td_queue_head_ptr != NULL) {
                // 处理该 QH 下的所有 TD
                TD *td = qh->td_queue_head_ptr;
                while (td != NULL) {
                    uhci_process_td(td);
                    td = td->next_link_ptr;
                }
            }
            
            // 继续遍历水平链表
            current_ptr = qh->horizontal_link_ptr;
        } else {
            // 当前节点是 TD(最低位为 0)
            TD *td = (TD *)(current_ptr & 0xFFFFFFF0);
            uhci_process_td(td);
            current_ptr = td->next_link_ptr;
        }
    }
}
注意:上述伪代码仅为逻辑示意。实际 UHCI 硬件在处理 TD 时会更新 TD 的状态字段(如 Active 位、Error 位等),并在处理完一个 QH 的所有 TD 后,才会继续遍历下一个 QH。硬件不会在同一个帧内重新遍历已经处理过的 QH。

通过理解 QH 与 TD 的链表结构、水平与垂直链接方式以及调度策略,嵌入式开发者可以正确配置 UHCI 主机控制器,实现高效的控制传输与 DMA 传输。在实际测试验证中,建议使用逻辑分析仪或 USB 协议分析仪观察链表遍历顺序,确保调度行为符合预期。