1. DMA 基础概念:DMA 工作原理,为何需要 DMA,DMA 与 CPU 数据搬移对比

1.1 什么是 DMA?我为什么离不开它?

DMA,全称 Direct Memory Access,翻译过来就是“直接存储器访问”。

说白了,它是一个硬件模块,能在没有 CPU 干预的情况下,自己把数据从一个地方搬到另一个地方。CPU 只需要告诉它:“嘿,帮我把这 1024 字节从外设 FIFO 搬到内存地址 0x8000_0000 去,搬完了告诉我一声。”然后 CPU 就可以转头去干其他活了。

我当年刚接触嵌入式时,总觉得 CPU 自己搬数据也挺快,干嘛非要加个 DMA 这么麻烦?直到我做第一个高速数据采集项目,CPU 一边要处理中断,一边要搬运 ADC 数据,还要跑控制算法,结果系统直接卡死……那次之后我才真正体会到:DMA 不是可选项,是高性能系统的必需品。

核心思想:DMA 本质是一个专用数据搬运引擎,专门为“从 A 拷贝到 B”这个操作而生,效率远超通用处理器。

1.2 DMA 工作原理——三个角色一台戏

一个典型的 DMA 传输过程,需要三方协作:

  1. 发起者(CPU 或外设):告诉 DMA 控制器“做什么”。
  2. 执行者(DMA 控制器):接管总线,按配置参数搬运数据。
  3. 目标(内存/外设寄存器):数据的来源和去向。

具体流程是这样的:

  • CPU 先把传输参数(源地址、目的地址、数据长度、传输方向等)写入 DMA 控制器的寄存器。
  • DMA 控制器请求总线控制权(或通过总线矩阵仲裁获取)。
  • 一旦获准,DMA 直接在源和目标之间传输数据,每次搬完一个单元(字节/半字/字),地址自动递增(如果配置了递增)或保持不变。
  • 传输完成后,DMA 向 CPU 发送中断信号,或者设置一个状态位供 CPU 轮询。

我习惯把 DMA 比作一个“勤快的快递小哥”:CPU 是老板,只管写地址单;DMA 按单子一件一件搬;搬完了回来敲门汇报。老板不用自己跑腿,效率一下就上来了。

1.3 为什么需要 DMA?——三个真实场景

你可能会问:“CPU 自己循环搬运不也行吗?”嗯,行是行,但代价太高了。我们来看几个实际场景。

场景 数据流量 CPU 自己搬会怎样? DMA 搬会怎样?
UART 接收不定长数据 ~115200 bps 每接收一个字节触发一次中断,CPU 频繁进出中断,任务响应延迟变大。 DMA 自动将数据存入环形缓冲区,CPU 只在收到特定结束字符或缓冲区满时才处理。
摄像头帧采集(VSYNC 触发) 640×480×2 字节 ≈ 600 KB/帧 CPU 循环读取 FIFO,耗时占满整个帧间隔,无法并行做图像预处理。 DMA 在后台搬完一整帧,CPU 同时做前一帧的处理,流水线效率倍增。
高速 DAC 波形输出(定时器触发) 1 MSPS CPU 每 1μs 置一次数据寄存器,无法执行任何其他代码。 DMA 从预存波形表自动填充 DAC,CPU 零等待,还能响应上层命令。

总结一下:DMA 的核心价值在于解放 CPU,让最宝贵的计算资源去干它最擅长的事——逻辑判断、控制、算法,而不是机械的字节搬运。

我曾经踩过的坑:有一次我用 CPU 循环搬运 SPI 数据,波特率 10 Mbps,CPU 主频 72 MHz。看起来每字节搬一次也就 10 个时钟周期?不对!加上循环开销、中断处理、栈操作,实际占用超过 40% 的 CPU 带宽。换成 DMA 后,CPU 占用直接降到 2% 以下。你说值不值?

1.4 DMA vs CPU 数据搬移——性能对比表

下面这个表格是我做项目时经常用来跟团队沟通的,也是面试时喜欢问的问题。

对比维度 CPU 手动搬移 DMA 自动搬移
CPU 占用率 100%(保持时间内) 仅配置和中断时占用(通常 <1%)
传输速率 受限于内核指令取指/译码/执行流水线 专为搬运设计,通常 1~2 时钟/字
并发能力 无法并发,需轮询或中断嵌套 可与 CPU 并行(若总线冲突可配置优先级)
灵活性 可对数据做任意处理后再写入 纯粹的搬运,不支持算术运算(个别支持链路的 DMA 可做简单拼接)
代码复杂度 简单的循环,但中断管理可能复杂 需要理解 DMA 描述符、回环、中断处理,初期学习曲线陡峭
适用场景 少量数据、随机字节、需要边读边算 大量数据、周期性流、外设与内存频繁交互

看到这里你应该明白了:DMA 不是替代 CPU,而是给 CPU 当专职搬运工。两者配合使用,才能让系统效率最大化。

1.5 一张图看懂 DMA 核心结构

下面这张 SVG 图把 DMA 传输的基本框架画出来了。你看着它,配合上面的文字,就能把整个流程串起来。

DMA 传输核心结构示意图 CPU(发起者) 配置 DMA 参数 DMA 控制器 执行数据搬运 总线仲裁器 内存(目标) SRAM / SDRAM 外设(源/目标) UART / SPI / ADC 总线请求/授予 数据搬运路径 中断信号 流程:CPU 配置 DMA → DMA 请求总线 → 总线仲裁 → DMA 从外设读取数据 → 写入内存 → 传输完成中断

1.6 一个简单例子——验证你的理解

假设你有一个 UART 外设,每收到 1 字节就发出一个硬件请求。你想把这 10 个字节自动搬到内存数组 buffer[10] 中。用伪代码描述的话,大概是这样的:

// 配置 DMA 通道
DMA_Channel->SRC_ADDR = UART->DR;        // 源:UART 数据寄存器(固定地址)
DMA_Channel->DST_ADDR = &buffer[0];       // 目的:内存数组首地址
DMA_Channel->TRANSFER_SIZE = 10;          // 长度:10 字节
DMA_Channel->SRC_INC = 0;                 // 源地址不递增(外设寄存器固定)
DMA_Channel->DST_INC = 1;                 // 目的地址递增(数组连续)
DMA_Channel->TRIGGER = UART_RX_REQ;       // 触发源:UART 接收请求
DMA_Channel->ENABLE = 1;                  // 使能 DMA

// CPU 后续只需等待中断,或轮询一个“传输完成”标志位
while (!transfer_done_flag);  // 或者等待中断回调
process_data(buffer);

你想想看,如果没有 DMA,你需要在 UART 中断里一个一个地读字节并判断是否收满 10 个。有了 DMA,这 10 个接收动作全部由硬件自动完成,CPU 只需要在最后一次性处理数据。差距就是这么明显。

1.7 小结——把概念理清楚,后面就好办了

这一章节我们把 DMA 的基石打牢了:

  • DMA 是什么——硬件搬运工。
  • 为什么需要它——解放 CPU,提高系统吞吐量。
  • 怎么工作的——配置→请求总线→搬运→中断。
  • 跟 CPU 搬移比——各有优劣,但量大的场景 DMA 完胜。

我个人的小建议:刚接触 DMA 的同学,别急着去配寄存器。先把“谁发起、谁执行、谁受益”这几个角色关系理清楚。弄懂了这个,Scatter Gather 那种高级玩法其实就是更大号的描述符列表而已。后面的章节我们就会深入进去。