DMA 基础概念:让数据自己跑起来
各位同学,咱们今天聊聊 DMA。说白了,DMA 就是「直接存储器访问」。我刚开始接触嵌入式时,总觉得这玩意儿挺神秘——数据怎么就能自己跑,不用 CPU 管呢?后来调了几个项目才明白,它其实就是个硬件搬运工。
DMA 工作原理:一个专职的搬运工
想象一下这个场景:你正在写代码,突然需要把 1MB 数据从外设搬到内存。如果没有 DMA,CPU 得一条条指令去读、去写,就像你亲自搬砖。有了 DMA,你只需要告诉搬运工「从哪搬、搬到哪、搬多少」,然后就可以干别的去了。
核心流程:
- CPU 配置 DMA 控制器:源地址、目标地址、传输长度、触发源
- DMA 接管总线控制权,开始搬运
- 每搬运一个单元,DMA 内部计数器减 1
- 计数器归零时,DMA 发出中断通知 CPU
嗯,这里要注意:DMA 不是万能的。它只能做「内存↔内存」「内存↔外设」「外设↔外设」这三种搬运。我在 STM32F4 项目上就踩过坑——想用 DMA 搬运两个外设之间的数据,结果发现其中一个外设不支持 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 匹配。
循环传输模式
数据搬完一圈,自动从头开始。适合环形缓冲区、音频流、连续采样等场景。配置时要注意:
- 源地址和目标地址都要设成循环模式
- 缓冲区大小必须是 2 的幂次(很多 DMA 控制器的硬件限制)
- 中断处理要快,否则会丢数据
// 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 连续采样 | 循环 + 双缓冲 | 避免数据覆盖,保证完整性 |