4. UHCI 传输描述符 (TD)
UHCI 主机控制器通过一个称为“传输描述符”(Transfer Descriptor, TD) 的数据结构来管理 USB 总线上的每一个数据包传输。TD 是 UHCI 调度与 DMA 协同工作的核心单元,它精确描述了“谁、从哪里、传多少、传完后怎么办”。本章将深入剖析 TD 的结构体定义、链接机制、控制状态位以及实际传输长度的计算规则。
4.1 TD 结构体定义
UHCI 的 TD 是一个 32 字节(4 个 64 位 QWORD)的固定长度数据结构,必须按 16 字节对齐存放在系统内存中。其结构定义如下:
// UHCI 传输描述符 (TD) 结构体定义
// 共 32 字节,按 16 字节对齐
typedef struct _UHCI_TD {
// QWORD 0: 链接指针 (Link Pointer)
uint32_t LinkPointer; // [31:4] TD 或 QH 物理地址, [3:2] 保留, [1] VF, [0] T
uint32_t Reserved1; // 保留,必须为 0
// QWORD 1: 控制状态 (Control & Status)
uint32_t ControlStatus; // [31:28] 保留, [27] IOC, [26] IOS, [25] LS, [24:23] SPD,
// [22:21] 保留, [20:16] ErrorCount, [15] CRC/Timeout,
// [14] BitStuff, [13] BufferError, [12] Active,
// [11] Stalled, [10] DataBufferError, [9] Babble,
// [8] NAK, [7:4] 保留, [3:0] ActualLength
uint32_t Reserved2; // 保留,必须为 0
// QWORD 2: 令牌 (Token)
uint32_t Token; // [31:30] PID (00=OUT, 01=IN, 10=SETUP),
// [29:21] DeviceAddress, [20:18] Endpoint,
// [17:16] DataToggle, [15:8] MaxPacketSize,
// [7:0] 保留
uint32_t Reserved3; // 保留,必须为 0
// QWORD 3: 缓冲区指针 (Buffer Pointer)
uint32_t BufferPointer; // 数据缓冲区物理地址 (4KB 对齐)
uint32_t Reserved4; // 保留,必须为 0
} UHCI_TD;
对齐要求: TD 必须按 16 字节边界对齐。如果 TD 地址未对齐,UHCI 主机控制器会产生未定义行为,导致 DMA 传输失败。在嵌入式系统中,建议使用
__attribute__((aligned(16))) 或链接脚本强制对齐。
4.2 链接指针 (Link Pointer)
链接指针是 UHCI 实现“链表调度”的关键字段。它位于 TD 的第一个 DWORD,格式如下:
| 位域 | 宽度 | 描述 |
|---|---|---|
| [31:4] | 28 位 | 下一个 TD 或 QH 的物理地址(高 28 位,低 4 位隐含为 0) |
| [3:2] | 2 位 | 保留,必须为 0 |
| [1] VF | 1 位 | 垂直标志位 (Vertical Flag):0 = 链接到下一个 TD,1 = 链接到 QH |
| [0] T | 1 位 | 终止位 (Terminate):0 = 链表继续,1 = 链表结束 |
链接指针支持三种链表拓扑:
- TD → TD 水平链接: VF=0, T=0,指向下一个同层 TD,形成水平队列。
- TD → QH 垂直链接: VF=1, T=0,指向一个队列头 (QH),用于中断端点或批量端点调度。
- 链表终止: T=1,表示链表结束,UHCI 停止遍历该分支。
DMA 协同要点: 当 UHCI 通过 DMA 读取 TD 时,链接指针的地址必须指向物理内存中已对齐的 TD 或 QH。嵌入式调试端口可以利用此机制,在调试模式下动态修改链接指针,将调试 TD 注入到正常传输链中,实现“无侵入”的数据捕获。
4.3 控制状态位 (Control & Status)
控制状态字段是 TD 的“大脑”,它控制传输行为并记录传输结果。该字段位于 TD 的第二个 DWORD,位定义如下:
| 位域 | 宽度 | 名称 | 描述 |
|---|---|---|---|
| [27] | 1 | IOC | 中断完成位 (Interrupt on Complete):置 1 时,该 TD 完成后触发中断 |
| [26] | 1 | IOS | 中断状态位 (Interrupt on Status):置 1 时,TD 状态变化时触发中断 |
| [25] | 1 | LS | 低速设备位 (Low Speed):1 = 低速传输,0 = 全速传输 |
| [24:23] | 2 | SPD | 传输速度/方向:00 = 全速 OUT,01 = 全速 IN,10 = 低速 OUT,11 = 低速 IN |
| [20:16] | 5 | ErrorCount | 错误计数:记录该 TD 重试次数,最大 31 次 |
| [15] | 1 | CRC/Timeout | CRC 错误或超时:1 = 发生 CRC 错误或总线超时 |
| [14] | 1 | BitStuff | 位填充错误:1 = 检测到位填充错误 |
| [13] | 1 | BufferError | 缓冲区错误:1 = 缓冲区指针无效或越界 |
| [12] | 1 | Active | 激活位:1 = TD 等待处理,0 = TD 已完成或出错 |
| [11] | 1 | Stalled | 端点停止:1 = 设备返回 STALL 握手 |
| [10] | 1 | DataBufferError | 数据缓冲区错误:1 = DMA 访问缓冲区时发生错误 |
| [9] | 1 | Babble | 设备发言错误:1 = 设备传输数据超过 MaxPacketSize |
| [8] | 1 | NAK | NAK 接收:1 = 设备返回 NAK(仅用于 IN 传输) |
| [3:0] | 4 | ActualLength | 实际传输长度(以字节为单位,最大 15 字节) |
调试陷阱: 在嵌入式调试端口中,
Active 位是轮询 TD 完成状态的关键标志。当 UHCI 完成一个 TD 后,它会将 Active 位清零并更新错误位和实际长度。调试软件应始终先读取 Active 位,再读取其他状态位,以避免读取到中间状态。
4.4 实际传输长度 (ActualLength)
实际传输长度字段位于控制状态字的 [3:0] 位,是一个 4 位无符号整数,表示该 TD 实际传输的字节数。其取值范围为 0~15,但实际含义取决于传输方向:
| 传输方向 | ActualLength 含义 | 典型值 |
|---|---|---|
| OUT (主机→设备) | 主机实际发送的字节数 | 通常等于 MaxPacketSize 或小于 MaxPacketSize(短包) |
| IN (设备→主机) | 设备实际发送的字节数 | 0~MaxPacketSize,0 表示 NAK 或零长度包 |
| SETUP | 固定为 8(USB 规范要求 SETUP 包为 8 字节) | 8 |
实际传输长度与 MaxPacketSize 的关系决定了“短包”检测逻辑:
- 正常完成: ActualLength == MaxPacketSize,表示传输了完整的数据包。
- 短包: ActualLength < MaxPacketSize,表示传输结束或设备发送了短包。
- 零长度包: ActualLength == 0,用于 IN 传输中的 NAK 或显式零长度包。
- 错误情况: 如果 Babble 位置 1,ActualLength 可能无效,应丢弃该 TD 的数据。
DMA 协同优化: 在调试端口场景中,DMA 控制器可以在 UHCI 更新 TD 的 ActualLength 后,自动触发下一次 DMA 传输。通过将 ActualLength 与预定义的阈值比较,调试端口可以智能地决定是继续填充当前缓冲区还是切换到下一个缓冲区,实现“零 CPU 干预”的数据流管理。
4.5 令牌字段 (Token) 与缓冲区指针
令牌字段定义了 USB 传输的“身份信息”,包括 PID、设备地址、端点号、数据切换位和最大包大小。缓冲区指针则指向数据在系统内存中的物理地址。
| 字段 | 位置 | 描述 |
|---|---|---|
| PID | [31:30] | 包标识符:00=OUT, 01=IN, 10=SETUP |
| DeviceAddress | [29:21] | USB 设备地址(0~127) |
| Endpoint | [20:18] | 端点号(0~7) |
| DataToggle | [17:16] | 数据切换位:00=DATA0, 01=DATA1, 10/11=保留 |
| MaxPacketSize | [15:8] | 最大包大小(字节),全速最大 64,低速最大 8 |
| BufferPointer | QWORD 3 | 数据缓冲区物理地址,必须 4KB 对齐 |
调试端口注入技巧: 在调试模式下,可以通过修改 TD 的 BufferPointer 字段,将数据重定向到调试专用的 DMA 缓冲区。结合 IOC 中断位,调试端口可以在每个 TD 完成后立即捕获数据,而无需修改 USB 设备驱动。
4.6 TD 链表操作示例
以下代码展示了如何在嵌入式系统中初始化一个 TD 链表,并设置 DMA 传输:
// TD 链表初始化示例
#define TD_COUNT 4
UHCI_TD td_pool[TD_COUNT] __attribute__((aligned(16)));
uint8_t data_buffer[TD_COUNT][64] __attribute__((aligned(4096)));
void td_chain_init(void) {
for (int i = 0; i < TD_COUNT; i++) {
// 清空 TD
memset(&td_pool[i], 0, sizeof(UHCI_TD));
// 设置链接指针:水平链接,最后一个 TD 终止
if (i < TD_COUNT - 1) {
td_pool[i].LinkPointer = (uint32_t)(uintptr_t)&td_pool[i+1] | 0; // VF=0, T=0
} else {
td_pool[i].LinkPointer = 1; // T=1, 终止
}
// 设置控制状态:激活,全速 IN,IOC 使能
td_pool[i].ControlStatus = (1 << 12) // Active
| (1 << 27) // IOC
| (1 << 23); // SPD=01 (全速 IN)
// 设置令牌:IN 传输,设备地址 2,端点 1,DATA0,最大包 64
td_pool[i].Token = (1 << 30) // PID=01 (IN)
| (2 << 21) // DeviceAddress=2
| (1 << 18) // Endpoint=1
| (0 << 16) // DataToggle=DATA0
| (64 << 8); // MaxPacketSize=64
// 设置缓冲区指针
td_pool[i].BufferPointer = (uint32_t)(uintptr_t)data_buffer[i];
}
}
// DMA 完成回调:检查 ActualLength 并处理数据
void td_dma_complete(UHCI_TD *td) {
uint32_t status = td->ControlStatus;
if (status & (1 << 12)) {
// Active 仍为 1,TD 未完成,等待
return;
}
uint8_t actual_len = status & 0x0F;
if (status & (1 << 9)) {
// Babble 错误,丢弃数据
debug_printf("Babble error on TD %p\n", td);
return;
}
// 处理实际传输的数据
debug_printf("TD completed: actual_len=%d bytes\n", actual_len);
process_usb_data(td->BufferPointer, actual_len);
}
关键设计原则: 在嵌入式调试端口中,TD 链表应设计为“循环队列”或“双缓冲”模式。当 UHCI 完成一个 TD 后,调试软件应立即重新初始化该 TD 并将其重新链接到链表尾部,确保 DMA 传输永不中断。ActualLength 字段是判断数据有效性的第一依据,必须在处理数据前校验。