4. DMA描述符链概念:描述符的定义、描述符链表的组织方式、链式传输与循环传输的区别
DMA描述符链是高性能DMA传输的核心数据结构。描述符(Descriptor)本质上是硬件与软件之间的一份“传输契约”,它精确描述了每一次DMA传输的源地址、目的地址、数据长度以及控制标志。多个描述符通过指针链接形成链表,使得DMA控制器能够在不依赖CPU干预的情况下,连续执行一系列传输任务。
4.1 描述符的定义与结构
描述符通常由硬件厂商定义,其字段布局因DMA控制器架构而异。一个典型的DMA描述符包含以下关键字段:
| 字段 | 位宽 | 描述 |
|---|---|---|
| SRC_ADDR | 32/64 bit | 源数据缓冲区物理地址 |
| DST_ADDR | 32/64 bit | 目的数据缓冲区物理地址 |
| TRANSFER_LEN | 16/32 bit | 本次传输的字节数 |
| CTRL_FLAGS | 16/32 bit | 控制标志:中断使能、链式使能、传输方向等 |
| NEXT_DESC_PTR | 32/64 bit | 下一个描述符的物理地址(链式关键字段) |
| STATUS | 16/32 bit | 传输完成状态、错误标志(由硬件写入) |
驱动开发要点:描述符必须存放在DMA可访问的内存区域(通常是DMA ZONE或一致性内存池),且地址对齐要求严格(通常32位描述符要求4字节对齐,64位要求8字节对齐)。驱动在分配描述符内存时,应使用
dma_alloc_coherent() 或 dmam_alloc_coherent() 确保物理地址连续且缓存一致性。
以下是一个基于TI OMAP/Linux内核的典型描述符结构体定义示例:
/* 32位DMA描述符结构体 */
struct dma_descriptor {
__le32 src_addr; /* 源地址 */
__le32 dst_addr; /* 目的地址 */
__le16 transfer_len; /* 传输长度(字节) */
__le16 ctrl_flags; /* 控制标志 */
__le32 next_desc_ptr; /* 下一个描述符指针 */
__le32 status; /* 状态字(硬件更新) */
__le32 reserved; /* 保留字段 */
} __attribute__((packed, aligned(4)));
4.2 描述符链表的组织方式
描述符链表(Descriptor Chain)是将多个描述符通过 NEXT_DESC_PTR 字段串联起来的单向链表。DMA控制器从链表头开始,依次取出描述符执行传输,直到遇到 NEXT_DESC_PTR 为NULL或链尾标志。
链表组织方式分为两种主流模式:
- 静态链表:驱动在初始化时一次性分配所有描述符,构建完整的链表。传输过程中不动态增减节点,适合固定数量的批量传输。
- 动态链表:驱动在运行时动态分配描述符并插入链表尾部,同时回收已完成的描述符。适合流式数据、不确定传输次数的场景。
链表组织核心规则:
- 链表头地址必须写入DMA控制器的描述符基址寄存器(Descriptor Base Address Register)。
- 每个描述符的
NEXT_DESC_PTR必须指向下一个描述符的物理地址,最后一个描述符的NEXT_DESC_PTR置为0或特殊标志。 - 描述符的
CTRL_FLAGS中必须设置链式使能位(Chain Enable),否则DMA控制器不会解析NEXT_DESC_PTR。 - 所有描述符必须位于同一4KB页内或满足硬件地址对齐要求,否则可能触发DMA地址错误。
下图展示了描述符链表的典型组织架构:
4.3 链式传输与循环传输的区别
链式传输(Chained Transfer)和循环传输(Cyclic Transfer)是描述符链的两种典型工作模式,它们在链表结构、传输行为和适用场景上有本质区别。
| 对比维度 | 链式传输(Chained) | 循环传输(Cyclic) |
|---|---|---|
| 链表结构 | 单向链表,最后一个描述符的NEXT为NULL | 环形链表,最后一个描述符的NEXT指向链表头 |
| 传输行为 | 从链表头开始,依次执行,执行完链尾后停止 | 从链表头开始,执行完链尾后自动回到链表头,无限循环 |
| CPU干预 | 传输完成后需要CPU重新配置或更新链表 | 传输持续进行,CPU只需在需要时更新描述符内容 |
| 中断频率 | 通常在链尾产生一次完成中断 | 可配置每轮循环完成中断或每描述符中断 |
| 典型场景 | 批量数据搬运、文件I/O、网络数据包收发 | 音频流播放、ADC/DAC连续采样、PWM波形生成 |
| 描述符复用 | 通常一次性使用,完成后回收或重新初始化 | 描述符内容可被硬件反复读取,驱动需在后台更新 |
| 带宽特性 | 单次传输带宽高,但链间切换有微小延迟 | 持续带宽稳定,适合实时数据流 |
驱动开发常见陷阱:
- 循环传输的缓存一致性问题:在循环模式下,硬件持续读取描述符,驱动更新描述符时必须确保CPU缓存与DMA一致性。建议使用
dma_sync_single_for_device()在更新后刷新缓存。 - 链式传输的原子性:当DMA正在执行某个描述符时,驱动不应修改该描述符的内容。建议使用“乒乓缓冲”或“双缓冲”策略,交替使用两组描述符链。
- 链尾处理:链式传输完成后,DMA控制器可能进入空闲状态。驱动应在中断处理中及时提交新的描述符链,避免总线空闲导致带宽浪费。
下图直观展示了链式传输与循环传输在链表结构上的核心差异:
4.4 描述符链管理的驱动实现要点
在Linux内核DMA引擎驱动中,描述符链的管理通常遵循以下流程:
- 描述符池初始化:在驱动probe阶段,使用
dma_pool_create()创建描述符内存池,预分配固定数量的描述符。 - 链表构建:根据传输需求,从池中取出描述符,填充地址、长度、控制标志,并设置
NEXT_DESC_PTR链接关系。 - 链表提交:将链表头描述符的物理地址写入DMA控制器的描述符基址寄存器,然后启动DMA通道。
- 完成回调:在DMA完成中断中,遍历已完成的描述符,调用上层回调函数,并将描述符归还到空闲池。
- 错误恢复:当DMA传输错误发生时,驱动需要停止当前传输,清理异常描述符,并重新构建有效的描述符链。
带宽优化提示:描述符链的长度直接影响DMA传输效率。链过短会导致频繁中断和CPU干预,降低有效带宽;链过长则占用大量内存且增加延迟。建议根据系统负载和传输数据量动态调整链长度,典型值为8~64个描述符。对于循环传输模式,建议使用双缓冲(Double Buffering)策略,即准备两组描述符链,一组由硬件执行,另一组由软件更新,交替使用以消除等待时间。