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 | 硬件回写状态:完成标志、错误码、传输字节数 |
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_alloc_coherent() 分配描述符内存是最安全的方式。
4.3 硬件自动加载描述符机制
硬件自动加载描述符(Hardware Auto-Fetch)是现代 DMA 控制器的关键特性。当软件将链表首描述符地址写入 DMA 控制器的链表基址寄存器(LLI Base Address Register)并启动通道后,DMA 控制器内部的描述符获取引擎(Descriptor Fetch Engine)自动执行以下流水线步骤:
- 预取描述符:硬件通过总线读取当前描述符到内部寄存器组。
- 解析控制字:提取传输长度、突发大小、传输方向、中断使能等参数。
- 执行数据传输:按配置启动源到目的的数据搬移,支持单次或突发传输。
- 回写状态:传输完成后,硬件将完成标志和实际传输字节数写入描述符的 STATUS 字段。
- 加载下一描述符:读取当前描述符的 NEXT_PTR 字段,若非零则跳转到步骤 1,否则触发传输完成中断。
下图展示硬件自动加载描述符的流水线时序:
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 重新配置 | 仅链尾触发一次中断 | 无需中断,硬件自动循环 |
| 内存连续性 | 要求源/目的连续 | 支持不连续内存块 | 支持不连续内存块 |
| 传输效率 | 低(频繁中断) | 高(流水线预取) | 极高(零中断开销) |
| 适用场景 | 简单外设控制 | 网络、存储、多媒体 | 音频流、数据采集 |
| 描述符内存 | 单个描述符 | 描述符数组或链表 | 描述符环形缓冲区 |
| 错误恢复 | 简单(重新配置) | 需遍历链表定位错误描述符 | 需维护环形指针状态 |
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 意外修改。
wmb()(写内存屏障),确保所有描述符写入对硬件可见。