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或链尾标志。

链表组织方式分为两种主流模式:

  • 静态链表:驱动在初始化时一次性分配所有描述符,构建完整的链表。传输过程中不动态增减节点,适合固定数量的批量传输。
  • 动态链表:驱动在运行时动态分配描述符并插入链表尾部,同时回收已完成的描述符。适合流式数据、不确定传输次数的场景。
链表组织核心规则:
  1. 链表头地址必须写入DMA控制器的描述符基址寄存器(Descriptor Base Address Register)。
  2. 每个描述符的 NEXT_DESC_PTR 必须指向下一个描述符的物理地址,最后一个描述符的 NEXT_DESC_PTR 置为0或特殊标志。
  3. 描述符的 CTRL_FLAGS 中必须设置链式使能位(Chain Enable),否则DMA控制器不会解析 NEXT_DESC_PTR。
  4. 所有描述符必须位于同一4KB页内或满足硬件地址对齐要求,否则可能触发DMA地址错误。

下图展示了描述符链表的典型组织架构:

DMA描述符链表组织架构图 DMA描述符链表组织架构 DMA控制器 描述符基址寄存器 描述符 0 SRC_ADDR: 0x8000_0000 DST_ADDR: 0x9000_0000 LEN: 1024 FLAGS: CHAIN_EN NEXT: → Desc 1 STATUS: PENDING 描述符 1 SRC_ADDR: 0x8000_0400 DST_ADDR: 0x9000_0400 LEN: 512 FLAGS: CHAIN_EN NEXT: → Desc 2 STATUS: PENDING 描述符 2 SRC_ADDR: 0x8000_0600 DST_ADDR: 0x9000_0600 LEN: 256 FLAGS: CHAIN_EN NEXT: NULL (链尾) STATUS: PENDING 说明:DMA控制器从描述符基址寄存器获取链表头,依次读取描述符0→1→2, 每完成一个描述符即自动加载下一个,直到链尾(NEXT为NULL)停止。

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控制器可能进入空闲状态。驱动应在中断处理中及时提交新的描述符链,避免总线空闲导致带宽浪费。

下图直观展示了链式传输与循环传输在链表结构上的核心差异:

链式传输与循环传输结构对比 链式传输 vs 循环传输 结构对比 链式传输(Chained) 描述符 A NEXT → B 描述符 B NEXT → C 描述符 C NEXT → NULL 停止 执行完C后DMA停止,等待CPU重新配置 循环传输(Cyclic) 描述符 X NEXT → Y 描述符 Y NEXT → Z 描述符 Z NEXT → X ↻ 无限循环 执行完Z后自动回到X,持续传输无需CPU干预

4.4 描述符链管理的驱动实现要点

在Linux内核DMA引擎驱动中,描述符链的管理通常遵循以下流程:

  1. 描述符池初始化:在驱动probe阶段,使用 dma_pool_create() 创建描述符内存池,预分配固定数量的描述符。
  2. 链表构建:根据传输需求,从池中取出描述符,填充地址、长度、控制标志,并设置 NEXT_DESC_PTR 链接关系。
  3. 链表提交:将链表头描述符的物理地址写入DMA控制器的描述符基址寄存器,然后启动DMA通道。
  4. 完成回调:在DMA完成中断中,遍历已完成的描述符,调用上层回调函数,并将描述符归还到空闲池。
  5. 错误恢复:当DMA传输错误发生时,驱动需要停止当前传输,清理异常描述符,并重新构建有效的描述符链。
带宽优化提示:描述符链的长度直接影响DMA传输效率。链过短会导致频繁中断和CPU干预,降低有效带宽;链过长则占用大量内存且增加延迟。建议根据系统负载和传输数据量动态调整链长度,典型值为8~64个描述符。对于循环传输模式,建议使用双缓冲(Double Buffering)策略,即准备两组描述符链,一组由硬件执行,另一组由软件更新,交替使用以消除等待时间。