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)。
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 |
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
✅ 所有 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 的协同工作流程
- 帧开始:UHCI 控制器读取当前帧号(Frame Number),以帧号为索引从帧列表中取出 FLP。
- 队列遍历:根据 FLP 找到 QH,QH 的垂直链接指向第一个 TD。
- TD 执行:控制器读取 TD 的 Token 和 Buffer Pointer,发起 USB 事务(IN/OUT/SETUP)。
- 状态回写:事务完成后,控制器更新 TD 的 Control/Status 字段(清零 Active,设置错误位)。
- 链表推进:通过 TD 的 Link Pointer 找到下一个 TD,重复步骤 3~4,直到遇到 T=1 或时间片耗尽。
- 帧结束:控制器递增帧号,开始下一帧的调度。
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 覆盖未处理的数据。