1. DMA 基础概念与工作原理
咱们这门课,上来就得聊清楚一个东西——DMA。
DMA,全称 Direct Memory Access。说白了,就是给 CPU 找了个专职的数据搬运工。
我刚开始做嵌入式的时候,老觉得这玩意儿可有可无。直到有一次做高速数据采集,CPU 被中断折腾得够呛,我才意识到:没有 DMA,很多活儿根本干不了。
1.1 什么是 DMA?
CPU 平常忙着跑操作系统、做算法、响应中断。但数据搬运这种体力活——把外设的数据拷到内存,或者从内存搬到外设——如果都让 CPU 一手包办,那它就没空干正事了。
DMA 就是专门干这个的。
举个例子:ADC 采样完 1024 个点,如果不走 DMA,CPU 得一个一个字节去读。如果走 DMA,CPU 只需要告诉 DMA:「嘿,帮我把这批数据搬到内存地址 0x20001000,搬完通知我。」然后 CPU 就可以去忙别的事了。
你琢磨琢磨,效率差出多少。
我的习惯:凡是外设数据传输量超过 16 字节,我基本都会考虑 DMA。尤其是定时采样、音频流、视频帧这类实时性要求高的场景,没有 DMA 基本跑不动。
1.2 DMA 的工作原理
那 DMA 到底是怎么干活的?我画个流程图帮大家理解。
1.3 关键角色
一个完整的 DMA 传输,有四个关键角色。
| 角色 | 干什么的 | 我踩过的坑 |
|---|---|---|
| DMA 控制器 | 传输过程的「大脑」,负责仲裁、地址生成、传输控制 | 不同芯片的 DMA 控制器差异很大,移植代码时别想当然 |
| 源地址寄存器 | 存放数据来源的地址。可以是内存地址,也可以是外设地址 | 外设地址通常是固定的,但内存地址别忘了设置自增模式 |
| 目的地址寄存器 | 数据要送到哪里去 | 目的和源的宽度要匹配,否则数据会错位 |
| 传输计数寄存器 | 记录还要搬多少数据。每次传输完一个单元,自动减 1 | 有些芯片计数到 0 会自动 reload,有些不会 —— 别搞混 |
| 控制寄存器 | 配置模式、优先级、中断使能等 | 优先级设得太高,可能把 CPU 总线访问饿死,我吃过这个亏 |
1.4 DMA 传输的几种模式
不同场景下,DMA 的工作模式也不一样。我总结一下常见的三种:
- 单次传输(Single Transfer)
每次 DMA 请求只传输一个数据单元。适合数据量小、间隔不固定的场景。比如按键扫描、外部中断触发。 - 块传输(Block Transfer)
一次请求传输一整块数据,直到计数完毕才释放总线。适合大数据量连续传输,比如文件读写、图像数据搬运。 - 请求传输(Demand Transfer)
外设通过信号持续请求,DMA 就一直传输,直到外设撤销请求或计数完毕。适合 FIFO 型外设,比如 UART 接收。
避坑指南:我之前在一个项目中,用块传输做 SPI Flash 读取。结果外设还没准备好数据,DMA 就一股脑开始搬了,读到一堆乱码。后来改成请求传输模式,加上外设的「数据就绪」信号握手,问题就解决了。
1.5 几种常见的 DMA 架构
不同芯片厂商的 DMA 实现不太一样。我挑三个有代表性的说说:
| 架构 | 特点 | 典型芯片 |
|---|---|---|
| 简单 DMA | 只有一个 DMA 通道,每次只能处理一个传输任务。资源简单,但效率有限 | 早期 ARM7、8051 扩展 DMA |
| 多通道 DMA | 多个通道并行工作。每个通道有独立的源/目的/计数配置。这是主流方案 | STM32 系列 (DMA1/DMA2) |
| 链表 DMA(Scatter-Gather) | 用链表描述多个不连续的内存块。DMA 自动按链表顺序完成所有传输。特别适合网络包处理 | i.MX RT 系列、Xilinx Zynq |
你想想看,如果你的应用要同时从 ADC 采数据、往 DAC 送波形、还要和 PC 通过 USB 通信——那单通道 DMA 根本不够用。这时候就得选多通道甚至链表 DMA。
1.6 传输数据宽度与对齐
这里有个细节很多人容易忽视:数据宽度。
DMA 一次传一个字节(8 位)、两个字节(16 位)、还是四个字节(32 位)?
- 源宽度:外设数据寄存器的位宽。比如 UART 一般是 8 位,SPI 可能是 8/16 位。
- 目的宽度:内存地址的位宽。一般是 32 位。
- 对齐问题:如果源是 8 位、目的是 32 位,DMA 控制器会怎么处理?有的自动打包,有的会报错。
我的建议:除非特别清楚芯片的行为,否则尽量让源宽和目的宽保持一致。不一致时,先看芯片手册里对「字节重组」的描述。我遇到过一款芯片,8 位到 32 位传输时,数据放到了错误的内存偏移,排查了一整天。
1.7 中断与回调
DMA 传输完成后,通常会触发中断。
CPU 在中断服务程序里完成后续操作:比如处理数据、开启下一轮传输、或者唤醒等待的任务。
写 ISR 的时候有个原则:能干的活尽量少。
举个例子:DMA 搬完 1024 个采样点。ISR 里就干一件事——设置一个标志位。真正的数据处理放到主循环或任务里去做。这样能减少中断延迟,也避免在 ISR 里调耗时函数。
// 伪代码示例:DMA 传输完成中断
void DMA_IRQHandler(void) {
if (DMA_GetITStatus(DMA1_FLAG_TC)) {
// 1. 清除中断标志
DMA_ClearITPendingBit(DMA1_FLAG_TC);
// 2. 设置标志位(别在这做复杂处理)
dma_transfer_done = 1;
// 3. 如果有环形缓冲,可以重新加载配置
// DMA_Config(...);
}
}
// 主循环中处理数据
void main_loop() {
while (1) {
if (dma_transfer_done) {
dma_transfer_done = 0;
process_data(buffer, 1024); // 真正的处理放在这里
}
// ... 其他任务
}
}
这套路我用在好几个项目里,从来没出过问题。
1.8 本章小结
DMA 其实不复杂。就是让专门的硬件去搬数据,CPU 腾出手来干正事。
但实际项目中,每款芯片的 DMA 轮子都不太一样。踩过几次坑,你就知道关键在哪里了:地址配置、宽度匹配、优先级、中断处理——把这几个点抓住,DMA 就能用得顺溜。