1. DMA 基础概念

DMA(Direct Memory Access,直接存储器访问)是嵌入式系统中一项关键的硬件机制,允许外设与存储器之间直接交换数据,而无需 CPU 逐字节或逐字地参与搬运。理解 DMA 的定义、与 CPU 搬运的差异以及适用场景,是掌握后续 Cache 一致性与性能优化的前提。

1.1 DMA 定义与核心原理

DMA 是一种由硬件控制器(DMA Controller)管理的总线主控传输方式。CPU 只需配置传输的源地址、目的地址、数据长度和触发条件,随后 DMA 控制器独立占用总线完成数据移动,传输完成后通过中断通知 CPU。

核心特征: DMA 是“CPU 授权、硬件执行”的数据搬运模式,CPU 仅在传输开始和结束时介入,中间过程可并行执行其他任务。
DMA 系统架构与数据流 CPU DMA 控制器 系统总线(System Bus) 存储器(DDR/SRAM) 外设(UART/SPI/ETH) 高速外设(GPU/Camera) DMA 控制器通过系统总线直接连接存储器和外设,CPU 仅参与配置与中断响应

1.2 DMA 与 CPU 数据搬运对比

传统 CPU 搬运数据采用 PIO(Programmed I/O)模式,CPU 逐字节或逐字从源读取再写入目的,完全占用 CPU 核心。DMA 搬运则将这一重复性工作卸载给专用硬件。下表从多个维度对比两种方式:

对比维度 CPU 搬运(PIO) DMA 搬运
CPU 占用率 100% 持续占用,无法执行其他任务 仅配置阶段占用,传输过程 CPU 可并行运行
传输速率 受限于 CPU 主频与指令吞吐,通常较低 接近总线带宽,适合高速批量传输
软件复杂度 简单,直接读写寄存器或内存 需配置描述符链、处理中断、管理 Cache 一致性
适用数据量 小数据量(< 64 字节) 大数据量(≥ 64 字节,典型 1KB+)
实时性影响 阻塞 CPU,影响中断响应延迟 非阻塞,CPU 可响应高优先级中断
功耗 CPU 高频运行,功耗较高 DMA 硬件低功耗运行,CPU 可进入低功耗模式
工程经验: 在 Linux 内核中,对于小于 64 字节的寄存器级数据操作,PIO 模式反而更优,因为 DMA 描述符构建和中断处理的开销会超过直接搬运的收益。阈值通常设定在 256 字节以上才启用 DMA。

1.3 DMA 传输优势与适用场景

DMA 的核心优势在于卸载 CPU 负载和提升数据吞吐量。在以下场景中,DMA 是必不可少的优化手段:

  • 高速网络数据收发: 以太网、WiFi、5G 基带等接口,数据包到达速率极高,CPU 无法逐包搬运,必须依赖 DMA 将数据直接送入内存缓冲区。
  • 存储设备读写: eMMC、UFS、NVMe SSD 等块设备,每次读写涉及 4KB~1MB 的数据块,DMA 可实现与存储控制器的高速批量传输。
  • 多媒体数据流: 摄像头传感器采集的原始图像数据、音频编解码器的 PCM 数据流,需要持续、低延迟地搬运到内存或显示控制器。
  • 外设数据采集: ADC 采样、SPI 传感器、UART 高速串口等,DMA 可自动将采集数据存入环形缓冲区,CPU 只需周期性处理。
  • GPU/加速器数据传输: 深度学习推理、图像处理等场景中,CPU 通过 DMA 将输入数据批量传输到 GPU 显存或 NPU 本地内存。
DMA 典型适用场景分类 DMA 传输引擎 高速网络 存储设备 多媒体流 外设采集 适用原则:数据量大(≥256B)、传输频繁、CPU 需要并行处理其他任务时,优先选择 DMA 不适用场景:小数据量、寄存器级控制、传输间隔极长(秒级)的场合,PIO 更简单高效

1.4 DMA 传输的基本流程

一次典型的 DMA 传输包含以下步骤,以 Linux 内核中的 DMA Engine API 为例:

  1. 申请 DMA 通道: 调用 dma_request_channel() 获取可用的 DMA 控制器通道。
  2. 配置传输参数: 设置源地址、目的地址、传输宽度(8/16/32/64 位)、突发长度(burst size)和传输方向(内存→外设、外设→内存、内存→内存)。
  3. 提交描述符: 构建 DMA 描述符(descriptor),描述一次或多次传输的详细信息,支持链式传输(scatter-gather)。
  4. 启动传输: 调用 dmaengine_submit() 和 dma_async_issue_pending() 启动 DMA 硬件。
  5. 等待完成: 通过轮询或中断回调函数 callback() 获取传输完成状态。
  6. 释放通道: 传输结束后调用 dma_release_channel() 归还资源。
// Linux DMA Engine 典型调用序列(伪代码)
struct dma_chan *chan;
struct dma_async_tx_descriptor *tx;
dma_cookie_t cookie;

// 1. 申请 DMA 通道
chan = dma_request_channel(mask, filter_fn, NULL);

// 2. 准备传输描述符
tx = dmaengine_prep_slave_sg(chan, sgl, sg_len, direction, flags);

// 3. 注册完成回调
tx->callback = my_dma_callback;
tx->callback_param = &my_data;

// 4. 提交并启动
cookie = dmaengine_submit(tx);
dma_async_issue_pending(chan);

// 5. 等待完成(或在回调中处理)
wait_for_completion(&my_data.done);

// 6. 释放通道
dma_release_channel(chan);
常见陷阱: DMA 传输完成后,CPU 访问的数据可能仍在 Cache 中未写回,或者 DMA 写入的数据尚未被 CPU Cache 感知。这会导致 Cache 一致性问题,是后续章节的核心调试内容。在配置 DMA 前,务必对缓冲区执行 dma_map_single() 或 dma_sync_single_for_device() 等一致性操作。

1.5 DMA 与 Cache 一致性的初步关联

DMA 直接访问物理内存,而现代 CPU 使用 Cache 缓存数据。当 DMA 向内存写入数据时,CPU 的 Cache 中可能仍保留着该地址的旧数据(Cache Stale);反之,当 CPU 修改了 Cache 中的数据但尚未写回内存时,DMA 读取到的将是过时数据(Cache Bypass)。

这两种情况统称为 Cache 一致性问题,是 DMA 工程实践中必须解决的核心挑战。后续章节将深入探讨一致性协议、DMA 缓冲区管理策略以及性能调优方法。

DMA 与 Cache 一致性问题示意 CPU + Cache DMA 控制器 物理内存 Cache 写回 DMA 直接访问 问题:CPU 修改 Cache 后未写回,DMA 读取到旧数据(Stale Read) 问题:DMA 写入新数据到内存,CPU Cache 仍保留旧数据(Stale Cache)