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)。三者通过链表在内存中构成完整的批量传输调度流水线。

核心要点:HCCA 是主机控制器与驱动共享的“调度入口”;ED 描述端点的属性与队列状态;TD 描述每一次传输的缓冲区与状态。三者必须严格对齐到 32 字节边界,且驱动需主动处理 Cache 一致性。

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 保留,驱动不应使用
HCCA 内存布局与调度入口 HCCA (256 字节,256 字节对齐) HccaInterruptTable[32] — 128 字节 每项 4 字节,指向中断 ED 链表头 HccaFrameNumber HccaDoneHead Reserved 已完成 TD 链表 帧计数器递增 中断 ED 链表
调试提示:HccaDoneHead 是驱动回收已完成 TD 的关键入口。主机控制器每完成一个 TD,会将其挂入 Done 队列并触发中断。驱动在中断服务例程中读取 HccaDoneHead,遍历链表回收 TD 并更新请求状态。

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 时,队列为空。

ED 与 TD 队列关系 ED HeadTD → TD1 TailTD → TD3 FA=0x02, EP=1, IN TD1 Buffer: 0xA000 NextTD → TD2 TD2 Buffer: 0xB000 NextTD → TD3 TD3 Buffer: 0xC000 NextTD → NULL 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 字段在部分实现中可用)。
常见错误:驱动分配 ED/TD 时未使用 cache-line 对齐分配函数(如 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 分配一致性内存
Cache 一致性:DMA 与 CPU 的协同 CPU 读/写缓冲区 Cache 系统内存 缓冲区 / ED / TD 主机控制器 DMA 引擎 dma_sync_* DMA 读/写 Cache 一致性操作流程 OUT: dma_map_single → 启动 DMA → 等待完成 → dma_unmap_single IN: dma_map_single → 启动 DMA → 等待完成 → dma_sync_single_for_cpu → 读取数据

4.5 批量传输的 DMA 协同流程

  1. 分配与初始化:驱动使用 dma_alloc_coherent 分配 ED 和 TD 内存(保证一致性),使用 kmalloc + dma_map_single 分配数据缓冲区。
  2. 构建链表:将 TD 链接成单向链表,设置 ED 的 HeadTD 指向第一个 TD,TailTD 指向最后一个 TD 的下一个位置(通常为 NULL 或占位 TD)。
  3. 挂入调度:将 ED 挂入 HCCA 的 BulkHeadED 链表(或 Periodic 链表用于中断传输)。
  4. 启动传输:写 HcCommandStatus 寄存器中的 BulkListFilled 位,通知主机控制器开始处理批量链表。
  5. 完成回收:主机控制器每完成一个 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 寄存器。