很多刚接触 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 是搬运工,外设和内存是货物的两端。
先看一个不用 DMA 的 ADC 采样流程。
伪代码如下:
for (int i = 0; i < 1024; i++)
{
Start_ADC_Convert();
Wait_ADC_Complete();
adc_buf[i] = Read_ADC_DataRegister();
}CPU 做了很多重复工作:
如果采样频率低,这样写没问题。 但如果 ADC 采样很快,或者系统同时还要跑电机控制、通信协议、UI、算法任务,CPU 就会被数据搬运占用太多时间。
串口接收也是类似问题。
不用 DMA 时,CPU 可能每收到一个字节就进一次中断:
void USART_IRQHandler(void)
{
rx_buf[index++] = USART_DR;
}如果串口数据量大,中断频率就会很高。 CPU 一直进中断,主程序自然就被打断得很碎。
先看没有 DMA 的情况,CPU 基本是在替外设搬数据。

对比着看,DMA 的价值就更容易理解了:它把“搬数据”这件事从 CPU 手里拿走。
DMA 的全称是 Direct Memory Access,直译是“直接内存访问”。
这个名字容易让人误会,好像 DMA 只是访问内存。 更准确地说,它是:
MCU 内部的一个硬件数据搬运控制器,可以在不让 CPU 逐个搬运数据的情况下,在外设和内存之间传输数据。
常见传输方向有三种:
方向 | 例子 |
|---|---|
外设到内存 | ADC 采样到数组、UART 接收到缓冲区 |
内存到外设 | UART 发送数组、SPI 刷屏、DAC 输出波形 |
内存到内存 | 拷贝一段 RAM 数据,部分 MCU 支持 |
DMA 不是凭空工作的。 它需要 CPU 先告诉它几件事:
这些就是我们在工程里配置 DMA 的本质。
先看系统结构图。

从图里可以看到,DMA 不是 CPU 的一条语句,也不是普通函数。 它是 MCU 内部一个独立的硬件模块,并且和 CPU 一样可以访问总线。
以 ADC 采样为例:
所以 DMA 的核心不是“让数据消失在后台”,而是把原本 CPU 做的数据搬运交给硬件完成。
再看一次完整流程。

还是以 ADC → 内存 为例。
CPU 先配置:
源地址:ADC 数据寄存器地址
目标地址:adc_buf 数组首地址
传输长度:1024
源数据宽度:16 bit
目标数据宽度:16 bit
源地址自增:不自增
目标地址自增:自增
模式:普通模式或循环模式为什么 ADC 源地址不自增?
因为 ADC 数据永远从同一个寄存器读:
ADC_DR为什么目标地址要自增?
因为每次采样值要依次写入数组:
adc_buf[0]
adc_buf[1]
adc_buf[2]
...这就是 DMA 配置里“地址自增”的含义。
DMA 不一定自己主动搬。
对于 ADC、UART、SPI 这类外设,通常是外设事件触发 DMA:
每来一次请求,DMA 就按配置搬一次数据。
DMA 每搬一次,会做几件事:
读取源地址
写入目标地址
根据配置更新地址
传输计数器减 1
判断是否半传输或传输完成比如 ADC 采样到数组:
第 1 次:ADC_DR → adc_buf[0]
第 2 次:ADC_DR → adc_buf[1]
第 3 次:ADC_DR → adc_buf[2]
...CPU 不需要每次都介入。
如果传输长度是 1024:
搬到 512 个:半传输 HT
搬到 1024 个:传输完成 TC这两个事件非常重要。
在 ADC 连续采样里,我们经常用双半区处理:
DMA 正在写前半区时,CPU 处理后半区
DMA 正在写后半区时,CPU 处理前半区这样可以实现连续采样,不容易丢数据。
DMA 首先要知道:
从哪里搬
搬到哪里例如 ADC 到内存:
源地址:&ADCx->DR
目标地址:adc_bufUART 发送:
源地址:tx_buf
目标地址:&USARTx->DRUART 接收:
源地址:&USARTx->DR
目标地址:rx_buf方向不同,源和目标就不同。
数据宽度决定每次搬多少。
常见有:
8 bit
16 bit
32 bit例如:
如果宽度配错,轻则数据错位,重则传输异常。
地址自增是 DMA 最容易让初学者困惑的地方。
判断方法很简单:
如果每次都访问同一个寄存器,不自增。 如果每次要写入或读取数组下一个元素,自增。
ADC 到数组:
ADC_DR:不自增
adc_buf:自增UART 发送数组:
tx_buf:自增
USART_DR:不自增内存到内存:
src_buf:自增
dst_buf:自增传输长度不是字节数,而是“传输次数”。
如果配置:
数据宽度:16 bit
传输长度:1024表示搬 1024 个半字,也就是 2048 字节。
如果配置:
数据宽度:8 bit
传输长度:1024表示搬 1024 个字节。
这个概念很关键,尤其在调 ADC、SPI、I2S、LCD 时。
普通模式:
搬完指定长度后停止适合:
循环模式:
搬完指定长度后,从头继续搬适合:
循环模式一定要配合好缓冲区处理,否则 CPU 还没处理完,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 思路。
串口接收如果一个字节进一次中断,数据量大时 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 不需要每个字节都进中断,而是在收到一段数据后统一处理。
很多人会说“DMA 不占 CPU”,这句话不够严谨。
更准确的说法是:
DMA 不需要 CPU 逐个数据搬运,但 CPU 仍然需要配置 DMA、处理完成事件、处理错误和消费缓冲区数据。
CPU 仍然要做:
DMA 省掉的是最重复、最机械的搬运动作。
会。
DMA 和 CPU 都要访问内存和外设寄存器,本质上都要用总线。 所以 DMA 并不是魔法,它只是把搬运工作从 CPU 指令流里拿出来,交给独立硬件模块执行。
在高性能 MCU 上,可能会有:
这些都会影响 DMA 行为。
一般入门阶段先记住:
DMA 可以减少 CPU 参与,但大量 DMA 传输仍然会占用总线带宽。
所以在高速采样、屏幕刷新、音频传输、网络收发等场景里,仍然要注意带宽和缓冲区设计。
ADC 到数组时,如果目标地址没有自增,就会一直写 adc_buf[0]。
UART 发送时,如果源地址没有自增,就会一直发送同一个字节。
ADC 是 12 bit 结果,通常用 16 bit 存。 如果 DMA 配成 8 bit,数据就可能被截断或错位。
传输长度是次数,不一定是字节数。
例如 uint16_t adc_buf[1024],长度应该填 1024,不是 2048。
循环模式下,如果 CPU 处理太慢,DMA 会重新写到旧位置。
解决办法:
在带 D-Cache 的 MCU 上,比如 STM32H7,DMA 和 CPU 看到的数据可能不一致。
典型现象:
这是 Cache 一致性问题,需要根据芯片和工程处理:
这部分是进阶内容,但如果你用的是 H7、MPU、带 Cache 的 Cortex-M7,一定要留意。
适合用 DMA 的场景:
典型场景:
场景 | 是否适合 DMA |
|---|---|
ADC 连续采样 | 很适合 |
UART 大量接收 | 很适合 |
UART 发送大包 | 适合 |
SPI 刷屏 | 很适合 |
I2S 音频 | 很适合 |
DAC 输出波形 | 很适合 |
偶尔发 1 个字节 | 没必要 |
很复杂的非规则协议解析 | DMA 只负责接收,解析仍由 CPU 做 |
简单判断:
如果 CPU 在重复搬数据,考虑 DMA。 如果 CPU 在做判断、计算、协议解析,DMA 不能替你完成这些逻辑。
DMA 可以从三个层次理解。
第一层:
DMA 是硬件搬运工。
第二层:
CPU 配置搬运规则,DMA 根据外设请求在外设和内存之间搬数据。
第三层:
DMA 减少 CPU 的重复搬运和高频中断压力,但仍然会占用总线,也需要正确的缓冲区、中断和 Cache 处理。
如果你第一次学 DMA,不要一上来陷入寄存器细节。 先把这几个问题想清楚:
能回答这几个问题,DMA 就已经理解了一大半。
最后用一句话收尾:
DMA 的本质不是让 CPU 消失,而是让 CPU 从“搬数据”升级为“安排数据怎么搬”。
如果你在调 STM32、RS485、Modbus、RTT、波形采集或者数据可视化,上位机工具 多多盒子助手。 它把串口、J-Link RTT、Modbus、实时图表、CRC 校验、日志保存和固件升级这些常用能力放在了一起,适合做日常嵌入式调试。
项

目地址:
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。