4. DMA 描述符与传输链:描述符结构、链表传输与硬件自动加载机制

DMA 描述符(Descriptor)是 DMA 控制器与软件之间交换传输元数据的核心数据结构。单个描述符定义一次传输的源地址、目的地址、数据长度及控制标志;多个描述符通过指针链接形成传输链(Descriptor Chain),使硬件能够在不中断 CPU 的情况下连续执行批量传输。本章深入分析描述符结构、链表传输模式以及硬件自动加载描述符的流水线机制。

4.1 描述符结构设计

描述符通常由硬件固件定义,但软件必须按照固定格式填充。不同 SoC 的描述符字段略有差异,但核心字段高度一致。下表列出典型 DMA 描述符的通用字段布局:

字段 偏移(字节) 宽度(位) 说明
SRC_ADDR 0x00 32/64 源物理地址(内存或外设 FIFO)
DST_ADDR 0x04/0x08 32/64 目的物理地址
CONTROL 0x08/0x10 32 传输长度、突发大小、传输类型、中断使能等
NEXT_PTR 0x0C/0x18 32/64 下一个描述符的物理地址(链表关键字段)
STATUS 0x10/0x20 32 硬件回写状态:完成标志、错误码、传输字节数
对齐要求:描述符基地址必须按 32 字节或 64 字节对齐(取决于 DMA 控制器设计),否则硬件可能忽略低地址位或触发总线错误。在分配描述符内存时,应使用 dma_alloc_coherent() 或 kmalloc() 配合 ALIGN() 宏。

以下是一个基于 ARM PL080 DMA 控制器的描述符结构体定义(简化版):

/* 描述符结构体,必须按 32 字节对齐 */
struct dma_descriptor {
    u32 src_addr;       /* 源地址 */
    u32 dst_addr;       /* 目的地址 */
    u32 control;        /* 控制字:位[0:11]传输长度,位[12:14]突发大小 */
    u32 next;           /* 下一个描述符物理地址,0 表示链表结束 */
    u32 status;         /* 硬件回写:位[0]完成标志,位[1]错误标志 */
    u32 reserved[3];    /* 保留字段,硬件可能用于内部状态 */
} __attribute__((aligned(32)));
性能要点:描述符的 CONTROL 字段中的中断使能位应仅在链表最后一个描述符中置位,避免每个描述符都触发中断,从而减少 CPU 中断开销。批量传输时,CPU 仅在整条链完成后处理一次中断。

4.2 链表传输模式

链表传输(Scatter-Gather DMA)是高性能 DMA 的核心模式。软件预先构建一个描述符链表,每个描述符指向一块不连续的内存区域,硬件按顺序自动遍历链表,完成所有传输后才通知 CPU。这种模式消除了 CPU 对每个数据块的干预,显著降低传输延迟。

下图展示了一个包含 4 个描述符的链表传输结构:

DMA 描述符链表传输结构 DMA 描述符链表传输结构 描述符 0 SRC: 0x1000 DST: 0x5000 LEN: 1024 描述符 1 SRC: 0x2000 DST: 0x6000 LEN: 2048 描述符 2 SRC: 0x3000 DST: 0x7000 LEN: 512 描述符 3 SRC: 0x4000 DST: 0x8000 LEN: 4096 数据块 0 (1KB) 数据块 1 (2KB) 数据块 2 (0.5KB) 数据块 3 (4KB) 链表传输流程: 1. CPU 构建描述符链表,将首描述符地址写入 DMA 控制器的 LLI 寄存器。 2. 硬件自动加载描述符 0,从 SRC 地址读取数据写入 DST 地址。 3. 传输完成后,硬件通过 NEXT_PTR 加载描述符 1,无需 CPU 参与。 4. 当遇到 NEXT_PTR = 0 时,硬件触发中断通知 CPU 整链传输完成。
避坑指南:描述符链表必须位于非缓存(non-cacheable)或一致性(coherent)内存区域。如果描述符被 CPU 缓存,硬件读取到的可能是陈旧数据,导致传输地址错误。使用 dma_alloc_coherent() 分配描述符内存是最安全的方式。

4.3 硬件自动加载描述符机制

硬件自动加载描述符(Hardware Auto-Fetch)是现代 DMA 控制器的关键特性。当软件将链表首描述符地址写入 DMA 控制器的链表基址寄存器(LLI Base Address Register)并启动通道后,DMA 控制器内部的描述符获取引擎(Descriptor Fetch Engine)自动执行以下流水线步骤:

  1. 预取描述符:硬件通过总线读取当前描述符到内部寄存器组。
  2. 解析控制字:提取传输长度、突发大小、传输方向、中断使能等参数。
  3. 执行数据传输:按配置启动源到目的的数据搬移,支持单次或突发传输。
  4. 回写状态:传输完成后,硬件将完成标志和实际传输字节数写入描述符的 STATUS 字段。
  5. 加载下一描述符:读取当前描述符的 NEXT_PTR 字段,若非零则跳转到步骤 1,否则触发传输完成中断。

下图展示硬件自动加载描述符的流水线时序:

硬件自动加载描述符流水线时序 硬件自动加载描述符流水线时序 T0 T1 T2 T3 T4 预取描述符 0 解析控制字 数据传输 回写状态 加载下一描述符 流水线关键特性: • 预取与传输可重叠:硬件在传输当前描述符数据的同时,预取下一个描述符。 • 零 CPU 干预:整条链的遍历完全由硬件状态机完成,CPU 仅在链尾中断。 • 突发传输优化:硬件支持在描述符间保持突发对齐,减少总线仲裁开销。 • 错误处理:若描述符地址无效或总线错误,硬件立即终止链并上报错误中断。 • 循环模式:部分 DMA 支持将链表尾指向链表头,实现无限循环传输。

4.4 描述符链表编程实战

以下代码展示如何在 Linux 内核驱动中构建并启动一个包含 3 个描述符的传输链:

/* 描述符链表编程示例 */
#define NR_DESCRIPTORS  3

struct dma_chan_state {
    struct dma_descriptor *desc_virt;   /* 虚拟地址 */
    dma_addr_t desc_phys;               /* 物理地址 */
    struct completion done;
};

static int build_descriptor_chain(struct dma_chan_state *cs)
{
    int i;
    struct dma_descriptor *desc = cs->desc_virt;

    /* 分配一致性内存用于描述符 */
    cs->desc_virt = dma_alloc_coherent(dev, 
                       NR_DESCRIPTORS * sizeof(struct dma_descriptor),
                       &cs->desc_phys, GFP_KERNEL);
    if (!cs->desc_virt)
        return -ENOMEM;

    for (i = 0; i < NR_DESCRIPTORS; i++) {
        desc[i].src_addr = src_buf_phys[i];   /* 源缓冲区物理地址 */
        desc[i].dst_addr = dst_buf_phys[i];   /* 目的缓冲区物理地址 */
        desc[i].control  = (len[i] & 0xFFF) |   /* 传输长度 */
                           (BURST_16 << 12) |   /* 突发大小 */
                           (i == NR_DESCRIPTORS-1 ? INT_EN : 0); /* 最后一项使能中断 */
        desc[i].next     = (i < NR_DESCRIPTORS-1) ? 
                           cs->desc_phys + (i+1) * sizeof(struct dma_descriptor) : 0;
        desc[i].status   = 0;
        /* 确保描述符写入内存,硬件可见 */
        wmb();  /* 写内存屏障 */
    }
    return 0;
}

static void start_dma_chain(struct dma_chan_state *cs)
{
    /* 将链表基址写入 DMA 控制器的 LLI 寄存器 */
    writel(cs->desc_phys, dma_base + DMA_LLI_REG);
    /* 使能通道并启动传输 */
    writel(CH_ENABLE | START_TRANSFER, dma_base + DMA_CTRL_REG);
}
性能优化技巧:
  • 描述符预分配:在驱动初始化时一次性分配描述符内存池,避免运行时动态分配带来的延迟。
  • 批量提交:将多个小传输合并为一个描述符链,减少 DMA 启动次数。
  • 缓存一致性:在构建描述符后调用 dma_sync_single_for_device(),确保 CPU 写入对 DMA 控制器可见。
  • 中断聚合:仅在链尾描述符使能中断,配合 NAPI 或 threaded IRQ 减少中断上下文开销。

4.5 描述符模式对比

特性 单描述符模式 链表模式(Scatter-Gather) 循环链表模式
CPU 干预 每次传输后需 CPU 重新配置 仅链尾触发一次中断 无需中断,硬件自动循环
内存连续性 要求源/目的连续 支持不连续内存块 支持不连续内存块
传输效率 低(频繁中断) 高(流水线预取) 极高(零中断开销)
适用场景 简单外设控制 网络、存储、多媒体 音频流、数据采集
描述符内存 单个描述符 描述符数组或链表 描述符环形缓冲区
错误恢复 简单(重新配置) 需遍历链表定位错误描述符 需维护环形指针状态
工程建议:在嵌入式 Linux 中,网络驱动(如 stmmac、dwc_eth_qos)和存储驱动(如 dw_mmc)广泛使用描述符链表模式。阅读这些驱动的 tx_complete 和 rx_complete 函数,可以深入理解链表传输的实际应用。

4.6 缓存一致性对描述符的影响

描述符本身也面临缓存一致性问题。由于 DMA 控制器直接通过总线读取描述符内存,如果描述符数据仍停留在 CPU 缓存中(写回策略),硬件将读取到陈旧数据。解决方案包括:

  • 使用一致性内存:dma_alloc_coherent() 分配的内存保证 CPU 和 DMA 看到相同数据,无需手动同步。
  • 手动缓存维护:若使用 kmalloc() 分配描述符内存,必须在启动 DMA 前调用 dma_sync_single_for_device(),在传输完成后调用 dma_sync_single_for_cpu()。
  • 描述符只读优化:将描述符放在只读内存区域(如 __read_mostly),避免 CPU 意外修改。
常见错误:在 SMP 系统上,如果描述符链表由多个 CPU 核心共享构建,必须使用自旋锁或原子操作保护链表操作。同时,在启动 DMA 前必须执行 wmb()(写内存屏障),确保所有描述符写入对硬件可见。