4. DMA 基础概念:DMA 控制器工作原理、传输模式与 CPU 协同

直接存储器访问(Direct Memory Access, DMA)是嵌入式与高性能系统中绕开 CPU 参与数据搬运的核心硬件机制。在 Linux 内核驱动中,DMA 是实现零拷贝、降低 I/O 延迟的基石。本节从控制器内部结构出发,逐步拆解三种传输模式,并厘清 DMA 与 CPU 之间的握手与竞争关系。

核心观点:DMA 的本质是让外设与内存之间建立一条“硬件直通车道”,CPU 只需在起点和终点做配置与中断处理,中间的数据搬运完全由 DMA 控制器自主完成。

4.1 DMA 控制器工作原理

DMA 控制器(DMAC)是一个独立的总线主控设备,能够发起总线事务,直接读写系统内存或外设 FIFO。其内部关键单元包括:

  • 控制寄存器组:存放传输方向(内存→外设 / 外设→内存 / 内存→内存)、地址递增模式、传输宽度(字节/半字/字)等配置。
  • 源地址寄存器(SAR):当前读取数据的源地址。
  • 目的地址寄存器(DAR):当前写入数据的目的地址。
  • 传输计数寄存器(CTR):剩余待传输的数据量,每完成一次总线传输自动递减。
  • 状态与中断逻辑:检测传输完成、错误、超时等事件,并向 CPU 发出中断请求。

一次典型的 DMA 传输流程如下:

  1. CPU 通过寄存器编程设置 SAR、DAR、CTR、传输方向与触发源。
  2. CPU 使能 DMA 通道,DMAC 进入等待触发状态。
  3. 外设(如 UART、SPI、网卡)发出 DMA 请求信号(DREQ)。
  4. DMAC 获得总线控制权,执行单次或突发传输,更新地址与计数。
  5. CTR 归零后,DMAC 产生传输完成中断,通知 CPU 处理数据。
DMA 控制器内部结构框图 DMA 控制器内部结构 系统总线接口 控制寄存器组 源地址寄存器 (SAR) 目的地址寄存器 (DAR) 传输计数 (CTR) 状态与中断逻辑 通道仲裁器 外设请求接口 (DREQ) 说明:CPU 通过总线接口配置寄存器组,外设通过 DREQ 触发传输,仲裁器管理多通道优先级。

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 在后台处理已填满的半区。

性能提示:循环传输模式下,CPU 与 DMA 共享环形缓冲区时,必须使用读/写指针同步机制(如 Linux 内核的 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 */
    /* ... 其他回调与状态字段 */
};
三种 DMA 传输模式对比 三种 DMA 传输模式对比 单次传输:CPU 配置 → 传输 → 中断 CTR 归零 → 中断 CPU 循环传输:自动重载 CTR 地址回绕,持续传输 链表传输:硬件自动跳转 描述符 1 → 描述符 2 → ... 模式选择建议 • 单次传输:适合数据量小(< 1 KB)、外设速率低(如 UART 115200 bps) • 循环传输:适合连续流数据(音频 48 kHz、ADC 1 MSPS),CPU 负载极低 • 链表传输:适合高速、非连续物理内存(网卡、NVMe),支持 scatter-gather • 实际 SoC 通常支持多种模式混合使用,同一 DMA 控制器可配置不同通道为不同模式

4.3 DMA 与 CPU 的协同

DMA 虽然接管了数据搬运,但 CPU 仍承担配置、同步与异常处理职责。两者之间的协同机制直接影响系统吞吐量与延迟。

4.3.1 协同工作流程

  1. 配置阶段(CPU 主导):CPU 分配 DMA 缓冲区、设置传输参数、注册完成回调。
  2. 启动阶段(CPU 主导):CPU 写使能位,DMAC 进入就绪状态。
  3. 传输阶段(DMA 主导):DMAC 自主搬运数据,CPU 可执行其他任务或进入低功耗模式。
  4. 同步阶段(CPU 主导):DMAC 通过中断或轮询状态寄存器通知 CPU 传输完成。
  5. 收尾阶段(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 传输完成后直接访问缓冲区,未调用 dma_sync_single_for_cpu,导致读取到缓存中的旧数据。在 ARM 等弱一致性架构上,此问题尤为突出,可能引发数据损坏。

4.3.3 中断延迟与批量处理

每次 DMA 完成都触发中断会带来不可忽视的延迟开销。优化策略包括:

  • 中断合并(Interrupt Coalescing):设置阈值,累积多个传输完成后再产生一次中断。
  • 轮询模式:在高速场景下,CPU 主动轮询 DMA 状态寄存器,避免中断上下文切换开销。
  • 多描述符批量提交:链表模式下,CPU 一次性提交多个描述符,减少寄存器编程次数。
DMA 与 CPU 协同时序 DMA 与 CPU 协同时序 CPU DMA 外设 配置 DMA 执行其他任务 中断处理 DMA 传输数据 完成 DREQ 请求 说明:CPU 配置后即可执行其他任务,DMA 自主完成传输后通过中断通知 CPU,实现并行工作。

4.4 本章小结

  • DMA 控制器通过 SAR、DAR、CTR 等寄存器实现自主数据搬运,CPU 仅参与配置与中断处理。
  • 单次模式适合低速外设,循环模式适合连续流,链表模式适合高速 scatter-gather 场景。
  • CPU 与 DMA 的协同核心在于缓存一致性维护与中断延迟优化,Linux 内核提供 dma_alloc_coherent、dma_map_single 等 API 保证数据正确性。
  • 实际驱动开发中,应根据外设速率、数据量、实时性要求选择合适的 DMA 模式与同步策略。