4. DMA 传输模式:单次传输、块传输、请求传输、链式传输(Scatter-Gather)原理与对比
DMA 控制器支持多种传输模式,以适应不同外设带宽、数据粒度与系统延迟要求。选择错误的传输模式会导致总线占用过高、Cache 抖动加剧或外设 FIFO 溢出。本节从硬件调度机制、总线占用特征与 Cache 一致性影响三个维度,对比四种核心传输模式。
4.1 传输模式分类与核心特征
| 传输模式 | 触发方式 | 总线占用 | 适用场景 | Cache 一致性风险 |
|---|---|---|---|---|
| 单次传输 | 单次请求触发 | 低(每次释放总线) | 低速外设(UART、GPIO) | 低(数据量小) |
| 块传输 | 单次请求触发整块 | 高(连续占用总线) | 高速块设备(MMC、NAND) | 中(需整块同步) |
| 请求传输 | 外设请求信号(DREQ) | 动态(按需占用) | 流式外设(I2S、SPI) | 高(频繁中断) |
| 链式传输 | LLI 链表自动加载 | 低(分散传输) | 非连续内存(网络、视频) | 高(多段地址) |
工程提示: 在嵌入式 Linux 中,
dmaengine 框架通过 dma_slave_config 的 src_addr_width 和 dst_maxburst 参数隐式选择传输模式。块传输对应 DMA_PREP_INTERRUPT 标志,链式传输对应 DMA_CTRL_ACK 与 DMA_PREP_FENCE 组合。
4.2 单次传输(Single Transfer)
每次 DMA 请求只传输一个数据单元(字节、半字或字),传输完成后释放总线控制权。适用于外设 FIFO 深度小、数据到达间隔不确定的场景。
- 调度特征: 每次传输需重新仲裁总线,延迟较高但公平性好。
- Cache 影响: 单次传输数据量通常小于 Cache line 大小(64 字节),每次传输后需执行
dma_sync_single_for_cpu或dma_sync_single_for_device。 - 典型外设: UART 16550、低速 SPI 从设备。
性能指标: 单次传输模式在 ARM Cortex-A72 上,DMA 请求延迟约 200–400 ns,有效带宽利用率仅 30%–50%,因频繁总线仲裁导致。
4.3 块传输(Block Transfer)
一次 DMA 请求传输一个完整的数据块(通常为 512 字节、1 KB 或 4 KB),传输期间连续占用总线,直到块传输完成才释放。适用于高速块设备,如 eMMC、NAND Flash。
- 调度特征: 块传输期间 CPU 与其它 DMA 通道无法访问总线,可能导致高优先级中断延迟增加。
- Cache 影响: 块传输通常跨越多个 Cache line,需在传输前后执行整块
dma_map_single/dma_unmap_single,保证 Cache 与内存一致性。 - 典型外设: eMMC 控制器、AHCI SATA、USB 批量端点。
一致性陷阱: 块传输模式下,若 DMA 写入内存后 CPU 读取同一地址,但 Cache 中仍存有旧数据(Cache 命中),将导致数据不一致。必须在 DMA 完成回调中调用
dma_sync_single_for_cpu 或 dma_unmap_single 使 Cache 失效。
4.4 请求传输(Demand Transfer / Request Transfer)
外设通过 DREQ 信号逐次请求数据传输,DMA 控制器每次响应后传输一个突发(burst),然后等待下一个 DREQ。适用于流式外设,数据以恒定速率到达。
- 调度特征: 传输速率由外设 DREQ 频率决定,DMA 控制器在每次突发后释放总线,允许其他主设备介入。
- Cache 影响: 突发大小通常为 4–16 字,与 Cache line 对齐时一致性开销最小;若不对齐,可能触发读-修改-写(RMW)操作。
- 典型外设: I2S 音频接口、高速 SPI、Camera 接口。
优化建议: 请求传输模式下,将
maxburst 设置为 Cache line 大小(如 16 字 = 64 字节)的整数倍,可避免 DMA 写入时触发部分 Cache line 失效,减少一致性维护开销。
4.5 链式传输(Scatter-Gather / Linked List Transfer)
链式传输通过内存中的链表(Linked List Item, LLI)描述多个不连续的源/目标地址段,DMA 控制器自动加载 LLI 并依次执行传输,无需 CPU 逐段干预。这是解决非连续物理内存 DMA 传输的核心方案。
- 调度特征: 每个 LLI 传输完成后,DMA 控制器自动从内存加载下一个 LLI,支持循环、跳转和中断标志。
- Cache 影响: 每个 LLI 指向的缓冲区可能位于不同 Cache line,需逐段执行
dma_map_sg/dma_unmap_sg,一致性维护复杂度最高。 - 典型外设: 以太网控制器(描述符环)、视频编解码器、NVMe SSD。
关键约束: LLI 链表本身必须位于非 Cache 区域(或执行
dma_alloc_coherent 分配),否则 DMA 控制器读取 LLI 时可能读到 Cache 中的陈旧数据,导致传输地址错误。这是链式传输中最隐蔽的 Bug 来源。
4.6 四种模式对比总结
| 对比维度 | 单次传输 | 块传输 | 请求传输 | 链式传输 |
|---|---|---|---|---|
| 总线占用时间 | 短(1 数据单元) | 长(整块) | 中(突发长度) | 分散(多段) |
| CPU 干预频率 | 高(每次传输) | 低(每块一次) | 中(每突发一次) | 极低(链表自动) |
| Cache 同步粒度 | 单字/半字 | 整块 | 突发对齐 | 逐段 |
| Linux API | dmaengine_prep_slave_single |
dmaengine_prep_slave_sg(单段) |
dmaengine_prep_slave_sg(循环) |
dmaengine_prep_slave_sg(多段) |
| 典型延迟 | 200–400 ns | 1–10 µs(含仲裁) | 500 ns–2 µs | 2–20 µs(链表加载) |
选型指南:
- 外设 FIFO 深度 < 4 字 → 单次传输
- 外设 FIFO 深度 ≥ 16 字且数据连续 → 块传输
- 外设数据速率恒定且需低延迟 → 请求传输
- 内存非连续且传输量大 → 链式传输
4.7 链式传输的 LLI 结构示例
以下代码展示一个典型的 Scatter-Gather LLI 链表初始化过程,使用 dma_alloc_coherent 分配 LLI 内存以避免 Cache 一致性问题。
struct lli_entry {
u32 src_addr;
u32 dst_addr;
u32 next_lli;
u32 control;
};
struct lli_entry *lli_base;
dma_addr_t lli_phys;
/* 分配非 Cache 的 LLI 内存 */
lli_base = dma_alloc_coherent(dev, NUM_LLI * sizeof(*lli_base),
&lli_phys, GFP_KERNEL);
/* 初始化第 0 个 LLI */
lli_base[0].src_addr = src_buf_phys_0;
lli_base[0].dst_addr = dst_buf_phys_0;
lli_base[0].next_lli = lli_phys + sizeof(*lli_base); /* 指向下一个 LLI */
lli_base[0].control = LLI_CTRL_TCI | (512 << LLI_TRANSFER_SIZE_SHIFT);
/* 初始化第 1 个 LLI */
lli_base[1].src_addr = src_buf_phys_1;
lli_base[1].dst_addr = dst_buf_phys_1;
lli_base[1].next_lli = 0; /* 链表结束 */
lli_base[1].control = LLI_CTRL_TCI | (1024 << LLI_TRANSFER_SIZE_SHIFT);
/* 配置 DMA 控制器从 lli_phys 开始加载 */
dma_engine_desc = dmaengine_prep_slave_sg(chan, &sg_table, 2,
DMA_DEV_TO_MEM,
DMA_PREP_INTERRUPT);
调试要点: 链式传输中,若
next_lli 指向的物理地址错误或 LLI 内容被 Cache 污染,DMA 控制器将加载非法描述符,导致总线错误(Bus Fault)。使用 dma_alloc_coherent 分配 LLI 内存是标准做法,也可使用 dma_pool 管理小尺寸 LLI 对象。