3. UHCI 帧列表与传输描述符(TD)

UHCI 主机控制器通过帧列表(Frame List)和传输描述符(Transfer Descriptor, TD)两级数据结构来管理 USB 总线上的所有传输事务。帧列表是时间基准调度表,TD 则是描述单个 USB 事务的指令单元。驱动开发者必须精确理解这两者的内存布局、链接规则和状态流转,才能正确初始化控制器并调试批量/中断传输的异常行为。

3.1 帧列表(Frame List)结构

帧列表是一个物理地址连续的指针数组,每个元素(Frame List Pointer, FLP)指向一个等时传输描述符(iTD)或一个队列头(QH)的物理地址。UHCI 以 1 ms 为周期遍历该列表,驱动通过修改列表项来动态调整每帧的传输调度。

参数 值 说明
基地址寄存器 USB 基地址 + 08h FLBASEADD,存放帧列表的 32 位物理基址
列表长度 1024 项 固定为 1024,对应 1024 帧(约 1 秒)
每项大小 4 字节 32 位物理指针,低 4 位为控制位
对齐要求 4 KB 边界 帧列表整体必须 4 KB 对齐
控制位 [3:0] 0 = 指向 QH
1 = 指向 iTD
UHCI 仅使用位 0 区分类型
驱动调试要点:帧列表基址写入 FLBASEADD 后,必须等待 HCHalted 位清零(即控制器开始运行)才能保证列表被正确加载。若帧列表未 4 KB 对齐,控制器会抛出系统错误(HSE 位置 1)。
帧列表与队列头/传输描述符的链接关系 帧列表 [0] [1] [2] ... [1023] 每项 4 字节 4 KB 对齐 FLP[0] 队列头 1 批量端点 OUT 垂直链接 TD 1 Token=OUT 水平链接 TD 2 Token=IN FLP[1] 队列头 2 中断端点 IN TD 3 Token=IN 帧列表每项指向一个队列头(QH),QH 通过垂直链接指向第一个 TD,TD 之间通过水平链接形成单向链表

3.2 传输描述符(TD)格式

每个 TD 固定为 16 字节,由 4 个 32 位字段组成:Link Pointer、Control/Status、Token、Buffer Pointer。UHCI 控制器通过 DMA 直接读取 TD 并执行 USB 事务,完成后回写状态位。

偏移 字段 位宽 描述
+0x00 Link Pointer 32 指向下一个 TD 或 QH 的物理地址;低 4 位为链接类型控制位
+0x04 Control / Status 32 错误计数、完成状态、中断使能、SPD 等控制位
+0x08 Token 32 端点号、设备地址、PID(IN/OUT/SETUP)、数据切换位
+0x0C Buffer Pointer 32 数据缓冲区的物理地址(最大 4 KB 边界对齐)

3.3 Link Pointer 字段详解

Link Pointer 的低 4 位用于控制链表遍历行为,高 28 位为物理地址。驱动在构造链表时必须正确设置这些控制位,否则控制器可能错误地终止遍历或误判链接类型。

位 名称 值 含义
[0] Terminate (T) 0 = 有效链接
1 = 链表结束
若置 1,控制器停止遍历该队列
[1] Depth/Breadth (D) 0 = 深度优先
1 = 广度优先
仅 QH 使用;TD 中必须为 0
[2] Reserved 0 必须写 0
[3] Next Type 0 = 下一个是 QH
1 = 下一个是 TD
指示链接目标类型
调试技巧:当驱动发现批量传输完成但数据未更新时,优先检查 Link Pointer 的 T 位是否被意外置 1。很多调试场景下,驱动在初始化 TD 池时未清零 T 位,导致控制器提前终止链表遍历。

3.4 Control / Status 字段

该字段由驱动在初始化时写入控制位,控制器在事务完成后回写状态位。驱动必须通过轮询或中断读取该字段来判断传输结果。

位 名称 访问 描述
[23] Active R/W 1 = 待处理;0 = 已完成(控制器清零)
[22] Stalled R 1 = 设备返回 STALL
[21] Data Buffer Error R 1 = 缓冲区访问越界或对齐错误
[20] Babble R 1 = 设备发送了超过最大包长的数据
[19] NAK R 1 = 设备返回 NAK(仅中断/批量)
[18] CRC/TimeOut Error R 1 = CRC 校验失败或总线超时
[17:16] Error Counter R/W 初始值 3,每次错误递减,减到 0 时 Active 清零
[7] Interrupt on Complete (IOC) W 1 = 该 TD 完成后触发中断
[6] ISO W 1 = 等时传输(不重试)
[5] Low Speed (LS) W 1 = 低速设备
[4] Short Packet Detect (SPD) W 1 = 遇到短包时停止传输
[3:0] Reserved - 必须写 0
传输描述符(TD)16 字节内存布局 Link Pointer (32 bits) — 偏移 +0x00 Control / Status (32 bits) — 偏移 +0x04 Token (32 bits) — 偏移 +0x08 Buffer Pointer (32 bits) — 偏移 +0x0C 每个 TD 固定 16 字节,物理地址必须 16 字节对齐;控制器通过 DMA 读取后自动回写状态

3.5 Token 字段与 Buffer Pointer

Token 字段编码了 USB 事务的核心参数,Buffer Pointer 则指向数据缓冲区。驱动在构造批量或中断 TD 时,必须确保 Token 中的设备地址、端点号和 PID 与目标设备匹配。

位 名称 描述
[31:29] Reserved 必须写 0
[28:19] Device Address USB 设备地址(0~127)
[18:15] Endpoint Number 端点号(0~15)
[14:13] Data Toggle 00 = DATA0 / 01 = DATA1 / 10 = DATA2 / 11 = Reserved
[12:10] Reserved 必须写 0
[9:8] PID Code 00 = OUT / 01 = IN / 10 = SETUP / 11 = Reserved
[7:0] Maximum Packet Length 最大包长(字节),通常为 8/16/32/64
常见错误:Buffer Pointer 指向的缓冲区必须物理连续且对齐到 4 KB 边界。若缓冲区跨越 4 KB 边界,控制器会触发 Data Buffer Error。驱动应使用 __get_free_pages() 或 DMA 一致性分配函数确保对齐。

3.6 TD 链表构造示例

以下代码展示如何在驱动中初始化一个包含 3 个 TD 的批量传输链表。注意 Link Pointer 的 T 位和 Next Type 位的设置。

/* 定义 TD 结构体 */
struct uhci_td {
    uint32_t link;          /* +0x00 */
    uint32_t control;       /* +0x04 */
    uint32_t token;         /* +0x08 */
    uint32_t buffer;        /* +0x0C */
} __attribute__((packed, aligned(16)));

/* 构造 3 个 TD 的链表 */
struct uhci_td *td_pool = dma_alloc_coherent(dev, 3 * sizeof(*td), &dma_handle, GFP_KERNEL);

/* TD0: OUT 传输,链接到 TD1 */
td_pool[0].link    = (dma_handle + 1 * sizeof(*td)) | 0x08;  /* Next Type = TD */
td_pool[0].control = (3 << 16) | (1 << 7);                 /* IOC=1, ErrorCnt=3 */
td_pool[0].token   = (dev_addr << 19) | (ep_num << 15) | (0 << 13) | (0 << 8) | 64;
td_pool[0].buffer  = dma_map_single(dev, out_buf, 64, DMA_TO_DEVICE);

/* TD1: IN 传输,链接到 TD2 */
td_pool[1].link    = (dma_handle + 2 * sizeof(*td)) | 0x08;
td_pool[1].control = (3 << 16) | (1 << 7);
td_pool[1].token   = (dev_addr << 19) | (ep_num << 15) | (1 << 13) | (1 << 8) | 64;
td_pool[1].buffer  = dma_map_single(dev, in_buf, 64, DMA_FROM_DEVICE);

/* TD2: 最后一个 TD,T 位置 1 终止链表 */
td_pool[2].link    = 0x01;  /* T=1, 其余位忽略 */
td_pool[2].control = (3 << 16) | (1 << 7);
td_pool[2].token   = (dev_addr << 19) | (ep_num << 15) | (0 << 13) | (0 << 8) | 64;
td_pool[2].buffer  = dma_map_single(dev, out_buf2, 64, DMA_TO_DEVICE);
调试检查清单:
✅ 所有 TD 的 Active 位(bit 23)在初始化时置 1
✅ 最后一个 TD 的 Link Pointer T 位置 1
✅ Buffer Pointer 指向 4 KB 对齐的物理地址
✅ Token 中的 PID Code 与端点方向一致(OUT=00, IN=01)
✅ 帧列表项指向第一个 QH,QH 的垂直链接指向第一个 TD

3.7 帧列表与 TD 的协同工作流程

  1. 帧开始:UHCI 控制器读取当前帧号(Frame Number),以帧号为索引从帧列表中取出 FLP。
  2. 队列遍历:根据 FLP 找到 QH,QH 的垂直链接指向第一个 TD。
  3. TD 执行:控制器读取 TD 的 Token 和 Buffer Pointer,发起 USB 事务(IN/OUT/SETUP)。
  4. 状态回写:事务完成后,控制器更新 TD 的 Control/Status 字段(清零 Active,设置错误位)。
  5. 链表推进:通过 TD 的 Link Pointer 找到下一个 TD,重复步骤 3~4,直到遇到 T=1 或时间片耗尽。
  6. 帧结束:控制器递增帧号,开始下一帧的调度。
UHCI 帧列表与 TD 协同工作流程 1. 读取帧列表 Frame Number → FLP 2. 找到 QH 垂直链接 → 首个 TD 3. 执行 TD USB 事务 IN/OUT 4. 回写状态 Active=0, 错误位 5. 检查 T 位 T=0 → 下一个 TD 6. 帧结束 Frame Number++ 下一帧循环 驱动调试关键点 • 每帧最多执行 128 个 TD(时间片限制),超出的 TD 会延迟到下一帧 • 中断 TD 应放在帧列表的前部以保证低延迟 • 批量 TD 可跨帧执行,但必须保证链表完整性

3.8 批量传输与中断传输的 TD 差异

特性 批量传输 TD 中断传输 TD
调度优先级 低,帧列表靠后位置 高,帧列表靠前位置
重试策略 遇到 NAK 立即重试(Error Counter 递减) 遇到 NAK 停止当前帧,下一帧重试
最大包长 8/16/32/64 字节 1~64 字节(全速)
IOC 中断 通常仅在最后一个 TD 置 IOC 每个 TD 均可置 IOC
SPD 使用 常用,用于检测短包结束 不常用,中断传输通常固定长度
错误处理 Error Counter 减到 0 后停止,驱动需重置 Error Counter 减到 0 后停止,驱动需重置
性能优化建议:批量传输的 TD 链表可以构造为环形(最后一个 TD 的 Link Pointer 指向第一个 TD),配合 SPD 位实现连续数据流。但必须确保驱动在 Active 位清零后及时回收缓冲区,避免 DMA 覆盖未处理的数据。