DMA 基础概念:让数据自己跑起来

各位同学,咱们今天聊聊 DMA。说白了,DMA 就是「直接存储器访问」。我刚开始接触嵌入式时,总觉得这玩意儿挺神秘——数据怎么就能自己跑,不用 CPU 管呢?后来调了几个项目才明白,它其实就是个硬件搬运工。

DMA 工作原理:一个专职的搬运工

想象一下这个场景:你正在写代码,突然需要把 1MB 数据从外设搬到内存。如果没有 DMA,CPU 得一条条指令去读、去写,就像你亲自搬砖。有了 DMA,你只需要告诉搬运工「从哪搬、搬到哪、搬多少」,然后就可以干别的去了。

核心流程:

  1. CPU 配置 DMA 控制器:源地址、目标地址、传输长度、触发源
  2. DMA 接管总线控制权,开始搬运
  3. 每搬运一个单元,DMA 内部计数器减 1
  4. 计数器归零时,DMA 发出中断通知 CPU

嗯,这里要注意:DMA 不是万能的。它只能做「内存↔内存」「内存↔外设」「外设↔外设」这三种搬运。我在 STM32F4 项目上就踩过坑——想用 DMA 搬运两个外设之间的数据,结果发现其中一个外设不支持 DMA 请求。所以选型时一定要看数据手册。

DMA 数据传输流程 CPU DMA 控制器 内存 外设 ① 配置 ② 读数据 ③ 写数据 ④ 传输完成中断 💡 关键点: DMA 传输期间,CPU 可以继续执行指令(除非总线冲突)。这就是为什么 DMA 能大幅提升系统吞吐量。

DMA 与 CPU 数据传输对比:谁更香?

你可能会问:既然 DMA 这么好,那还要 CPU 搬数据干嘛?我个人的经验是:小数据量用 CPU,大数据量用 DMA。具体分水岭在哪?看下面这张表。

对比维度 CPU 直接传输 DMA 传输
CPU 占用率 100%(全程占用) 仅配置和中断处理时占用
传输速度 受限于 CPU 主频和指令周期 接近总线理论带宽
适用场景 小数据量(< 64 字节) 大数据量(> 256 字节)
代码复杂度 简单,直接读写寄存器 需要配置 DMA 通道、中断、回调
实时性影响 阻塞其他任务 非阻塞,可并行处理

我的经验:在 STM32H7 上用 DMA 搬运 1KB 数据到 DAC,CPU 占用率从 85% 降到 3%。但如果是 16 字节的配置数据,用 DMA 反而更慢——配置 DMA 通道的开销比直接搬还大。

DMA 传输模式:单次、突发、循环

这三种模式,说白了就是「搬一次」「搬一堆」「一直搬」。我最早做音频项目时,就因为选错了模式,导致声音断断续续。咱们一个一个说。

单次传输模式

最基础的模式。DMA 收到一次请求,搬一个单元,然后停下来等下一次请求。适合外设数据速率不固定的场景,比如按键扫描。

  • 每次传输后 DMA 释放总线
  • CPU 可以随时介入
  • 缺点:频繁仲裁总线,效率低

突发传输模式

一次请求,搬一批数据。比如一次请求搬 16 个字节。我习惯在 SPI 驱动里用这个模式——SPI 的 FIFO 深度是 8,我就设突发长度为 8,刚好填满 FIFO。

  • 减少总线仲裁次数
  • 提高总线利用率
  • 注意:突发长度不能超过 FIFO 深度

我曾经踩过的坑:在 LPC1768 上做 ADC 采集,突发长度设了 32,结果 ADC 采样率跟不上,数据全乱了。后来查手册才发现 ADC 的 FIFO 只有 16 级。突发长度一定要和外设 FIFO 匹配。

循环传输模式

数据搬完一圈,自动从头开始。适合环形缓冲区、音频流、连续采样等场景。配置时要注意:

  1. 源地址和目标地址都要设成循环模式
  2. 缓冲区大小必须是 2 的幂次(很多 DMA 控制器的硬件限制)
  3. 中断处理要快,否则会丢数据
// Rust 风格的 DMA 循环模式配置示例(伪代码)
let dma_config = DmaConfig::new()
    .transfer_mode(TransferMode::Circular)  // 循环模式
    .burst_size(BurstSize::Burst8)          // 突发长度 8
    .data_size(DataSize::HalfWord)          // 半字(16 位)
    .priority(DmaPriority::High);           // 高优先级

// 配置源地址和目标地址
dma_config.set_source_address(adc_buffer.as_ptr());
dma_config.set_destination_address(ram_buffer.as_mut_ptr());
dma_config.set_transfer_length(1024);       // 1024 个半字

三种模式怎么选?

我一般按这个原则来选:

场景 推荐模式 原因
按键/GPIO 中断 单次 事件不频繁,单次传输足够
SPI 批量数据 突发 匹配 FIFO 深度,提高效率
音频播放/录制 循环 需要连续不间断的数据流
ADC 连续采样 循环 + 双缓冲 避免数据覆盖,保证完整性