DMA 基础概念:它到底在解决什么问题?
各位同学,咱们今天聊 DMA。很多初学者觉得 DMA 是个黑盒子,配置完就不管了。其实没那么玄乎。DMA 的全称是 Direct Memory Access,直接存储器访问。说白了,就是让数据在内存和外设之间自己搬,不用 CPU 一趟一趟跑腿。
我刚开始做嵌入式那会儿,有个项目要用 ADC 采集音频数据,CPU 每采一个点就得进一次中断,把数据从 ADC 数据寄存器搬到内存数组里。采样率一高,CPU 几乎啥也干不了,全在搬数据。后来换成 DMA,CPU 彻底解放了,该干啥干啥。嗯,这就是 DMA 的核心价值——把 CPU 从重复的数据搬运中解放出来。
DMA 工作原理:谁在干活?
DMA 传输的本质,是 DMA 控制器接管了总线控制权。CPU 把传输任务交代给 DMA 控制器,然后 DMA 控制器自己去完成数据搬运,搬运完了再通知 CPU。
我习惯把 DMA 传输拆成三个角色:
- 源地址:数据从哪来。可以是外设的数据寄存器,也可以是内存数组。
- 目的地址:数据搬到哪去。可以是内存,也可以是外设的发送寄存器。
- 传输长度:搬多少数据。单位可以是字节、半字或字。
举个例子,你要把 ADC 采集的 1024 个数据搬到内存数组 adc_buffer[1024] 里。传统做法是 CPU 每次进中断读一个数据,循环 1024 次。用 DMA 呢?你只需要配置好源地址(ADC->DR)、目的地址(adc_buffer)、传输长度(1024),然后启动 DMA。剩下的活,DMA 控制器自己干。
关键点:DMA 传输期间,CPU 可以继续执行其他代码,或者进入睡眠模式降低功耗。这是低功耗设计的核心手段之一。
我曾经在一个电池供电的传感器节点上,用 DMA + 睡眠模式把系统功耗从 5mA 降到了 200μA。怎么做到的?ADC 采集用 DMA 自动搬运,CPU 全程睡觉,等 DMA 传输完成中断再唤醒 CPU 处理数据。
DMA 传输模式:三种常见玩法
STM32 的 DMA 支持几种传输模式,我挑最常用的三种讲:
| 传输模式 | 特点 | 典型应用场景 |
|---|---|---|
| 单次传输 | 传输一次就停,需要重新配置才能再次启动 | 一次性数据搬移,比如把数据从 Flash 搬到 RAM |
| 循环传输 | 传输完成后自动从头开始,无限循环 | ADC 连续采样、音频数据流、串口连续收发 |
| 突发传输 | 一次请求传输多个数据单元(突发长度可配) | 高速外设(如 SDIO、FSMC)批量数据传输 |
单次传输最简单。配置好源、目的、长度,启动,传输完成就停。我早期做的一个项目,需要把传感器校准参数从 Flash 搬到 RAM 里加速访问,用的就是单次传输。一次搞定,不用操心。
循环传输我用的最多。比如做音频播放,DMA 循环把音频数据从内存搬到 DAC,CPU 只需要偶尔更新一下缓冲区内容。你想想看,如果没有循环模式,每次传输完都得重新配置 DMA,那多麻烦。
突发传输稍微复杂一点。它允许 DMA 在一次总线仲裁中获得多个传输周期的权限,连续搬好几个数据。适合高速外设,但要注意总线冲突。我记得有个项目用 SDIO 读写 SD 卡,突发传输没配好,导致 DMA 和 CPU 抢总线,系统卡顿。后来把突发长度调小,问题解决了。
个人建议:初学者先从单次传输入手,跑通了再试循环模式。突发传输等你对 DMA 比较熟悉了再碰,不然踩坑了不好排查。
DMA 请求与响应机制:谁叫谁?
DMA 不是自己主动干活的,它需要被触发。触发源就是外设的 DMA 请求信号。
流程是这样的:
- 外设(比如 USART、ADC、SPI)准备好数据了,或者需要数据了,就向 DMA 控制器发一个请求信号。
- DMA 控制器收到请求,检查当前通道是否空闲、优先级是否够。
- 如果条件满足,DMA 控制器接管总线,执行一次数据传输。
- 传输完成后,DMA 控制器释放总线,等待下一个请求。
这里有个容易混淆的点:DMA 请求和 DMA 中断是两码事。请求是外设发给 DMA 控制器的,告诉它“有活干了”。中断是 DMA 控制器发给 CPU 的,告诉它“活干完了”。
避坑指南:我曾经在一个项目里,把 DMA 请求和 DMA 中断搞混了。配置 USART 的 DMA 接收时,忘了使能 USART 的 DMA 请求,结果数据死活不往内存里搬。查了两天才发现,是 USART 的 DMA 发送/接收使能位没置上。嗯,这种低级错误,犯过一次就记住了。
STM32 的 DMA 控制器有多个通道,每个通道可以响应多个外设的请求。但同一时间,一个通道只能服务一个外设。你需要通过配置寄存器,把外设的 DMA 请求映射到对应的通道上。
DMA 传输的核心流程
我把 DMA 一次完整传输的流程画成了 SVG 图,方便你理解:
DMA 传输配置的关键参数
配置 DMA 时,有几个参数必须搞清楚。我列了个表:
| 参数 | 说明 | 常见取值 |
|---|---|---|
| 源地址 | 数据来源的地址 | 外设寄存器地址、内存数组地址 |
| 目的地址 | 数据要去的地址 | 内存数组地址、外设寄存器地址 |
| 传输长度 | 要传输的数据单元个数 | 1 ~ 65535 |
| 数据宽度 | 每次传输的数据大小 | 字节(8bit)、半字(16bit)、字(32bit) |
| 地址增量 | 每次传输后地址是否自动增加 | 使能/禁止 |
| 传输模式 | 单次、循环、突发 | 根据场景选择 |
| 优先级 | 多个 DMA 通道同时请求时的仲裁 | 低/中/高/最高 |
经验之谈:配置地址增量时要注意。如果源地址是外设数据寄存器(固定地址),源地址增量要禁止。如果目的地址是内存数组(需要递增),目的地址增量要使能。反过来也一样。我见过有人把这两个搞反了,数据全写到了同一个地址上。
DMA 与 CPU 的协作关系
DMA 不是取代 CPU,而是帮 CPU 分担重复劳动。CPU 负责决策和控制,DMA 负责数据搬运。两者通过中断和标志位来同步。
典型的协作流程:
- CPU 配置 DMA 参数,启动传输。
- CPU 继续执行其他代码,或者进入低功耗模式。
- DMA 完成传输后,触发中断。
- CPU 响应中断,处理搬运完的数据。
这种模式在低功耗设计中特别有用。CPU 大部分时间在睡觉,DMA 默默干活。等数据准备好了,再叫醒 CPU 处理。功耗能降一个数量级。