1、DMA基础原理:DMA是什么?为什么需要DMA?DMA的工作流程与传输模式
各位工程师朋友,咱们今天聊聊 DMA。这玩意儿在嵌入式系统里,说白了就是个“数据搬运工”。我记得刚入行时,总觉得它是个黑盒子,直到亲手调过几个坑之后,才真正摸透了它的脾气。
1.1 DMA是什么?——CPU 的“甩手掌柜”
DMA,全称 Direct Memory Access,直接存储器访问。它的核心能力就一条:在不需要 CPU 干预的情况下,自主完成数据在外设和存储器之间的搬运。
你想想看,如果没有 DMA,CPU 得亲自干这些活儿:从 SPI 接收寄存器里读一个字节,再写到内存缓冲区里,然后去干别的事……每次都得亲自跑一趟。这事儿要是频繁发生,CPU 就成专职搬运工了,哪还有精力处理算法、响应中断?
核心要点:DMA 是一个专用硬件模块,它接管了数据搬移的工作。CPU 只需告诉它“从哪里搬、搬到哪里、搬多少”,然后就可以去忙别的了。搬完了,DMA 会发个中断通知 CPU:“活干完了,你来处理吧。”
我在项目中遇到过一种情况:一个系统同时要处理 SPI 屏幕刷新、ADC 采样和串口通信。如果全靠 CPU 中断去搬数据,CPU 占用率直接飙到 80% 以上。后来引入 DMA,CPU 占用率一下降到 20%——系统顿时“清爽”了。
1.2 为什么需要DMA?——三个现实场景的答案
回答这个问题之前,咱们先想想:没有 DMA,系统会遇到什么麻烦?我总结了三类最典型的场景:
- 高频率数据搬移:SPI 以 10MHz 时钟传输,每 0.1μs 就得处理一个字节。CPU 如果每字节都去中断处理,基本上什么都干不了了。
- 大数据块传输:比如从 SD 卡读 1MB 数据到内存。CPU 一个一个字节搬,效率极低;DMA 批量搬运,能在后台悄悄完成。
- 实时性要求高:比如音频流,必须保证连续不间断。DMA 配合乒乓缓冲(double buffering),能让数据无缝衔接。
| 场景 | 无DMA时的CPU负担 | 有DMA后的CPU负担 |
|---|---|---|
| SPI 10MHz 连续接收 | CPU 100% 忙于中断处理 | CPU 仅需配置DMA,占用 <10% |
| 从SD卡拷贝1MB数据 | 逐字节搬运,CPU持续忙碌 | DMA后台搬运,CPU可并行处理 |
| 音频连续播放 | 中断频繁,容易丢数据 | DMA双缓冲,数据无缝衔接 |
小提示:DMA 不是万能的。它只适合那些“数据量大、频率高、但处理逻辑简单”的场景。如果每个字节都要做复杂计算(比如 CRC 校验),DMA 就帮不上忙了——CPU 还是得亲自动手。
1.3 DMA的工作流程——四步走,清晰明了
DMA 干活的流程其实不复杂。我习惯把它拆成四个步骤,方便记忆:
- 配置初始化:CPU 设置 DMA 的源地址、目标地址、传输宽度(8/16/32位)、传输数量。说白了,就是告诉 DMA“去哪儿搬、搬到哪儿、搬多少”。
- 启动传输:CPU 给 DMA 一个“开始”信号(通常通过写寄存器触发)。DMA 接到指令后,立刻开始工作。
- 数据搬移:DMA 接管总线,从源地址搬数据到目标地址。这个过程不需要 CPU 参与,DMA 自己管理地址递增和计数。
- 传输完成:当 DMA 搬完指定数量的数据后,它会触发一个完成中断。CPU 收到中断信号,就知道数据已经就位,可以开始处理了。
我更习惯用一幅图来理解这个过程——它把 CPU、DMA、外设和内存之间的协作关系展示得清清楚楚。
1.4 DMA的传输模式——三种模式,各有用处
DMA 不是只能干一种活。不同的场景需要不同的传输模式。我根据自己的项目经验,把最常用的三种模式梳理一下:
| 传输模式 | 工作方式 | 典型场景 | 我的评价 |
|---|---|---|---|
| 单次传输 | 配置一次,搬运一块数据,完成后停止 | SPI 一次读取、SD卡读一个扇区 | 简单直接,适合“一次性”任务 |
| 循环传输 | 搬运完数据后自动从头开始,周而复始 | 音频播放、ADC连续采样 | 省去重复配置,实时性极佳 |
| 链式传输 | 多组DMA配置链接起来,依次执行 | 复杂外设序列通信、多通道采集 | 灵活但配置复杂,适合高级场景 |
小提示:我个人最常用的是循环传输。比如做音频播放时,我把 DMA 配置成循环模式,配合乒乓缓冲(两个缓冲区交替使用),声音输出非常平稳,CPU 几乎不用操心。你想想看,这得省多少事?
注意!链式传输虽然灵活,但配置复杂度较高。我曾经在一个项目中,因为 DMA 链表的配置顺序搞反了,导致数据全部乱套。从那以后,我每次用链式模式都会画好流程图再编码。嗯,这里千万不能偷懒。
1.5 一个简单的初始化示例(伪代码)
实际验证一下。下面是一个典型的 SPI DMA 接收初始化伪代码。我习惯先把配置逻辑写出来,再去填寄存器细节:
// 1. 配置 SPI:使能 DMA 接收请求
SPI->CR2 |= SPI_CR2_RXDMAEN;
// 2. 配置 DMA 控制器
DMA->CPARx = (uint32_t)&SPI->DR; // 源地址:SPI数据寄存器
DMA->CMARx = (uint32_t)rx_buffer; // 目标地址:内存缓冲区
DMA->CNDTRx = BUFFER_SIZE; // 传输长度:缓冲区大小
// 3. 设置传输方向、宽度、循环模式等
DMA->CCRx |= DMA_CCR_MEM2MEM = 0; // 外设到内存
DMA->CCRx |= DMA_CCR_PSIZE_8 | // 外设数据宽度 8 位
DMA_CCR_MSIZE_8; // 内存数据宽度 8 位
DMA->CCRx |= DMA_CCR_CIRC; // 使能循环模式(可选)
// 4. 使能 DMA 传输完成中断
DMA->CCRx |= DMA_CCR_TCIE;
// 5. 使能 DMA 通道,开始工作
DMA->CCRx |= DMA_CCR_EN;
关键点:配置 DMA 时,源地址和目标地址的设定千万不能搞反。我有一次调试了一个下午,发现数据全写反了,后来一看,原来是寄存器填错了。吃一堑长一智,现在我先用纸笔把地址写在纸上,再填寄存器。
好了,关于 DMA 的基础原理就聊到这里。这一节的内容是整个课程的地基,后面我们再深入 DMA 与 SPI 时钟的匹配技巧时,你们会发现这些基础知识有多重要。记住一句话:DMA 是 CPU 的“得力助手”,不是“替代品”——它只干搬砖的苦活,聪明活儿还得 CPU 来。