4. OHCI 中的 DMA 架构:HCCA、ED、TD 数据结构详解
OHCI(Open Host Controller Interface)通过 DMA(Direct Memory Access)实现主机内存与 USB 设备之间的批量数据传输,无需 CPU 逐字节干预。其核心依赖三种数据结构:HCCA(Host Controller Communication Area)、ED(Endpoint Descriptor)和 TD(Transfer Descriptor)。三者通过链表在内存中构成完整的批量传输调度流水线。
4.1 HCCA:调度入口与中断状态
HCCA 是主机控制器在每帧(1 ms)开始读取的第一个数据结构,位于系统内存中,由驱动分配并填充其物理地址到 HcHCCA 寄存器。HCCA 大小为 256 字节,必须 256 字节对齐。
| 偏移 | 大小 | 字段 | 描述 |
|---|---|---|---|
| 0x00 | 128 字节 | HccaInterruptTable[32] | 中断端点 ED 链表头指针数组,每项 4 字节,共 32 项 |
| 0x80 | 4 字节 | HccaFrameNumber | 当前帧号(16 位有效,主机控制器每帧递增) |
| 0x84 | 2 字节 | HccaPad1 | 保留填充 |
| 0x86 | 2 字节 | HccaDoneHead | 已完成 TD 链表头指针(物理地址高 16 位) |
| 0x88 | 116 字节 | HccaReservedArea | 保留,驱动不应使用 |
4.2 ED:端点描述符与批量传输队列
每个 ED 描述一个 USB 端点的属性,包括端点号、设备地址、方向、速度以及 TD 队列头指针。批量传输使用 Bulk ED,挂在 HCCA 的 BulkHeadED 寄存器指向的链表上。
| 位域 | 大小 | 描述 |
|---|---|---|
| 31:30 | 2 位 | FA(Function Address)最高两位 |
| 29:28 | 2 位 | EP(Endpoint Number)最高两位 |
| 27 | 1 位 | D(Direction):0 = OUT(主机到设备),1 = IN(设备到主机) |
| 26 | 1 位 | S(Speed):0 = 全速,1 = 低速 |
| 25:24 | 2 位 | K(Skip):批量传输通常置 0 |
| 23:16 | 8 位 | FA[7:0](Function Address 低 8 位) |
| 15:14 | 2 位 | EP[1:0](Endpoint Number 低 2 位) |
| 13:0 | 14 位 | MaxPacketSize(最大包长,批量传输通常为 64 或 512) |
ED 的 TailTD 和 HeadTD 指针构成 TD 队列。TailTD 指向队列末尾(驱动追加新 TD 的位置),HeadTD 指向主机控制器当前正在处理的 TD。当 HeadTD 追上 TailTD 时,队列为空。
4.3 TD:传输描述符与缓冲区管理
TD 描述一次 USB 传输的缓冲区地址、传输长度、完成状态以及下一个 TD 的链接。批量传输使用 General TD 格式,大小为 16 字节,必须 32 字节对齐。
| 偏移 | 大小 | 字段 | 描述 |
|---|---|---|---|
| 0x00 | 4 字节 | NextTD | 下一个 TD 的物理地址(32 字节对齐,低 5 位为标志位) |
| 0x04 | 4 字节 | BufferEnd | 缓冲区结束地址(包含最后一个字节) |
| 0x08 | 4 字节 | BufferStart | 缓冲区起始物理地址(页对齐要求见下文) |
| 0x0C | 4 字节 | Flags / Status | 传输方向、数据切换、完成码、实际传输长度等 |
内存对齐要求:
- HCCA:256 字节对齐(地址低 8 位为 0)。
- ED:32 字节对齐(地址低 5 位为 0)。
- TD:32 字节对齐(地址低 5 位为 0)。
- BufferStart:必须页对齐(4 KB 边界),除非使用偏移量机制(Offset 字段在部分实现中可用)。
kmem_cache_alloc 配合 SLAB_HWCACHE_ALIGN),导致 DMA 操作跨越 Cache 行边界,引发数据损坏。批量传输大数据块时尤其容易触发。
4.4 Cache 一致性挑战与解决方案
在 ARM、x86 等带 Cache 的处理器上,DMA 传输绕过 CPU Cache,直接访问物理内存。如果驱动在分配缓冲区后修改了数据但未刷 Cache,主机控制器可能读到过时的 Cache 数据(OUT 传输);或者主机控制器写入数据后,CPU 从 Cache 读取到旧值(IN 传输)。
| 场景 | 问题 | 解决方案 |
|---|---|---|
| 批量 OUT(主机→设备) | CPU 写缓冲区后,Cache 中数据未写回内存,DMA 读到旧数据 | 在启动 DMA 前调用 dma_map_single 或 dma_sync_single_for_device |
| 批量 IN(设备→主机) | DMA 写入内存后,CPU 从 Cache 读取到旧数据 | 在 CPU 读取前调用 dma_sync_single_for_cpu 或 dma_unmap_single |
| ED/TD 结构体访问 | 主机控制器读取 ED/TD 时,驱动修改了内存但未刷 Cache | 对 ED/TD 内存区域使用 dma_alloc_coherent 分配一致性内存 |
4.5 批量传输的 DMA 协同流程
- 分配与初始化:驱动使用
dma_alloc_coherent分配 ED 和 TD 内存(保证一致性),使用kmalloc+dma_map_single分配数据缓冲区。 - 构建链表:将 TD 链接成单向链表,设置 ED 的 HeadTD 指向第一个 TD,TailTD 指向最后一个 TD 的下一个位置(通常为 NULL 或占位 TD)。
- 挂入调度:将 ED 挂入 HCCA 的 BulkHeadED 链表(或 Periodic 链表用于中断传输)。
- 启动传输:写 HcCommandStatus 寄存器中的 BulkListFilled 位,通知主机控制器开始处理批量链表。
- 完成回收:主机控制器每完成一个 TD,将其挂入 Done 队列并触发中断。驱动在中断处理中遍历 Done 队列,检查 TD 状态码,回收缓冲区,并更新请求完成回调。
- ED/TD 地址是否 32 字节对齐?使用
printk打印物理地址并检查低 5 位。 - 数据缓冲区是否页对齐?批量传输要求 BufferStart 在 4 KB 边界。
- 是否在启动 DMA 前调用了
dma_sync_single_for_device? - 是否在读取完成数据前调用了
dma_sync_single_for_cpu? - Done 队列中断是否被正确使能?检查 HcInterruptEnable 寄存器。