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 干活的流程其实不复杂。我习惯把它拆成四个步骤,方便记忆:

  1. 配置初始化:CPU 设置 DMA 的源地址、目标地址、传输宽度(8/16/32位)、传输数量。说白了,就是告诉 DMA“去哪儿搬、搬到哪儿、搬多少”。
  2. 启动传输:CPU 给 DMA 一个“开始”信号(通常通过写寄存器触发)。DMA 接到指令后,立刻开始工作。
  3. 数据搬移:DMA 接管总线,从源地址搬数据到目标地址。这个过程不需要 CPU 参与,DMA 自己管理地址递增和计数。
  4. 传输完成:当 DMA 搬完指定数量的数据后,它会触发一个完成中断。CPU 收到中断信号,就知道数据已经就位,可以开始处理了。

我更习惯用一幅图来理解这个过程——它把 CPU、DMA、外设和内存之间的协作关系展示得清清楚楚。

DMA 一次完整传输流程 ① 配置DMA ② 启动传输 ③ 数据搬移 ④ 传输完成 CPU(主控) 配置并启动DMA DMA控制器 自主搬运数据 外设(SPI) 数据源 内部内存 数据目的地 配置命令 读请求 数据流向 关键特征 • CPU 只参与配置和收尾 • DMA 独占总线进行搬移 • 传输完成触发中断通知CPU

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 来。