你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

DMA 原理从 0 到 1:为什么它能解放 CPU?一文讲透 DMA 的工作流程

[复制链接]
murphyMMC 发布时间:2026-7-27 19:58

前言

很多刚接触 STM32、GD32、NXP、ESP32 或其他 MCU 的同学,第一次看到 DMA 时都会有点懵:

DMA 到底是什么?** 为什么开启 DMA 后,ADC、串口、SPI 就能“不占 CPU”传数据? **DMA 是不是一个更快的 memcpy?** 为什么 DMA 还要配置源地址、目标地址、数据宽度、传输长度、自增模式? **DMA 半传输中断、传输完成中断又是什么意思?

如果只背概念,DMA 很抽象。** 但如果从一个实际问题出发,它就很好理解。**

假设我们要连续采集 ADC,并把采样结果放进数组:

uint16_t adc_buf[1024];

不用 DMA 时,CPU 需要不断做这件事:

adc_buf[i++] = ADC_DR;

也就是 ADC 每转换一次,CPU 就要过来读一次寄存器、写一次内存。** 如果采样频率很高,CPU 会被这种重复搬运拖住。**

DMA 的作用就是:

让 CPU 先把搬运规则配置好,然后由 DMA 硬件自动把数据从外设搬到内存,或者从内存搬到外设。

一句话理解:

CPU 是指挥,DMA 是搬运工,外设和内存是货物的两端。


1. 没有 DMA 时,CPU 在做什么

先看一个不用 DMA 的 ADC 采样流程。

伪代码如下:

for (int i = 0; i < 1024; i++)
{
    Start_ADC_Convert();
    Wait_ADC_Complete();
    adc_buf[i] = Read_ADC_DataRegister();
}

CPU 做了很多重复工作:

  • 启动转换
  • 等待完成
  • 读取 ADC 数据寄存器
  • 写入内存数组
  • 更新数组下标
  • 判断是否采完

如果采样频率低,这样写没问题。** 但如果 ADC 采样很快,或者系统同时还要跑电机控制、通信协议、UI、算法任务,CPU 就会被数据搬运占用太多时间。**

串口接收也是类似问题。

不用 DMA 时,CPU 可能每收到一个字节就进一次中断:

void USART_IRQHandler(void)
{
    rx_buf[index++] = USART_DR;
}

如果串口数据量大,中断频率就会很高。** CPU 一直进中断,主程序自然就被打断得很碎。**

先看没有 DMA 的情况,CPU 基本是在替外设搬数据。

image.png

对比着看,DMA 的价值就更容易理解了:它把“搬数据”这件事从 CPU 手里拿走。


2. DMA 到底是什么

DMA 的全称是 ****Direct Memory Access**,直译是“直接内存访问”。**

这个名字容易让人误会,好像 DMA 只是访问内存。** 更准确地说,它是:**

MCU 内部的一个硬件数据搬运控制器,可以在不让 CPU 逐个搬运数据的情况下,在外设和内存之间传输数据。

常见传输方向有三种:

方向 例子
外设到内存 ADC 采样到数组、UART 接收到缓冲区
内存到外设 UART 发送数组、SPI 刷屏、DAC 输出波形
内存到内存 拷贝一段 RAM 数据,部分 MCU 支持

DMA 不是凭空工作的。** 它需要 CPU 先告诉它几件事:**

  • 从哪里读数据
  • 写到哪里去
  • 每次搬几个字节
  • 总共搬多少次
  • 地址是否自动增加
  • 谁来触发传输
  • 搬完后要不要中断通知 CPU

这些就是我们在工程里配置 DMA 的本质。


3. DMA 在系统里的位置

先看系统结构图。

image.png

从图里可以看到,DMA 不是 CPU 的一条语句,也不是普通函数。** 它是 MCU 内部一个独立的硬件模块,并且和 CPU 一样可以访问总线。**

以 ADC 采样为例:

  1. ADC 转换完成后,数据出现在 ADC 数据寄存器里
  2. ADC 向 DMA 发出请求
  3. DMA 通过总线读取 ADC 数据寄存器
  4. DMA 再通过总线把数据写入 RAM 数组
  5. 搬运达到指定数量后,DMA 置位标志或触发中断
  6. CPU 在中断或回调里处理这块数据

所以 DMA 的核心不是“让数据消失在后台”,而是把原本 CPU 做的数据搬运交给硬件完成。


4. DMA 一次传输到底发生了什么

再看一次完整流程。

image.png

还是以 ADC → 内存 为例。

4.1 CPU 配置 DMA

CPU 先配置:

源地址:ADC 数据寄存器地址
目标地址:adc_buf 数组首地址
传输长度:1024
源数据宽度:16 bit
目标数据宽度:16 bit
源地址自增:不自增
目标地址自增:自增
模式:普通模式或循环模式

为什么 ADC 源地址不自增?

因为 ADC 数据永远从同一个寄存器读:

ADC_DR

为什么目标地址要自增?

因为每次采样值要依次写入数组:

adc_buf[0]
adc_buf[1]
adc_buf[2]
...

这就是 DMA 配置里“地址自增”的含义。

4.2 外设产生 DMA 请求

DMA 不一定自己主动搬。

对于 ADC、UART、SPI 这类外设,通常是外设事件触发 DMA:

  • ADC 转换完成,触发一次 DMA 请求
  • UART 收到一个字节,触发一次 DMA 请求
  • UART 发送寄存器空,触发一次 DMA 请求
  • SPI 接收缓冲非空,触发一次 DMA 请求
  • SPI 发送缓冲空,触发一次 DMA 请求

每来一次请求,DMA 就按配置搬一次数据。

4.3 DMA 搬数据并更新计数器

DMA 每搬一次,会做几件事:

读取源地址
写入目标地址
根据配置更新地址
传输计数器减 1
判断是否半传输或传输完成

比如 ADC 采样到数组:

第 1 次:ADC_DR → adc_buf[0]
第 2 次:ADC_DR → adc_buf[1]
第 3 次:ADC_DR → adc_buf[2]
...

CPU 不需要每次都介入。

4.4 半传输和传输完成

如果传输长度是 1024:

搬到 512 个:半传输 HT
搬到 1024 个:传输完成 TC

这两个事件非常重要。

在 ADC 连续采样里,我们经常用双半区处理:

DMA 正在写前半区时,CPU 处理后半区
DMA 正在写后半区时,CPU 处理前半区

这样可以实现连续采样,不容易丢数据。


5. DMA 的几个核心配置怎么理解

5.1 源地址和目标地址

DMA 首先要知道:

从哪里搬
搬到哪里

例如 ADC 到内存:

源地址:&ADCx->DR
目标地址:adc_buf

UART 发送:

源地址:tx_buf
目标地址:&USARTx->DR

UART 接收:

源地址:&USARTx->DR
目标地址:rx_buf

方向不同,源和目标就不同。

5.2 数据宽度

数据宽度决定每次搬多少。

常见有:

8 bit
16 bit
32 bit

例如:

  • UART 收发通常是 8 bit
  • ADC 结果通常用 16 bit
  • 某些 DAC、SPI、内存拷贝可能用 16 bit 或 32 bit

如果宽度配错,轻则数据错位,重则传输异常。

5.3 地址自增

地址自增是 DMA 最容易让初学者困惑的地方。

判断方法很简单:

如果每次都访问同一个寄存器,不自增。** 如果每次要写入或读取数组下一个元素,自增。**

ADC 到数组:

ADC_DR:不自增
adc_buf:自增

UART 发送数组:

tx_buf:自增
USART_DR:不自增

内存到内存:

src_buf:自增
dst_buf:自增

5.4 传输长度

传输长度不是字节数,而是“传输次数”。

如果配置:

数据宽度:16 bit
传输长度:1024

表示搬 1024 个半字,也就是 2048 字节。

如果配置:

数据宽度:8 bit
传输长度:1024

表示搬 1024 个字节。

这个概念很关键,尤其在调 ADC、SPI、I2S、LCD 时。

5.5 普通模式和循环模式

普通模式:

搬完指定长度后停止

适合:

  • UART 发送一包数据
  • SPI 发送一帧图片数据
  • 一次性内存拷贝

循环模式:

搬完指定长度后,从头继续搬

适合:

  • ADC 连续采样
  • I2S 音频采集
  • UART 环形接收
  • DAC 循环输出波形

循环模式一定要配合好缓冲区处理,否则 CPU 还没处理完,DMA 又把旧数据覆盖了。


6. 一个实际例子:ADC + DMA 连续采样

假设我们要采集 1024 个 ADC 数据。

缓冲区:

uint16_t adc_buf[1024];

启动:

HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_buf, 1024);

当 DMA 写完前 512 个数据时,会进入半传输回调:

void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc)
{
    ProcessAdcData(&adc_buf[0], 512);
}

当 DMA 写完后 512 个数据时,会进入完成回调:

void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
    ProcessAdcData(&adc_buf[512], 512);
}

如果 DMA 是循环模式,流程就是:

DMA 写前半区 → CPU 处理前半区
DMA 写后半区 → CPU 处理后半区
DMA 再写前半区 → CPU 再处理前半区
...

这就是很多音频采集、波形采集、电流采样里常见的 ping-pong 思路。


7. 一个实际例子:UART + DMA 接收

串口接收如果一个字节进一次中断,数据量大时 CPU 压力会很明显。

DMA 接收可以这样理解:

UART 每收到 1 字节
  ↓
触发 DMA 请求
  ↓
DMA 从 UART 数据寄存器读出
  ↓
写入 rx_buf

常见启动方式:

uint8_t rx_buf[256];

HAL_UART_Receive_DMA(&huart1, rx_buf, sizeof(rx_buf));

如果接收的是不定长数据,通常会配合空闲中断:

HAL_UARTEx_ReceiveToIdle_DMA(&huart1, rx_buf, sizeof(rx_buf));

回调:

void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
    if (huart->Instance == USART1)
    {
        ParseFrame(rx_buf, Size);
        HAL_UARTEx_ReceiveToIdle_DMA(&huart1, rx_buf, sizeof(rx_buf));
    }
}

这样 CPU 不需要每个字节都进中断,而是在收到一段数据后统一处理。


8. DMA 不是完全不占 CPU

很多人会说“DMA 不占 CPU”,这句话不够严谨。

更准确的说法是:

DMA 不需要 CPU 逐个数据搬运,但 CPU 仍然需要配置 DMA、处理完成事件、处理错误和消费缓冲区数据。

CPU 仍然要做:

  • 初始化 DMA
  • 启动外设
  • 启动 DMA
  • 处理半传输中断
  • 处理传输完成中断
  • 处理错误中断
  • 处理缓冲区里的数据

DMA 省掉的是最重复、最机械的搬运动作。


9. DMA 会不会和 CPU 抢总线

会。

DMA 和 CPU 都要访问内存和外设寄存器,本质上都要用总线。** 所以 DMA 并不是魔法,它只是把搬运工作从 CPU 指令流里拿出来,交给独立硬件模块执行。**

在高性能 MCU 上,可能会有:

  • AHB 总线
  • AXI 总线
  • 多层总线矩阵
  • DMA 控制器
  • DMAMUX
  • Cache
  • TCM / SRAM 多区域

这些都会影响 DMA 行为。

一般入门阶段先记住:

DMA 可以减少 CPU 参与,但大量 DMA 传输仍然会占用总线带宽。

所以在高速采样、屏幕刷新、音频传输、网络收发等场景里,仍然要注意带宽和缓冲区设计。


10. DMA 常见错误

10.1 地址自增配置错

ADC 到数组时,如果目标地址没有自增,就会一直写 adc_buf[0]

UART 发送时,如果源地址没有自增,就会一直发送同一个字节。

10.2 数据宽度配置错

ADC 是 12 bit 结果,通常用 16 bit 存。** 如果 DMA 配成 8 bit,数据就可能被截断或错位。**

10.3 传输长度理解错

传输长度是次数,不一定是字节数。

例如 uint16_t adc_buf[1024],长度应该填 1024,不是 2048。

10.4 缓冲区被覆盖

循环模式下,如果 CPU 处理太慢,DMA 会重新写到旧位置。

解决办法:

  • 增大缓冲区
  • 使用半传输/完成双半区处理
  • 降低采样率
  • 提高处理效率
  • 只在回调里置标志,主循环里处理

10.5 Cache 一致性问题

在带 D-Cache 的 MCU 上,比如 STM32H7,DMA 和 CPU 看到的数据可能不一致。

典型现象:

  • DMA 明明接收了数据,CPU 读缓冲区还是旧值
  • CPU 改了发送缓冲区,DMA 发出去的是旧数据

这是 Cache 一致性问题,需要根据芯片和工程处理:

  • DMA 缓冲区放到非 Cache 区域
  • 接收后 invalidate cache
  • 发送前 clean cache
  • 注意缓冲区地址和长度对齐

这部分是进阶内容,但如果你用的是 H7、MPU、带 Cache 的 Cortex-M7,一定要留意。


11. 怎么判断一个场景该不该用 DMA

适合用 DMA 的场景:

  • 数据量大
  • 传输频繁
  • 数据格式规则
  • CPU 不想被频繁中断打断
  • 外设支持 DMA 请求
  • 可以用缓冲区批量处理

典型场景:

场景 是否适合 DMA
ADC 连续采样 很适合
UART 大量接收 很适合
UART 发送大包 适合
SPI 刷屏 很适合
I2S 音频 很适合
DAC 输出波形 很适合
偶尔发 1 个字节 没必要
很复杂的非规则协议解析 DMA 只负责接收,解析仍由 CPU 做

简单判断:

如果 CPU 在重复搬数据,考虑 DMA。** 如果 CPU 在做判断、计算、协议解析,DMA 不能替你完成这些逻辑。**


12. 总结

DMA 可以从三个层次理解。

第一层:

DMA 是硬件搬运工。

第二层:

CPU 配置搬运规则,DMA 根据外设请求在外设和内存之间搬数据。

第三层:

DMA 减少 CPU 的重复搬运和高频中断压力,但仍然会占用总线,也需要正确的缓冲区、中断和 Cache 处理。

如果你第一次学 DMA,不要一上来陷入寄存器细节。** 先把这几个问题想清楚:**

  • 数据从哪里来?
  • 数据要到哪里去?
  • 每次搬多宽?
  • 搬多少次?
  • 哪边地址要自增?
  • 是普通模式还是循环模式?
  • 搬到一半和搬完后 CPU 要做什么?

能回答这几个问题,DMA 就已经理解了一大半。

最后用一句话收尾:

DMA 的本质不是让 CPU 消失,而是让 CPU 从“搬数据”升级为“安排数据怎么搬”。

如果你在调 STM32、RS485、Modbus、RTT、波形采集或者数据可视化,上位机工具 ****多多盒子助手** **它把串口、J-Link RTT、Modbus、实时图表、CRC 校验、日志保存和固件升级这些常用能力放在了一起,适合做日常嵌入式调试。

项目地址:duoduobox-jlink-rtt-waveform.png

https://gitee.com/momingchuan/duo-duo-box

收藏 评论0 发布时间:2026-7-27 19:58

举报

0个回答

所属标签

相似技术帖

官网相关资源

关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
STM32N6 AI生态系统
STM32MCU,MPU高性能GUI
ST ACEPACK电源模块
意法半导体生物传感器
STM32Cube扩展软件包
关注我们
st-img 微信公众号
st-img 手机版