STM32 DMA到底解决了什么问题?从CPU搬运数据到自动传输完整解析
STM32 DMA到底解决了什么问题?从CPU搬运数据到自动传输完整解析
前言
学习 STM32 时,经常会看到下面这些函数:
HAL_UART_Receive_DMA();
HAL_UART_Transmit_DMA();
HAL_ADC_Start_DMA();
HAL_SPI_TransmitReceive_DMA();
很多初学者知道 DMA 可以“减轻 CPU 负担”,但对下面这些问题并不清楚:
- DMA 到底替 CPU 做了什么?
- 没有 DMA 时,CPU 是怎样搬运数据的?
- DMA 是不是完全不需要 CPU?
- DMA 和中断有什么区别?
- 数据从外设搬到内存时,经过了哪些步骤?
- Normal、Circular 和双缓冲模式有什么区别?
- 为什么用了 DMA,串口还是会丢数据?
- 为什么 Cortex-M7 使用 DMA 后,CPU 读到的还是旧数据?
- DMA 什么时候值得使用,什么时候反而没有必要?
本文将从 CPU 搬运数据的问题开始,详细讲解 STM32 DMA 的工作原理、配置流程、典型应用和常见故障。
一、先说结论:DMA解决了什么问题
DMA 的英文全称是:
Direct Memory Access
中文通常称为:
直接存储器访问
DMA 解决的核心问题是:
外设和内存之间存在大量、重复的数据搬运工作,如果所有数据都由 CPU 一次一次读取和写入,会浪费大量 CPU 时间。
DMA 可以按照 CPU 预先配置好的规则,自动完成以下数据传输:
外设 → 内存
内存 → 外设
内存 → 内存
例如串口接收数据时:
USART数据寄存器 → 内存接收数组
没有 DMA 时,每收到一个字节,CPU 都要执行一次读取和保存。
使用 DMA 后,DMA 控制器可以自动将每个字节写入数组。CPU 不需要参与每一个字节的搬运,只需要在一批数据接收完成后进行处理。
一句话概括:
DMA不是帮助CPU计算数据,而是帮助CPU搬运数据。
二、为什么CPU搬运数据会成为问题
假设串口以 115200 bit/s 接收数据,数据格式为:
1位起始位
8位数据位
1位停止位
一个字节实际需要传输大约 10 bit。
每秒可以接收的字节数约为:
115200 ÷ 10 = 11520 Byte/s
如果使用逐字节中断接收,CPU 每秒可能进入一万多次串口中断。
每次中断都需要执行:
保存现场
进入中断服务函数
读取状态寄存器
读取数据寄存器
写入接收数组
更新数组下标
判断是否接收完成
恢复现场
退出中断
如果只有一个低速串口,CPU 可能还能承受。
但实际项目中可能同时存在:
- 多路 UART;
- ADC连续采样;
- SPI高速通信;
- CAN数据接收;
- I²S音频采集;
- 定时器输入捕获;
- 网络数据处理;
- FreeRTOS任务调度。
此时CPU如果一直忙于搬运数据,就没有足够时间执行真正的业务逻辑。
三、没有DMA时,CPU怎样读取外设数据
以UART接收一个字节为例。
1. 外设接收到数据
UART硬件接收到一个完整字节后,将数据放入接收数据寄存器:
UART_RX引脚
↓
串口接收移位寄存器
↓
USART接收数据寄存器
2. CPU读取数据寄存器
CPU需要执行类似操作:
data = USARTx->RDR;
3. CPU将数据写入内存
receive_buffer[index] = data;
index++;
数据实际经历的路径是:
USART数据寄存器
↓
CPU寄存器
↓
内存数组
CPU成为了外设和内存之间的“搬运工”。
如果数据量很大,CPU会反复执行:
读取外设
写入内存
读取外设
写入内存
读取外设
写入内存
……
这些操作本身没有复杂算法,只是不断复制数据。
DMA就是为了接管这类重复搬运工作。
四、DMA是怎样搬运数据的
使用DMA后,数据路径变成:
USART数据寄存器
↓
DMA控制器
↓
内存数组
CPU只需要提前告诉DMA:
- 数据从哪里来;
- 数据要搬到哪里;
- 一次搬多少数据;
- 每个数据有多宽;
- 地址是否自动增加;
- 什么时候开始;
- 完成后是否产生中断。
例如:
源地址:USART接收数据寄存器
目标地址:receive_buffer
传输数量:100字节
数据宽度:8位
源地址:固定
目标地址:每次加1
方向:外设到内存
配置完成后,CPU可以继续执行其他任务。
当UART收到数据时,会向DMA发出请求:
UART收到一个字节
↓
产生DMA请求
↓
DMA读取UART数据寄存器
↓
DMA写入内存数组
↓
剩余传输数量减1
当100个字节全部搬运完成后,DMA可以产生一次完成中断,通知CPU:
数据已经全部放到数组中,可以开始处理了。
五、DMA并不是完全不需要CPU
“DMA不需要CPU”是一种不准确的说法。
更准确的说法是:
DMA不需要CPU参与每一次数据搬运,但仍然需要CPU完成初始化、启动、状态处理和数据处理。
CPU仍然需要负责:
- 配置DMA通道或Stream;
- 设置源地址和目标地址;
- 设置传输数量;
- 设置数据宽度;
- 设置传输模式;
- 启动外设;
- 响应DMA完成中断;
- 处理接收到的数据;
- 处理传输错误;
- 必要时重新启动DMA。
因此,DMA降低的是:
CPU参与每个字节、每个半字或每个字搬运的频率
而不是让CPU完全退出系统。
六、轮询、中断和DMA有什么区别
外设数据传输通常有三种方式:
- 轮询;
- 中断;
- DMA。
1. 轮询方式
CPU不断检查外设状态:
while ((USART1->ISR & USART_ISR_RXNE) == 0)
{
/* 一直等待 */
}
data = USART1->RDR;
通信过程:
CPU检查状态
↓
没有数据
↓
继续检查
↓
有数据
↓
读取数据
优点
- 实现简单;
- 调试方便;
- 执行流程直观;
- 适合初始化阶段或少量数据。
缺点
- CPU一直等待;
- 不能及时处理其他任务;
- 数据量大时效率很低;
- 容易造成程序阻塞。
2. 中断方式
外设收到数据后通知CPU:
外设收到数据
↓
产生中断
↓
CPU进入中断服务函数
↓
读取并保存数据
优点
- CPU不需要一直轮询;
- 数据到达时才进行处理;
- 适合低速、随机到达的数据;
- 实时响应能力较好。
缺点
- 每个数据可能触发一次中断;
- 高频中断会占用大量CPU;
- 中断服务函数需要尽量短;
- 大数据量传输效率有限。
3. DMA方式
外设发出DMA请求后,由DMA完成数据搬运:
外设收到数据
↓
产生DMA请求
↓
DMA搬运数据
↓
整批完成后通知CPU
优点
- CPU不参与逐个数据搬运;
- 适合连续、大批量、高速数据;
- 降低中断次数;
- 提高系统并行处理能力;
- 有利于保持固定采样节奏。
缺点
- 配置比轮询复杂;
- 需要管理缓冲区;
- 存在通道映射和优先级问题;
- 高速系统可能涉及Cache一致性;
- DMA完成只代表数据搬完,不代表业务处理完成。
七、三种方式的直观对比
| 对比项目 | 轮询 | 中断 | DMA |
|---|---|---|---|
| CPU是否等待 | 是 | 否 | 否 |
| CPU是否逐个搬运数据 | 是 | 是 | 否 |
| 中断频率 | 通常没有 | 可能每个数据一次 | 通常每批数据一次 |
| 实现难度 | 低 | 中 | 较高 |
| 适合数据量 | 少量 | 少量或间歇数据 | 连续或大量数据 |
| 实时性 | 取决于轮询速度 | 较好 | 适合稳定连续传输 |
| CPU占用 | 高 | 中 | 较低 |
| 缓冲区管理 | 简单 | 需要 | 非常重要 |
可以简单理解为:
轮询:CPU一直等着搬
中断:数据来了叫CPU搬
DMA:数据来了DMA自己搬,搬完再叫CPU
八、DMA内部有哪些核心参数
配置DMA时,需要理解下面几个参数。
1. 传输方向
常见方向包括:
外设到内存
内存到外设
内存到内存
外设到内存
例如:
ADC → adc_buffer
UART_RX → uart_rx_buffer
SPI_RX → spi_rx_buffer
内存到外设
例如:
uart_tx_buffer → UART_TX
frame_buffer → SPI
audio_buffer → I2S
内存到内存
例如:
source_buffer → destination_buffer
是否支持内存到内存模式,要看具体STM32系列和DMA控制器。
2. 外设地址
外设地址通常是固定的寄存器地址。
例如UART接收时:
USARTx->RDR
同一个UART数据寄存器不断接收新数据,因此外设地址通常不增加。
DMA配置中一般选择:
Peripheral Increment:Disable
3. 内存地址
内存地址通常是数组首地址:
uint8_t uart_rx_buffer[100];
DMA依次写入:
uart_rx_buffer[0]
uart_rx_buffer[1]
uart_rx_buffer[2]
……
因此通常需要启用:
Memory Increment:Enable
4. 数据宽度
DMA传输宽度可能包括:
Byte:8位
Half Word:16位
Word:32位
必须与外设数据寄存器和内存缓冲区类型匹配。
例如ADC结果为12位,通常保存在16位变量中:
uint16_t adc_buffer[16];
DMA可以配置为:
外设宽度:Half Word
内存宽度:Half Word
如果误配置成Byte,ADC结果可能被截断。
5. 传输数量
DMA需要知道要搬运多少个数据。
例如:
uint16_t adc_buffer[32];
启动:
HAL_ADC_Start_DMA(
&hadc1,
(uint32_t *)adc_buffer,
32
);
这里的32通常表示32个传输单元,不一定表示32字节。
实际字节数由数据宽度决定。
如果每个传输单元为16位:
32个Half Word = 64字节
6. 地址自增
典型配置如下:
| 参数 | 外设地址 | 内存地址 |
|---|---|---|
| 是否自增 | 通常不增加 | 通常增加 |
例如UART接收:
源地址:USART_RDR,固定
目标地址:buffer[0]、buffer[1]、buffer[2]……,递增
7. DMA优先级
STM32可能同时有多个DMA请求:
- ADC;
- UART;
- SPI;
- I²S;
- 定时器;
- DAC。
当多个请求同时发生时,需要决定先服务谁。
常见优先级包括:
Low
Medium
High
Very High
优先级越高,DMA请求越容易先被处理。
但优先级不是越高越好。
如果所有DMA都配置成最高优先级,就失去了区分意义。
九、DMA是如何被外设触发的
DMA不是自己随时读取外设,而是由外设产生DMA请求。
以ADC为例:
ADC完成一次转换
↓
ADC数据寄存器产生新结果
↓
ADC发出DMA请求
↓
DMA读取ADC数据寄存器
↓
写入内存
以UART接收为例:
UART接收到一个字节
↓
接收数据寄存器非空
↓
UART发出DMA请求
↓
DMA读取RDR
↓
写入接收数组
以UART发送为例:
UART发送数据寄存器可以写入
↓
UART发出DMA请求
↓
DMA从内存读取一个字节
↓
写入UART发送寄存器
因此DMA和外设之间需要有硬件请求映射关系。
十、DMA通道、Stream和DMAMUX是什么
不同STM32系列的DMA结构不同。
1. 固定通道映射
部分较早STM32系列中,一个外设固定对应某个DMA通道。
例如:
USART1_RX → DMA1_Channel5
USART1_TX → DMA1_Channel4
如果选错通道,DMA不会正常工作。
2. Stream和Channel选择
部分STM32系列使用:
DMA控制器
↓
Stream
↓
Channel
一个Stream可以通过Channel选择连接不同外设请求。
配置时需要同时确认:
- DMA1还是DMA2;
- 使用哪个Stream;
- 使用哪个Channel;
- 是否与其他外设冲突。
3. DMAMUX
部分较新的STM32系列增加DMAMUX:
外设DMA请求
↓
DMAMUX
↓
DMA Channel
DMAMUX可以更灵活地将外设请求映射到DMA通道。
但配置也更加复杂,需要确认:
- Request编号;
- DMA通道;
- 同步触发;
- Request Generator;
- 是否存在请求冲突。
十一、Normal模式和Circular模式有什么区别
DMA最常见的两种模式是:
- Normal;
- Circular。
1. Normal模式
DMA按照设定数量完成一次传输后停止。
例如接收100字节:
接收第1字节
接收第2字节
……
接收第100字节
↓
DMA完成
↓
停止传输
如果需要继续接收,CPU需要重新启动DMA。
适合:
- 固定长度数据包;
- 单次发送;
- 单次ADC采样;
- 固定大小SPI传输。
2. Circular模式
DMA完成一轮后,自动从缓冲区开头重新开始。
例如:
buffer[0]
buffer[1]
……
buffer[15]
↓
buffer[0]
buffer[1]
……
适合:
- ADC连续采样;
- 音频数据流;
- 循环波形输出;
- 持续传感器采集;
- 固定速率数据流。
Circular模式的关键问题是:
CPU处理数据的速度必须跟得上DMA覆盖数据的速度。
如果CPU还没有处理完前一轮数据,DMA就重新写入同一块内存,旧数据会被覆盖。
十二、什么是半传输中断
假设DMA循环缓冲区长度为100:
buffer[0] ~ buffer[99]
DMA传输到一半时:
buffer[0] ~ buffer[49]
可以触发半传输中断。
全部传输完成时:
buffer[50] ~ buffer[99]
可以触发全传输中断。
这样CPU可以分段处理:
DMA填充前半区
↓
半传输中断
↓
CPU处理前半区
DMA继续填充后半区
↓
全传输中断
↓
CPU处理后半区
这是一种典型的“乒乓处理”思想。
优点是:
- DMA持续采集;
- CPU分块处理;
- 减少数据停顿;
- 降低缓冲区被覆盖的风险。
十三、什么是双缓冲模式
双缓冲模式使用两个内存缓冲区:
Buffer A
Buffer B
工作过程:
DMA写Buffer A
CPU处理Buffer B
DMA写满Buffer A后切换到Buffer B
CPU开始处理Buffer A
两块缓冲区交替使用:
DMA:A → B → A → B
CPU:B → A → B → A
这类似生产线:
一个缓冲区正在生产数据
另一个缓冲区正在消费数据
双缓冲适合:
- 音频采集;
- 高速ADC;
- 摄像头数据;
- 高速SPI;
- 网络数据流;
- 连续DAC输出。
十四、DMA为什么能降低CPU占用
假设需要接收1000个字节。
中断逐字节接收
可能产生:
1000次接收中断
CPU需要执行1000次:
进入中断
读取数据
保存数据
退出中断
DMA接收
DMA自动搬运1000个字节,可能只产生:
1次完成中断
或者:
1次半传输中断
1次全传输中断
CPU中断次数从1000次下降到1~2次。
因此,DMA主要降低:
- 中断进入和退出开销;
- 数据寄存器读取开销;
- 内存写入开销;
- 数组索引更新开销;
- CPU被频繁打断的次数。
十五、DMA是否一定比CPU快
不一定。
DMA的优势主要体现在:
降低CPU参与程度
提高并行处理能力
适合连续或批量传输
但DMA不一定在所有场景都更快。
例如只发送一个字节:
uint8_t data = 0x55;
如果使用DMA,需要:
配置地址
配置长度
清除标志
启动DMA
等待完成
处理回调
配置开销可能比CPU直接写寄存器还大。
因此:
少量、偶发数据:轮询或中断可能更简单
大量、连续数据:DMA优势明显
十六、STM32 UART轮询、IT和DMA代码对比
1. 轮询发送
uint8_t message[] = "Hello STM32\r\n";
HAL_UART_Transmit(
&huart1,
message,
sizeof(message) - 1U,
100U
);
CPU会等待发送完成。
2. 中断发送
uint8_t message[] = "Hello STM32\r\n";
HAL_UART_Transmit_IT(
&huart1,
message,
sizeof(message) - 1U
);
CPU不用一直等待,但发送过程中通常需要多次UART中断。
3. DMA发送
uint8_t message[] = "Hello STM32\r\n";
HAL_UART_Transmit_DMA(
&huart1,
message,
sizeof(message) - 1U
);
DMA负责将整个数组逐字节写入UART数据寄存器。
完成后会调用回调:
void HAL_UART_TxCpltCallback(
UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1)
{
/* DMA发送完成 */
}
}
十七、STM32 UART DMA接收固定长度数据
假设接收32字节:
#define UART_RX_LENGTH 32U
uint8_t uart_rx_buffer[UART_RX_LENGTH];
启动DMA:
if (HAL_UART_Receive_DMA(
&huart1,
uart_rx_buffer,
UART_RX_LENGTH) != HAL_OK)
{
Error_Handler();
}
接收完成后:
void HAL_UART_RxCpltCallback(
UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1)
{
/*
* uart_rx_buffer中已经有32字节数据。
* 不建议在回调中执行复杂业务。
*/
uart_rx_ready = 1U;
/*
* 如果DMA是Normal模式,
* 需要重新启动下一次接收。
*/
HAL_UART_Receive_DMA(
&huart1,
uart_rx_buffer,
UART_RX_LENGTH);
}
}
主循环中处理:
if (uart_rx_ready != 0U)
{
uart_rx_ready = 0U;
UART_ProcessData(
uart_rx_buffer,
UART_RX_LENGTH);
}
十八、不定长串口数据为什么不能只用固定长度DMA
实际串口协议的数据长度经常不固定。
例如:
第一帧:8字节
第二帧:21字节
第三帧:13字节
如果固定启动100字节DMA:
HAL_UART_Receive_DMA(
&huart1,
uart_rx_buffer,
100);
只有接满100字节才会触发完成回调。
如果对方只发送21字节,DMA会一直等待剩余79字节。
这时通常需要结合:
DMA + UART空闲中断
也就是:
DMA负责搬运字节
UART IDLE负责判断一帧暂时结束
十九、DMA加空闲中断接收不定长数据
很多STM32 HAL库提供:
HAL_UARTEx_ReceiveToIdle_DMA();
示例:
#define UART_RX_BUFFER_SIZE 256U
uint8_t uart_rx_buffer[UART_RX_BUFFER_SIZE];
启动接收:
if (HAL_UARTEx_ReceiveToIdle_DMA(
&huart1,
uart_rx_buffer,
UART_RX_BUFFER_SIZE) != HAL_OK)
{
Error_Handler();
}
可以关闭半传输中断,避免不需要的回调:
__HAL_DMA_DISABLE_IT(
huart1.hdmarx,
DMA_IT_HT);
接收事件回调:
void HAL_UARTEx_RxEventCallback(
UART_HandleTypeDef *huart,
uint16_t size)
{
if (huart->Instance == USART1)
{
/*
* size表示本次接收到的数据长度。
*/
UART_CopyToProcessBuffer(
uart_rx_buffer,
size);
/*
* Normal模式下重新启动。
* 某些HAL实现和模式可能已经自动管理,
* 需要结合具体芯片HAL确认。
*/
HAL_UARTEx_ReceiveToIdle_DMA(
&huart1,
uart_rx_buffer,
UART_RX_BUFFER_SIZE);
__HAL_DMA_DISABLE_IT(
huart1.hdmarx,
DMA_IT_HT);
}
}
这种方式非常适合:
- Modbus RTU;
- 自定义串口协议;
- AT指令;
- GPS数据;
- 传感器报文;
- 不定长调试命令。
二十、DMA接收不等于协议解析
DMA只负责:
把串口数据寄存器中的字节搬到内存数组
DMA不会自动完成:
- 判断帧头;
- 判断帧尾;
- CRC校验;
- Modbus地址判断;
- 功能码解析;
- 超时判断;
- 数据粘包处理;
- 数据拆包处理。
因此完整流程应该是:
UART接收数据
↓
DMA搬到内存
↓
空闲中断或长度条件判断帧结束
↓
协议解析
↓
CRC校验
↓
业务处理
不要把“DMA接收完成”误认为“协议帧一定完整”。
二十一、STM32 ADC使用DMA连续采集
ADC是DMA最典型的应用之一。
假设连续采集4个ADC通道:
#define ADC_CHANNEL_COUNT 4U
uint16_t adc_buffer[ADC_CHANNEL_COUNT];
启动ADC DMA:
if (HAL_ADC_Start_DMA(
&hadc1,
(uint32_t *)adc_buffer,
ADC_CHANNEL_COUNT) != HAL_OK)
{
Error_Handler();
}
如果ADC配置为:
扫描模式
连续转换
DMA循环模式
数据可能不断更新:
adc_buffer[0] → 通道1
adc_buffer[1] → 通道2
adc_buffer[2] → 通道3
adc_buffer[3] → 通道4
CPU只需要定期读取数组。
二十二、ADC DMA为什么比CPU逐次读取更合适
如果ADC采样率为100kHz,意味着每秒产生10万次转换结果。
如果每次转换都进入中断:
每秒10万次中断
CPU负担会非常大。
使用DMA后:
ADC每完成一次转换
↓
DMA自动写入数组
↓
一批数据完成后通知CPU
CPU可以一次处理几十、几百甚至几千个样本。
这对以下应用非常重要:
- 电流采样;
- 电压波形分析;
- 音频采集;
- 振动检测;
- 电机控制;
- FFT频谱计算;
- 多通道传感器采集。
二十三、ADC DMA缓冲区示例
假设需要连续采集128个样本:
#define ADC_SAMPLE_COUNT 128U
uint16_t adc_samples[ADC_SAMPLE_COUNT];
volatile uint8_t adc_first_half_ready;
volatile uint8_t adc_second_half_ready;
启动:
HAL_ADC_Start_DMA(
&hadc1,
(uint32_t *)adc_samples,
ADC_SAMPLE_COUNT);
半传输回调:
void HAL_ADC_ConvHalfCpltCallback(
ADC_HandleTypeDef *hadc)
{
if (hadc->Instance == ADC1)
{
adc_first_half_ready = 1U;
}
}
全传输回调:
void HAL_ADC_ConvCpltCallback(
ADC_HandleTypeDef *hadc)
{
if (hadc->Instance == ADC1)
{
adc_second_half_ready = 1U;
}
}
主循环处理:
if (adc_first_half_ready != 0U)
{
adc_first_half_ready = 0U;
ADC_ProcessSamples(
&adc_samples[0],
ADC_SAMPLE_COUNT / 2U);
}
if (adc_second_half_ready != 0U)
{
adc_second_half_ready = 0U;
ADC_ProcessSamples(
&adc_samples[ADC_SAMPLE_COUNT / 2U],
ADC_SAMPLE_COUNT / 2U);
}
这样ADC和CPU可以并行工作。
二十四、SPI为什么经常使用DMA
SPI速度通常比较高。
例如SPI时钟为20MHz,发送1KB数据时,CPU逐字节写入会产生大量操作。
典型应用包括:
- LCD刷新;
- Flash读写;
- 高速ADC;
- 传感器批量读取;
- 网络模块;
- FPGA通信。
DMA发送:
HAL_SPI_Transmit_DMA(
&hspi1,
tx_buffer,
tx_length);
DMA全双工传输:
HAL_SPI_TransmitReceive_DMA(
&hspi1,
tx_buffer,
rx_buffer,
data_length);
需要注意:
SPI DMA完成并不一定表示最后一个bit已经完全离开引脚。
某些场景中,DMA完成只表示最后一个数据已经写入SPI数据寄存器或FIFO。
如果要立即拉高片选,需要进一步等待SPI Busy标志清零。
二十五、DMA完成到底意味着什么
这是非常重要的工程问题。
DMA完成通常表示:
设定数量的数据已经由DMA完成读写
但不一定表示:
外设的物理传输已经完全结束
例如UART发送:
DMA完成:
最后一个字节已经写入UART发送寄存器。
UART TC完成:
最后一个字节的停止位已经真正从TX引脚发送完毕。
如果使用RS485,需要在发送完后切换DE方向:
DMA完成后立刻拉低DE
可能太早。
更可靠的流程是:
DMA把最后一个字节写入UART
↓
等待UART Transmission Complete
↓
确认停止位已经发送完成
↓
拉低RS485 DE
↓
切换到接收状态
二十六、DMA和中断是什么关系
DMA不是中断的替代品。
它们解决的是不同问题:
DMA:负责搬运数据
中断:负责通知事件
典型组合是:
DMA自动搬运1000个字节
↓
完成后产生一次DMA中断
↓
CPU处理这一批数据
因此实际系统通常是:
DMA + 中断
而不是:
DMA 或 中断二选一
二十七、为什么用了DMA仍然会丢数据
DMA可以降低CPU负担,但不能保证任何情况下都不丢数据。
常见原因包括:
1. 缓冲区太小
例如UART DMA缓冲区只有64字节,但CPU长时间没有处理。
新数据继续到来,缓冲区可能被覆盖。
2. CPU处理速度跟不上
DMA负责快速生产数据,CPU负责消费数据。
如果:
数据产生速度 > 数据处理速度
无论缓冲区多大,最终都会溢出。
这就是典型的生产者和消费者速度不匹配。
3. DMA没有及时重新启动
Normal模式传输完成后,如果CPU没有重新启动DMA,后续数据就无法接收。
4. 空闲中断处理不正确
可能出现:
- 清除IDLE标志方式错误;
- 数据长度计算错误;
- 重启DMA太晚;
- 回调中处理时间太长;
- 数据复制与DMA写入冲突。
5. UART发生溢出错误
如果UART接收寄存器中的数据没有及时被读取,可能出现ORE错误。
DMA配置错误或被其他高优先级总线访问长期阻塞时,也可能造成问题。
6. DMA优先级不合理
高速外设DMA优先级太低,可能无法及时获得总线访问权。
7. 中断被长时间关闭
如果程序长时间关闭中断,DMA可能仍在搬数据,但完成事件和缓冲区切换无法及时处理。
8. 缓冲区被其他代码修改
例如DMA正在发送:
HAL_UART_Transmit_DMA(
&huart1,
tx_buffer,
length);
但DMA还没结束,CPU就修改了:
tx_buffer[0] = new_data;
最终发送内容可能被改变。
二十八、DMA传输期间为什么不能随便修改缓冲区
DMA和CPU可能同时访问同一块内存。
例如:
DMA正在读取tx_buffer发送串口
CPU同时修改tx_buffer
可能发生:
前半部分是旧数据
后半部分是新数据
因此DMA发送期间,应保证缓冲区内容不被修改。
常见做法包括:
- 使用独立发送缓冲区;
- 发送前复制数据;
- 使用发送忙标志;
- 使用消息队列;
- 使用双缓冲;
- 完成回调后再释放缓冲区。
二十九、DMA缓冲区为什么经常使用volatile
中断回调和主程序之间共享的状态变量通常需要使用:
volatile
例如:
volatile uint8_t adc_data_ready;
因为这个变量可能在中断中被修改。
但DMA数据缓冲区是否必须声明为volatile,需要结合访问方式判断。
例如:
uint16_t adc_buffer[128];
仅仅因为DMA写这个数组,并不代表所有场景下都必须加volatile。
在有Cache的系统中,volatile也无法解决Cache一致性问题。
volatile的主要作用是告诉编译器:
该变量可能在程序正常控制流之外发生变化,
每次访问都应按照语言规则实际读取或写入。
它不能代替:
- 内存屏障;
- Cache维护;
- DMA同步;
- 临界区保护。
三十、Cortex-M7为什么会出现DMA Cache一致性问题
部分STM32 Cortex-M7芯片带有D-Cache。
CPU读取内存时,数据可能先被缓存到Cache中。
假设DMA接收数据:
DMA将新数据写入RAM
但CPU再次访问同一地址时,可能仍然读取Cache中的旧数据。
结果表现为:
- DMA明明完成了,数组内容却没有更新;
- 调试器看到的数据和程序读取的数据不同;
- 偶尔正常、偶尔异常;
- 关闭D-Cache后问题消失。
DMA接收时
DMA写内存后,CPU读取前通常需要使对应Cache行失效:
SCB_InvalidateDCache_by_Addr(
(uint32_t *)rx_buffer,
buffer_size);
DMA发送时
CPU修改内存后,DMA读取前通常需要将Cache中的数据清理到RAM:
SCB_CleanDCache_by_Addr(
(uint32_t *)tx_buffer,
buffer_size);
还需要注意:
- 地址按Cache Line对齐;
- 长度覆盖完整Cache Line;
- 缓冲区不要与其他变量共享同一Cache Line;
- 可将DMA缓冲区放入非缓存区域;
- 可使用MPU设置内存属性。
不同STM32系列和内存区域的Cache行为不同,应结合具体芯片手册设计。
三十一、DMA为什么可能产生总线竞争
CPU和DMA都需要访问存储器和外设总线。
例如:
CPU读取Flash
CPU访问SRAM
DMA读取外设
DMA写入SRAM
另一个DMA读取SRAM
这些操作可能同时请求总线。
总线矩阵或仲裁器会决定访问顺序。
因此DMA并不是“完全免费”的。
大量DMA传输可能带来:
- SRAM总线占用;
- CPU访问内存延迟增加;
- 多DMA之间竞争;
- 外设FIFO溢出;
- 实时任务抖动。
不过在绝大多数普通嵌入式项目中,合理使用DMA仍然比CPU逐字节搬运更高效。
三十二、DMA FIFO和Burst有什么作用
部分高性能STM32 DMA支持:
- FIFO;
- Burst传输。
1. FIFO
DMA可以先将数据暂存在内部FIFO,再成批访问总线。
作用包括:
- 缓冲外设和内存速度差异;
- 支持不同数据宽度;
- 减少总线访问次数;
- 提升突发传输效率。
2. Burst
DMA可以一次连续传输多个数据:
Single
INCR4
INCR8
INCR16
例如INCR4表示一次突发传输4个数据单元。
适合:
- 高带宽数据;
- 存储器批量传输;
- 高速外设;
- 音视频数据。
普通低速UART通常不需要复杂Burst配置。
三十三、DMA常见标志位
DMA通常包含以下事件:
传输完成 Transfer Complete
半传输 Half Transfer
传输错误 Transfer Error
FIFO错误 FIFO Error
直接模式错误 Direct Mode Error
程序不能只处理完成回调,也应该关注错误。
例如:
void HAL_DMA_ErrorCallback(
DMA_HandleTypeDef *hdma)
{
dma_error_flag = 1U;
}
对于可靠性要求高的系统,还需要记录:
- 哪个DMA出错;
- 错误代码;
- 当前剩余数量;
- 外设状态;
- 是否需要复位DMA;
- 是否需要复位外设。
三十四、DMA剩余传输数量有什么用
DMA内部通常会保存剩余传输数量。
HAL中可以读取:
uint32_t remaining;
remaining = __HAL_DMA_GET_COUNTER(
huart1.hdmarx);
如果总缓冲区长度为256:
已接收数量 = 256 - 剩余数量
例如:
剩余数量 = 200
已接收数量 = 256 - 200 = 56字节
这个机制常用于:
- UART空闲中断;
- 环形缓冲区;
- 当前DMA写指针计算;
- 数据流位置判断。
三十五、DMA环形缓冲区如何理解
假设DMA循环接收数组为:
#define RX_BUFFER_SIZE 128U
uint8_t rx_buffer[RX_BUFFER_SIZE];
DMA不断循环写入:
0 → 1 → 2 → …… → 127 → 0 → 1……
CPU需要维护自己的读取位置:
DMA写指针
CPU读指针
当两者不相等时,说明有新数据。
基本逻辑:
DMA负责写入
CPU负责读取
读指针追赶写指针
如果写指针追上读指针,可能意味着缓冲区已满,旧数据被覆盖。
三十六、DMA与FreeRTOS如何配合
在FreeRTOS中,不建议在DMA完成回调里执行复杂处理。
更合理的结构是:
DMA完成中断
↓
释放信号量/发送任务通知
↓
数据处理任务被唤醒
↓
任务中处理数据
例如:
void HAL_UART_RxCpltCallback(
UART_HandleTypeDef *huart)
{
BaseType_t higher_priority_task_woken =
pdFALSE;
if (huart->Instance == USART1)
{
vTaskNotifyGiveFromISR(
uart_task_handle,
&higher_priority_task_woken);
portYIELD_FROM_ISR(
higher_priority_task_woken);
}
}
任务中等待:
void UART_ProcessTask(void *argument)
{
for (;;)
{
ulTaskNotifyTake(
pdTRUE,
portMAX_DELAY);
UART_ProcessData(
uart_rx_buffer,
UART_RX_LENGTH);
}
}
这样可以让中断保持简短。
三十七、DMA使用中最常见的配置错误
1. DMA时钟没有使能
例如:
__HAL_RCC_DMA1_CLK_ENABLE();
没有使能DMA时钟,DMA不会工作。
2. DMA通道或Stream选错
外设请求和DMA映射不匹配。
表现为:
外设正常工作
DMA完全没有数据
完成中断不触发
3. 传输方向错误
例如UART接收应为:
Peripheral to Memory
却配置为:
Memory to Peripheral
4. 数据宽度错误
ADC结果是16位,DMA却配置为8位,导致数据截断或数组错位。
5. 地址自增配置错误
UART数据寄存器地址如果错误地自增,DMA第一次传输后就会访问错误寄存器。
内存地址如果没有自增,所有数据都会覆盖在数组第一个元素中。
6. DMA长度与缓冲区不匹配
例如:
uint8_t buffer[64];
却启动:
HAL_UART_Receive_DMA(
&huart1,
buffer,
128);
DMA会写越界,破坏其他变量甚至导致HardFault。
7. 没有正确关联DMA句柄
HAL库通常需要:
__HAL_LINKDMA(
&huart1,
hdmarx,
hdma_usart1_rx);
如果句柄没有关联,HAL_UART_Receive_DMA可能无法正常管理DMA。
8. DMA中断没有配置
如果需要完成回调,需要:
- 配置NVIC;
- 设置优先级;
- 使能DMA中断;
- 在中断函数中调用HAL处理函数。
例如:
void DMA1_Channel5_IRQHandler(void)
{
HAL_DMA_IRQHandler(
&hdma_usart1_rx);
}
9. 中断函数名字错误
启动文件中的中断向量名称必须与代码一致。
如果名称写错,DMA硬件已经完成,但程序不会进入正确的处理函数。
10. DMA缓冲区生命周期错误
例如在函数栈中定义:
void UART_Send(void)
{
uint8_t temp[32];
HAL_UART_Transmit_DMA(
&huart1,
temp,
sizeof(temp));
}
函数返回后,temp所在栈空间可能被其他函数复用,但DMA可能仍在读取。
DMA缓冲区应该在传输完成前保持有效。
可以使用:
- 全局数组;
- 静态数组;
- 动态内存并延迟释放;
- 专用缓冲池。
三十八、如何判断DMA是否真的在工作
可以按照下面的步骤排查。
第一步:检查外设本身是否正常
先用轮询方式验证:
HAL_UART_Receive();
HAL_ADC_PollForConversion();
HAL_SPI_Transmit();
如果轮询都不正常,先不要怀疑DMA。
第二步:检查DMA时钟
确认DMA控制器时钟已开启。
第三步:检查请求映射
确认:
外设
DMA控制器
Channel或Stream
Request编号
全部匹配。
第四步:检查DMA计数器
读取剩余数量:
__HAL_DMA_GET_COUNTER();
如果计数器完全不变化,说明DMA请求可能没有到达。
第五步:检查外设DMA使能位
外设中通常存在DMA发送或接收使能位。
例如UART需要使能:
DMAR
DMAT
HAL函数一般会自动配置,但手写寄存器时必须检查。
第六步:检查DMA中断标志
判断:
- 传输完成标志是否置位;
- 是否发生传输错误;
- 是否发生FIFO错误;
- 中断是否被NVIC屏蔽。
第七步:检查内存数组
观察:
- 数据是否写入;
- 是否只写入第一个元素;
- 是否发生错位;
- 是否被其他程序覆盖;
- 数据宽度是否正确。
三十九、DMA适合哪些场景
DMA非常适合以下应用。
1. ADC连续采样
ADC → DMA → 数组 → 滤波/FFT
2. 串口高速收发
UART → DMA → 环形缓冲区
3. SPI大批量传输
帧缓冲区 → DMA → SPI → LCD
4. I²S音频
I²S → DMA → 双缓冲 → 音频处理
5. DAC连续输出
波形数组 → DMA → DAC
6. 定时器捕获
输入捕获寄存器 → DMA → 时间戳数组
7. 摄像头或高速接口
DCMI/外设 → DMA → 图像缓冲区
四十、哪些场景不一定需要DMA
DMA并不是所有传输的最佳方案。
以下场景可以优先考虑轮询或中断:
- 只发送几个字节;
- 数据发生频率很低;
- 初始化时读取一次寄存器;
- 调试串口偶尔打印;
- 系统资源很少;
- DMA通道已经被重要外设占用;
- 传输长度不固定且协议逻辑非常简单;
- 数据处理本身比搬运更耗时。
例如I²C读取一个2字节温度寄存器,使用阻塞方式可能更简单:
HAL_I2C_Mem_Read(
&hi2c1,
device_address,
register_address,
I2C_MEMADD_SIZE_8BIT,
data,
2,
100);
不必为了“看起来高级”而强行使用DMA。
四十一、DMA的真正价值是什么
很多人认为DMA只是让数据传输更快。
实际上,DMA更重要的价值是:
1. 释放CPU时间
CPU可以处理:
- 控制算法;
- 通信协议;
- UI;
- 状态机;
- 故障判断;
- FreeRTOS任务。
2. 降低中断频率
从逐字节中断变成分块完成中断。
3. 实现外设和CPU并行工作
例如:
DMA采集下一批ADC数据
CPU处理上一批ADC数据
4. 保证连续数据流
适合:
- 音频;
- 波形采样;
- 高速通信;
- 连续显示刷新。
5. 提高系统实时性
CPU不需要被大量低价值搬运工作频繁打断。
四十二、一个完整的DMA系统设计思路
使用DMA前,建议回答以下问题:
数据从哪里来?
数据要到哪里去?
每次传多少?
数据宽度是多少?
传输是单次还是连续?
什么时候认为一帧结束?
缓冲区多大?
CPU处理速度能否跟上?
数据是否可能被覆盖?
完成后怎样通知任务?
是否需要半传输?
是否需要双缓冲?
是否存在Cache?
错误后如何恢复?
典型结构如下:
外设产生数据
↓
DMA写入缓冲区
↓
半传输/全传输/空闲事件
↓
中断中设置标志或通知任务
↓
任务处理数据
↓
完成协议解析或算法运算
四十三、面试中如何回答DMA解决了什么问题
如果面试官问:
STM32 DMA到底解决了什么问题?
可以这样回答:
DMA主要解决外设和内存之间大量重复数据搬运占用CPU的问题。
在没有DMA时,CPU需要通过轮询或中断,
逐个读取外设数据寄存器并写入内存,
或者从内存读取数据再写入外设。
使用DMA后,CPU只需要提前配置源地址、目标地址、
传输方向、数据宽度和传输数量,
DMA控制器就可以响应外设请求自动完成数据搬运。
传输完成后,DMA再通过中断通知CPU处理整批数据。
DMA可以明显减少逐字节中断和CPU搬运开销,
特别适合ADC连续采样、UART高速通信、
SPI批量传输、I2S音频和DAC波形输出。
但DMA并不是完全不需要CPU,
CPU仍然负责配置、启动、缓冲区管理、
完成回调、错误处理和业务数据处理。
四十四、总结
DMA的本质不是一个新的通信协议,而是一个硬件数据搬运控制器。
没有DMA时:
外设
↓
CPU读取
↓
CPU写入内存
使用DMA后:
外设
↓
DMA自动搬运
↓
内存
DMA主要解决以下问题:
CPU逐字节搬运数据占用时间;
高频中断导致系统负担过重;
连续高速数据难以稳定接收;
外设工作时CPU无法同时处理其他任务;
大量数据传输效率低。
DMA并不能自动解决:
协议解析;
帧头帧尾判断;
CRC校验;
缓冲区溢出;
CPU处理速度不足;
Cache一致性;
错误恢复;
业务逻辑。
轮询、中断和DMA可以简单总结为:
轮询:CPU一直等待并搬运数据;
中断:数据到达后通知CPU搬运;
DMA:硬件自动搬运,完成后再通知CPU。
最后用一句话总结:
STM32 DMA真正解决的不是“数据怎么计算”,而是“数据由谁搬运”的问题,它让CPU从重复的数据复制工作中释放出来,把时间用于更重要的控制、通信和算法处理。
更多推荐



所有评论(0)