1. DMA概述与工作原理
DMA,全称 Direct Memory Access——直接存储器访问。说白了,就是让数据从外设搬到内存,或者从内存搬到外设,不经过 CPU 的寄存器。你想想看,如果没有 DMA,CPU 就得一趟一趟地搬数据,干的全是苦力活儿。
我记得刚入行那会儿,做一个高速数据采集系统。ADC 每 1µs 产生一个数据,我用中断把数据读回来,再存到数组里。结果 CPU 的利用率直接飙到了 90%,系统几乎干不了别的。后来师傅跟我说:“试试 DMA。”——从此我对 DMA 有了刻骨铭心的认知。
1.1 为什么需要 DMA?
嵌入式系统里,外设(SPI、UART、ADC、Display)和内存之间需要频繁交换数据。传统方式有两种:
- 轮询(Polling):CPU 一直死等外设标志位,啥也干不了。
- 中断(Interrupt):每次传输都触发一次中断,CPU 保存现场、执行 ISR、恢复现场——开销很大。
对于 QSPI 这种高速接口(跑几十 MHz,甚至上百 MHz),中断的开销会严重拖慢吞吐量。DMA 的出现,就是让 CPU 从数据搬运工解放出来,专职做计算。
核心思想: DMA 控制器在总线矩阵中扮演“第二主人”角色。它掌握总线控制权后,可以自主完成数据块传输,传输完毕只发一个中断通知 CPU。
1.2 DMA 传输流程——保姆级拆解
一次典型的 DMA 传输,分这几个步骤:
- 配置通道:告诉 DMA 控制器,数据从哪读(源地址)、写到哪(目的地址)、传多少字节(计数)。
- 请求仲裁:外设(比如 QSPI 的 RX FIFO 非空)产生 DMA 请求,DMA 控制器向总线仲裁器请求总线控制权。
- 总线授权:CPU 让出总线(通常是在一个总线周期结束时),DMA 控制器获得总线控制权。
- 单次传输:DMA 从源地址读取一个字,写入目的地址,计数器减 1,地址增量(如果设置增量模式)。
- 循环直到完成:重复步骤 4,直到计数器归零。
- 传输结束中断:DMA 控制器拉高中断信号,通知 CPU:“我干完了,数据已经到位。”
我习惯在配置 DMA 时,把源和目的地址的宽度对齐。比如 QSPI 外设数据寄存器是 32 位的,那么源/目的地址也要按照 32 位边界对齐——否则某些 MCU 的 DMA 会触发“非对齐访问错误”。
1.3 DMA 在嵌入式系统中的真正作用
嗯,这里要分三点说透:
- 解放 CPU 算力:让 CPU 只算不搬,系统整体 MIPS(百万指令每秒)利用率大幅提升。以我上一款产品为例,DMA 搬运 QSPI Flash 的固件升级数据,CPU 利用率从 85% 降到了 12%。
- 降低响应延迟:中断响应时间一般几十个时钟周期,而 DMA 传输一个数据可能只需要 1~2 个总线周期,而且不打扰 CPU 流水线。
- 支持背靠背传输:DMA 可以连续搬运几百 KB 数据,外设 FIFO 永远不会溢出或欠载。我曾经遇到过用中断搬运大数据时,因为中断嵌套导致 FIFO 溢出丢数据——换 DMA 后再也没出过问题。
注意:DMA 不是万能的。如果每次传输的数据块很小(比如只有几个字节),配置 DMA 的开销反而比中断大。我一般在数据块 ≥ 16 字节时才启用 DMA。
为了直观展示 DMA 的工作流,我画了一张图——
从图中能看出来,DMA 控制器通过总线仲裁器与 CPU 共用同一根系统总线。但真正传输的时候,CPU 会暂时让出总线(或者通过分时共享),所以 DMA 传输期间 CPU 不能访问总线——不过 CPU 可以继续执行内部流水线、计算、访问 Cache,所以整体效率很高。
1.4 传统方式 vs DMA 方式
我做过一个对比测试:同样的 QSPI Flash 读取 4KB 数据。结果如下——
| 传输方式 | CPU 占用率 | 总时间(@80MHz) | 数据吞吐率 |
|---|---|---|---|
| 中断(每字节一次中断) | 87% | 120 µs | 33 MB/s |
| DMA(单块传输) | 6% | 52 µs | 77 MB/s |
| 轮询(while等待) | 97% | 48 µs | 83 MB/s |
轮询虽然很快,但 CPU 占满了。DMA 在吞吐率接近轮询的前提下,CPU 占用率几乎可以忽略——这也是为什么在实时性要求高的系统里(比如无人机飞控、电机控制),DMA 几乎是标配。
我踩过的一个坑:配置 DMA 传输长度时,计数器的单位取决于硬件设计。有的 DMA 按字节计数,有的按字计数,还有的按节拍计数。我当初没仔细看手册,把长度算错了,导致 QSPI 读出来的数据全是乱序——排查了一天。所以建议你每次配置 DMA 时,专门写一个注释记录计数单位。
1.5 本章小贴士
- 链式 DMA:某些高级 DMA 支持描述符链表,可以连续搬运多块不同大小的数据,CPU 只需配置一次。我常用在 QSPI 驱动的“内存映射模式”下做背景刷新。
- 双缓冲:设置两个 DMA 缓冲区,一个给传输用,另一个给 CPU 处理用。传输完自动切换。这种方法能彻底优化流水线——我在做音频播放时用过,效果立竿见影。
- 中断聚合:如果外设产生中断的频率很高,可以通过 DMA 的“传输完成中断”汇总一次通知,而不是每字节都中断。