4. DMA 基础概念:DMA 控制器工作原理、传输模式与 CPU 协同
直接存储器访问(Direct Memory Access, DMA)是嵌入式与高性能系统中绕开 CPU 参与数据搬运的核心硬件机制。在 Linux 内核驱动中,DMA 是实现零拷贝、降低 I/O 延迟的基石。本节从控制器内部结构出发,逐步拆解三种传输模式,并厘清 DMA 与 CPU 之间的握手与竞争关系。
4.1 DMA 控制器工作原理
DMA 控制器(DMAC)是一个独立的总线主控设备,能够发起总线事务,直接读写系统内存或外设 FIFO。其内部关键单元包括:
- 控制寄存器组:存放传输方向(内存→外设 / 外设→内存 / 内存→内存)、地址递增模式、传输宽度(字节/半字/字)等配置。
- 源地址寄存器(SAR):当前读取数据的源地址。
- 目的地址寄存器(DAR):当前写入数据的目的地址。
- 传输计数寄存器(CTR):剩余待传输的数据量,每完成一次总线传输自动递减。
- 状态与中断逻辑:检测传输完成、错误、超时等事件,并向 CPU 发出中断请求。
一次典型的 DMA 传输流程如下:
- CPU 通过寄存器编程设置 SAR、DAR、CTR、传输方向与触发源。
- CPU 使能 DMA 通道,DMAC 进入等待触发状态。
- 外设(如 UART、SPI、网卡)发出 DMA 请求信号(DREQ)。
- DMAC 获得总线控制权,执行单次或突发传输,更新地址与计数。
- CTR 归零后,DMAC 产生传输完成中断,通知 CPU 处理数据。
4.2 DMA 传输模式
根据应用场景与硬件能力,DMA 控制器支持三种基本传输模式。下表从触发方式、结束条件、适用场景三个维度进行对比:
| 传输模式 | 触发方式 | 结束条件 | 典型应用 | CPU 干预频率 |
|---|---|---|---|---|
| 单次传输 | 单次 DREQ 脉冲 | CTR 归零 | 低速外设(UART、I²C) | 每次传输完成 |
| 循环传输 | 自动重载 CTR | 软件停止 | 音频流、ADC 连续采样 | 仅初始化与停止 |
| 链表传输 | 硬件自动加载下一描述符 | 链表尾或错误 | 高速网卡、NVMe、GPU | 极低(批量提交) |
4.2.1 单次传输模式
单次传输是最基础的 DMA 模式。CPU 配置好 SAR、DAR、CTR 后启动通道,DMAC 在每次收到外设请求时传输一个数据单元(字节/半字/字),CTR 递减至零后产生中断。该模式实现简单,但每次传输完成都需要 CPU 重新配置,适合数据量小、频率低的外设。
4.2.2 循环传输模式
循环传输在 CTR 归零时自动将初始计数值重载到 CTR,地址也回绕到起始地址,形成环形缓冲区。CPU 只需在初始化时配置一次,后续传输完全自主运行。音频驱动中常用此模式:DMA 持续将麦克风数据写入环形缓冲区,CPU 在后台处理已填满的半区。
dma_alloc_coherent 分配一致性内存),避免缓存一致性问题。
4.2.3 链表传输模式
链表传输(也称描述符链或 scatter-gather DMA)是最高效的模式。CPU 在内存中构建一个描述符链表,每个描述符包含源地址、目的地址、传输长度、下一个描述符指针及控制标志。DMAC 自动从链表头加载描述符,完成当前传输后硬件直接跳转到下一描述符,无需 CPU 介入。
/* Linux 内核 DMA 描述符结构示例(基于 dmaengine 框架) */
struct dma_async_tx_descriptor {
dma_addr_t src; /* 源物理地址 */
dma_addr_t dst; /* 目的物理地址 */
size_t len; /* 传输长度 */
struct dma_async_tx_descriptor *next; /* 链表下一节点 */
enum dma_ctrl_flags flags; /* 控制标志 */
dma_cookie_t cookie; /* 传输 cookie */
/* ... 其他回调与状态字段 */
};
4.3 DMA 与 CPU 的协同
DMA 虽然接管了数据搬运,但 CPU 仍承担配置、同步与异常处理职责。两者之间的协同机制直接影响系统吞吐量与延迟。
4.3.1 协同工作流程
- 配置阶段(CPU 主导):CPU 分配 DMA 缓冲区、设置传输参数、注册完成回调。
- 启动阶段(CPU 主导):CPU 写使能位,DMAC 进入就绪状态。
- 传输阶段(DMA 主导):DMAC 自主搬运数据,CPU 可执行其他任务或进入低功耗模式。
- 同步阶段(CPU 主导):DMAC 通过中断或轮询状态寄存器通知 CPU 传输完成。
- 收尾阶段(CPU 主导):CPU 处理数据、重新填充描述符、启动下一轮传输。
4.3.2 缓存一致性问题
在带缓存(Cache)的 CPU 系统中,DMA 直接访问物理内存,而 CPU 可能看到的是缓存中的陈旧副本。Linux 内核提供以下 API 解决一致性问题:
| API 函数 | 用途 | 调用时机 |
|---|---|---|
dma_alloc_coherent |
分配一致性 DMA 缓冲区(禁用缓存或硬件维护一致性) | 驱动初始化 |
dma_map_single |
将虚拟地址映射为 DMA 地址,执行缓存刷新 | 每次传输前 |
dma_unmap_single |
解除映射,使 CPU 缓存失效 | 传输完成后 |
dma_sync_single_for_cpu |
同步 DMA 缓冲区给 CPU 访问(缓存失效) | CPU 读取数据前 |
dma_sync_single_for_device |
同步 DMA 缓冲区给设备访问(缓存刷写) | DMA 启动前 |
dma_sync_single_for_cpu,导致读取到缓存中的旧数据。在 ARM 等弱一致性架构上,此问题尤为突出,可能引发数据损坏。
4.3.3 中断延迟与批量处理
每次 DMA 完成都触发中断会带来不可忽视的延迟开销。优化策略包括:
- 中断合并(Interrupt Coalescing):设置阈值,累积多个传输完成后再产生一次中断。
- 轮询模式:在高速场景下,CPU 主动轮询 DMA 状态寄存器,避免中断上下文切换开销。
- 多描述符批量提交:链表模式下,CPU 一次性提交多个描述符,减少寄存器编程次数。
4.4 本章小结
- DMA 控制器通过 SAR、DAR、CTR 等寄存器实现自主数据搬运,CPU 仅参与配置与中断处理。
- 单次模式适合低速外设,循环模式适合连续流,链表模式适合高速 scatter-gather 场景。
- CPU 与 DMA 的协同核心在于缓存一致性维护与中断延迟优化,Linux 内核提供
dma_alloc_coherent、dma_map_single等 API 保证数据正确性。 - 实际驱动开发中,应根据外设速率、数据量、实时性要求选择合适的 DMA 模式与同步策略。