STM32使用DMA后数据异常?可能不是DMA配置问题:从缓存、变量类型、时序到外设状态全面排查

在STM32项目开发中,DMA几乎是绕不开的功能。
UART接收、ADC采样、SPI收发、I²C数据搬运、定时器PWM更新等场景,都可以通过DMA降低CPU占用,提高系统效率。
但实际调试时,经常遇到一些很“诡异”的问题:
- DMA明明启动成功了,但数组中的数据不对;
- 第一次接收正常,第二次开始异常;
- DMA接收到的数据总是少几个字节;
- ADC使用DMA后采样值跳变;
- UART DMA接收偶尔出现旧数据;
- SPI DMA读回来的数据全部错位;
- Debug单步正常,全速运行却异常;
- DMA完成中断已经进入,但数据仍然不是预期值;
- 修改DMA配置很多次,问题依旧存在。
这时候很多人的第一反应是:
“是不是DMA配置错了?”
实际上,在大量STM32工程中,DMA本身并没有配置错误,真正的问题可能出在缓存、变量类型、内存区域、外设状态、数据宽度、启动顺序、时序以及并发访问上。
本文以STM32常见DMA故障为主线,系统讲解:
- DMA到底是怎么搬运数据的;
- 为什么DMA配置正确,数据仍然可能异常;
- 最容易忽略的10类问题;
- UART、ADC、SPI使用DMA时分别需要注意什么;
- STM32F4和STM32H7等不同平台有哪些差异;
- 如何建立一套完整的DMA故障排查流程。
一、先理解DMA到底在做什么
DMA全称:
Direct Memory Access,直接存储器访问。
它最大的作用是:
在不需要CPU逐个搬运数据的情况下,实现外设和内存之间的数据传输。
例如UART接收。
如果不用DMA,CPU需要不断读取USART的数据寄存器:
while (HAL_UART_Receive(&huart1, &data, 1, 100) == HAL_OK)
{
rx_buf[index++] = data;
}
每收到一个字节,CPU都要参与。
如果使用DMA:
HAL_UART_Receive_DMA(&huart1, rx_buf, 100);
后续UART收到的数据可以由DMA自动搬运到:
rx_buf[100]
CPU只需要等待DMA传输完成中断。
整个流程大致为:
UART接收到数据
↓
UART数据寄存器
↓
DMA检测到外设DMA请求
↓
DMA读取UART数据寄存器
↓
DMA写入RAM
↓
传输计数减1
↓
全部完成
↓
DMA传输完成中断
看起来DMA只是“搬运工”。
所以有一个非常重要的调试思路:
DMA数据异常,不一定意味着DMA控制器有问题。
数据从外设到应用程序,需要经过很多环节:
外设信号
↓
外设寄存器
↓
DMA请求
↓
DMA控制器
↓
系统总线
↓
RAM
↓
Cache
↓
CPU读取
↓
应用程序处理
任何一个环节出问题,最终看到的结果都可能表现为:
DMA数据异常
二、DMA配置正确,为什么数据还是错?
假设UART DMA配置如下:
uint8_t rx_buf[100];
HAL_UART_Receive_DMA(&huart1, rx_buf, 100);
DMA已经正常开启。
DMA中断也能进入:
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance == USART1)
{
// DMA完成
}
}
但打印:
printf("%s\r\n", rx_buf);
发现:
ABCDE??????
或者:
ABCDEABCDE
很多人会开始检查:
DMA Stream
DMA Channel
Priority
Mode
FIFO
Memory Increment
Peripheral Increment
这些当然要检查。
但如果DMA中断能正常完成,DMA计数也符合预期,那么问题很可能已经不在DMA配置本身。
下面重点介绍最常见的问题。
三、问题一:DMA缓存和CPU Cache不一致
这个问题在STM32H7、STM32F7等带D-Cache的MCU上非常典型。
例如:
uint8_t rx_buf[1024];
HAL_UART_Receive_DMA(&huart1, rx_buf, sizeof(rx_buf));
DMA已经把新数据写入RAM:
RAM:
AA BB CC DD
但CPU读取数据时,可能读取的是Cache中的旧数据:
D-Cache:
11 22 33 44
于是出现一个非常诡异的现象:
DMA看起来已经接收成功
DMA完成中断正常
RAM实际已经更新
但是程序读取到的数据还是旧的
原因是:
DMA直接操作内存,而CPU可能操作Cache。
可以把STM32H7中的情况简单理解成:
┌──────────────┐
CPU ─────────────→│ D-Cache │
└──────┬───────┘
│
↓
SRAM
↑
│
DMA ────────────────────┘
DMA不会自动帮你刷新CPU缓存。
1. DMA接收后Invalidate Cache
例如:
HAL_UART_Receive_DMA(&huart1, rx_buf, 256);
DMA完成后:
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, 256);
然后CPU再读取:
ProcessData(rx_buf);
完整示例:
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance == USART1)
{
SCB_InvalidateDCache_by_Addr(
(uint32_t *)rx_buf,
sizeof(rx_buf)
);
ProcessData(rx_buf);
}
}
2. DMA发送前Clean Cache
如果是DMA发送:
HAL_UART_Transmit_DMA(&huart1, tx_buf, length);
CPU先修改:
tx_buf[0] = 0x12;
tx_buf[1] = 0x34;
新数据可能只存在Cache中,而还没有真正写入RAM。
这时候DMA直接读取RAM,就可能发送旧数据。
因此应该:
SCB_CleanDCache_by_Addr(
(uint32_t *)tx_buf,
length
);
HAL_UART_Transmit_DMA(
&huart1,
tx_buf,
length
);
可以记住一个简单规则:
CPU → DMA
发送前:Clean
DMA → CPU
接收后:Invalidate
这是STM32H7 DMA异常排查时必须优先检查的一项。
四、问题二:DMA Buffer放在了DMA无法访问的内存区域
这是STM32H7项目中特别容易踩的坑。
有些STM32内部存在多块RAM,例如:
ITCM
DTCM
AXI SRAM
SRAM1
SRAM2
SRAM3
SRAM4
它们并不是所有DMA都可以访问。
例如某些DMA无法直接访问DTCM。
假设:
uint8_t adc_buffer[1024];
链接脚本把这个变量放到了:
DTCM RAM
CPU访问它完全正常。
但DMA不一定能访问。
结果可能表现为:
DMA没有写进去
DMA数据全部为0
DMA数据不更新
DMA异常
因此排查STM32H7 DMA问题时,一定要检查变量地址。
例如:
printf("buffer addr = 0x%08X\r\n",
(unsigned int)adc_buffer);
如果发现地址位于DMA不可达区域,就需要调整。
例如:
__attribute__((section(".RAM_D2")))
uint8_t adc_buffer[1024];
然后在链接脚本中把:
.RAM_D2
映射到DMA可以访问的SRAM区域。
所以不要只问:
DMA配置对不对?
还要问:
DMA能不能访问这个buffer?
五、问题三:DMA数据宽度和变量类型不匹配
这是非常经典的问题。
假设ADC配置为:
Peripheral Data Width = Half Word
Memory Data Width = Half Word
也就是16bit。
但程序定义:
uint8_t adc_buffer[100];
这就会产生问题。
DMA每次搬运:
16 bit
而数组元素只有:
8 bit
最终数据排列可能和你想象的不一样。
正确做法通常是:
uint16_t adc_buffer[100];
例如:
HAL_ADC_Start_DMA(
&hadc1,
(uint32_t *)adc_buffer,
100
);
DMA宽度对应关系:
| DMA配置 | 建议变量 |
|---|---|
| Byte | uint8_t |
| Half Word | uint16_t |
| Word | uint32_t |
UART通常:
Byte
ADC通常:
Half Word
某些32位外设:
Word
如果数据宽度不一致,可能出现:
数据错位
高低字节异常
数组间隔异常
读取值异常
六、问题四:Memory Increment没有打开
假设:
uint8_t rx_buf[100];
正常情况下,DMA应该:
第1字节 → rx_buf[0]
第2字节 → rx_buf[1]
第3字节 → rx_buf[2]
第4字节 → rx_buf[3]
因此:
Memory Increment = Enable
如果关闭:
Memory Increment = Disable
DMA可能一直往同一个地址写:
第1字节 → rx_buf[0]
第2字节 → rx_buf[0]
第3字节 → rx_buf[0]
第4字节 → rx_buf[0]
最终结果:
rx_buf[0] = 最后一个数据
其余内容没有正确更新。
所以DMA参数至少要确认:
Peripheral Increment
Memory Increment
Peripheral Data Alignment
Memory Data Alignment
Mode
Priority
以UART RX为例,常见配置:
Direction Peripheral to Memory
Peripheral Increment Disable
Memory Increment Enable
Peripheral Data Width Byte
Memory Data Width Byte
Mode Normal / Circular
七、问题五:Normal模式和Circular模式理解错误
DMA常见两个工作模式:
Normal
Circular
Normal模式
例如:
HAL_UART_Receive_DMA(&huart1, rx_buf, 100);
DMA收到100字节后:
DMA停止
如果希望再次接收,需要重新调用:
HAL_UART_Receive_DMA(
&huart1,
rx_buf,
100
);
很多人遇到:
第一次DMA正常
第二次没有数据
第一反应是DMA坏了。
实际上可能只是DMA使用的是:
Normal模式
而没有重新启动。
Circular模式
Circular模式流程:
buffer[0]
↓
buffer[1]
↓
...
↓
buffer[99]
↓
buffer[0]
↓
继续循环
适用于:
ADC连续采样
UART连续数据流
音频采样
高速传感器数据
但Circular模式也容易产生另外一个问题:
CPU正在处理Buffer时,DMA已经开始重新覆盖Buffer。
例如:
DMA:
正在写第0~20字节
CPU:
还在处理上一次第0~100字节
这就出现数据竞争。
解决方案可以考虑:
双缓冲
Half Transfer中断
Transfer Complete中断
Ping-Pong Buffer
Ring Buffer
八、问题六:DMA完成不等于外设传输完成
这是非常重要的一点。
以UART TX DMA为例:
HAL_UART_Transmit_DMA(
&huart1,
tx_buf,
100
);
DMA完成意味着:
100字节已经从RAM搬到了UART
但不一定意味着:
100字节已经全部从TX引脚发送出去
因为UART内部还有:
TDR
Shift Register
所以实际流程:
RAM
↓
DMA
↓
UART TDR
↓
UART Shift Register
↓
TX Pin
DMA完成时,最后几个bit可能还没有真正发送完成。
这个问题在RS485中尤其明显。
假设DE控制:
RS485_DE_HIGH();
HAL_UART_Transmit_DMA(
&huart1,
tx_buf,
len
);
然后DMA完成立即执行:
RS485_DE_LOW();
最后一个字节可能直接被截断。
正确思路应该是:
等待UART TC
Transmit Complete
再切换DE。
例如:
while(__HAL_UART_GET_FLAG(
&huart1,
UART_FLAG_TC) == RESET)
{
}
RS485_DE_LOW();
所以一定要区分:
DMA Transfer Complete
和:
Peripheral Transfer Complete
它们不是一个概念。
九、问题七:启动DMA前外设残留旧数据
UART DMA中非常容易遇到。
例如上一帧通信出现异常:
ORE
RXNE
FE
NE
UART内部状态没有清理干净。
此时重新启动:
HAL_UART_Receive_DMA(...)
第一字节可能就是旧数据。
于是出现:
数据整体偏移一个字节
帧头错误
第一字节异常
偶发乱码
调试时可以重点检查UART状态寄存器。
对于部分STM32系列,可以在重新启动DMA前清除错误标志。
例如HAL中根据芯片系列处理:
__HAL_UART_CLEAR_OREFLAG(&huart1);
同时还需要考虑:
RXNE残留
ORE溢出
IDLE状态
DMA NDTR残留
特别是使用:
UART + DMA + IDLE
接收不定长数据时,这类问题非常常见。
十、问题八:UART IDLE + DMA计算长度错误
这是项目中非常高频的BUG。
假设:
uint8_t rx_buf[256];
HAL_UART_Receive_DMA(
&huart1,
rx_buf,
256
);
UART收到:
20 Byte
随后产生IDLE中断。
此时DMA剩余计数:
NDTR = 236
那么实际接收长度:
256 - 236 = 20
代码:
uint16_t len;
len = sizeof(rx_buf)
- __HAL_DMA_GET_COUNTER(huart1.hdmarx);
然后:
ProcessData(rx_buf, len);
如果长度计算错误,例如:
len = __HAL_DMA_GET_COUNTER(...);
那就会把:
236
当成实际长度。
后面的数据当然全部异常。
所以记住:
实际接收长度
=
Buffer总长度
-
DMA剩余长度
即:
recv_len =
BUFFER_SIZE
- __HAL_DMA_GET_COUNTER(hdma);
十一、问题九:Buffer被CPU和DMA同时修改
假设:
uint8_t tx_buf[100];
程序:
sprintf((char *)tx_buf,
"Temperature:%d",
temp);
HAL_UART_Transmit_DMA(
&huart1,
tx_buf,
strlen((char *)tx_buf)
);
DMA开始发送。
但DMA还没有发送结束,程序又执行:
sprintf((char *)tx_buf,
"Voltage:%d",
voltage);
那么DMA发送过程中,Buffer内容突然变了。
最终串口可能收到:
TemperatVoltage:12
或者其他混乱数据。
原因非常简单:
CPU修改Buffer
↓
同时
↑
DMA读取Buffer
产生竞争。
所以DMA发送Buffer在传输完成前,原则上:
不要修改。
可以采用:
tx_buf1
tx_buf2
双Buffer。
例如:
uint8_t tx_buf[2][256];
volatile uint8_t current_buf;
形成:
CPU写Buffer0
DMA发Buffer1
下一轮
CPU写Buffer1
DMA发Buffer0
这也是典型的Ping-Pong Buffer。
十二、问题十:栈上的局部变量被DMA使用
这是一个非常隐蔽的问题。
错误示例:
void SendData(void)
{
uint8_t buf[100];
sprintf((char *)buf,
"Hello STM32");
HAL_UART_Transmit_DMA(
&huart1,
buf,
100
);
}
调用:
SendData();
函数返回后:
buf生命周期结束
但是DMA可能仍然正在发送。
随后其他函数继续使用栈空间,把buf覆盖。
DMA再读取:
buf
读到的已经不是原来的数据。
于是出现:
DMA发送乱码
偶发错误
Debug正常Release异常
正确方式之一:
static uint8_t buf[100];
例如:
void SendData(void)
{
static uint8_t buf[100];
sprintf((char *)buf,
"Hello STM32");
HAL_UART_Transmit_DMA(
&huart1,
buf,
strlen((char *)buf)
);
}
或者定义为全局变量:
uint8_t uart_tx_buf[100];
这类问题非常值得检查。
十三、为什么Debug正常,全速运行异常?
这是DMA项目中特别典型的现象。
例如:
单步执行:正常
Run执行:错误
很多人会怀疑:
DMA不稳定
MCU有问题
实际上这种现象通常强烈暗示:
时序问题
竞争条件
缓存问题
变量生命周期
中断优先级
因为Debug单步实际上人为加入了很多延时。
比如正常运行:
CPU写Buffer
↓
启动DMA
↓
CPU立即修改Buffer
DMA还没有读完。
但单步调试:
CPU写Buffer
↓
暂停
↓
DMA已经传完
↓
CPU继续修改Buffer
问题被“隐藏”了。
所以:
Debug正常、Run异常,不要轻易认为是编译器问题,应优先检查时序和并发访问。
十四、ADC DMA数据异常怎么排查?
ADC + DMA是STM32项目最常见的组合之一。
例如:
uint16_t adc_buf[100];
HAL_ADC_Start_DMA(
&hadc1,
(uint32_t *)adc_buf,
100
);
如果ADC数据异常,可以从下面几个方向排查。
1. ADC采样时间是否太短
ADC输入并不是理想电压源。
如果信号源阻抗比较大,而ADC采样时间太短:
内部采样电容
无法充分充电
最终ADC值偏低或者不稳定。
例如:
ADC_SAMPLETIME_3CYCLES
可能改成:
ADC_SAMPLETIME_56CYCLES
或者更长。
DMA只是把ADC结果搬走。
真正数据错误可能来自:
ADC采样阶段
而不是DMA。
2. ADC扫描顺序是否正确
例如:
ADC Channel 0
ADC Channel 1
ADC Channel 2
DMA结果:
adc_buf[0]
adc_buf[1]
adc_buf[2]
但如果Rank配置错误:
Rank1 = CH2
Rank2 = CH0
Rank3 = CH1
程序却按照:
adc_buf[0] = CH0
adc_buf[1] = CH1
adc_buf[2] = CH2
理解。
那你会感觉:
DMA数据全错了
其实DMA完全正常,只是ADC通道顺序错了。
3. ADC DMA是否使用Circular
如果想持续采样:
Continuous Conversion = Enable
DMA Continuous Requests = Enable
DMA Mode = Circular
如果DMA配置为Normal:
采样一轮以后停止
就会出现:
第一次ADC正常
之后ADC值一直不变
这也是非常典型的误判。
十五、SPI DMA数据异常怎么排查?
SPI DMA通常比UART DMA更复杂。
因为SPI是全双工接口。
发送一个字节的同时:
一定会接收一个字节
例如读取SPI Flash:
发送命令
发送地址
发送Dummy
接收数据
实际SPI过程:
MOSI → 命令
MISO ← Dummy
MOSI → 地址
MISO ← Dummy
MOSI → Dummy
MISO ← Flash Data
所以SPI DMA接收Buffer中,前面可能天然存在:
Dummy Byte
例如:
RX:
00 00 00 12 34 56 78
真正有效数据:
12 34 56 78
如果直接从:
rx_buf[0]
开始解析,就会感觉:
DMA错位
实际上是SPI协议本身导致的。
另外SPI DMA还要重点检查:
CS拉低时间
CS拉高时机
TX DMA
RX DMA
BSY标志
FIFO
SPI Mode
CPOL
CPHA
尤其是:
DMA完成以后立即CS拉高
也可能在最后一个bit还没发完时破坏传输。
因此应确认:
SPI BSY == 0
然后再:
CS = HIGH
十六、UART DMA偶尔少字节是什么原因?
例如发送:
AA 55 01 02 03 04 05 06
但DMA偶尔接收到:
55 01 02 03 04 05 06
少了:
AA
重点检查:
DMA启动是否晚于数据到达
流程如果变成:
对方开始发送
↓
AA进入UART
↓
此时DMA还没开启
↓
程序开启DMA
↓
后续55 01 02...
自然丢失第一个字节。
正确思路:
先开启DMA接收
↓
再允许对方发送
通信协议设计中可以使用:
握手
READY信号
ACK
命令-响应
确保接收方已经准备好。
十七、DMA中断优先级也可能影响数据
在RTOS项目中尤其明显。
例如:
UART DMA IRQ
ADC DMA IRQ
Ethernet IRQ
USB IRQ
FreeRTOS Task
同时存在。
如果DMA中断优先级不合理,可能造成:
处理延迟
Buffer覆盖
事件响应不及时
例如Circular DMA:
Half Transfer IRQ
到来以后CPU应该及时处理前半Buffer。
如果一个高优先级中断长期占用CPU:
DMA继续写
最终前半Buffer可能在处理之前已经再次被覆盖。
所以RTOS系统中不仅要考虑:
DMA Priority
还要考虑:
NVIC Interrupt Priority
注意这两个不是一回事。
十八、DMA Priority和NVIC Priority不要混淆
CubeMX里面经常看到:
DMA Priority
Low
Medium
High
Very High
它表示:
多个DMA Stream同时请求总线时,谁优先。
而:
NVIC Priority
表示:
多个CPU中断同时发生时,CPU优先处理谁。
例如:
DMA Priority = Very High
不代表:
DMA中断优先级最高
这两个概念完全不同。
十九、volatile能不能解决DMA数据异常?
很多人一看到DMA变量就写:
volatile uint8_t rx_buf[100];
但需要注意:
volatile不是DMA问题的万能解决方案。
volatile主要告诉编译器:
不要假设这个变量不会改变
每次需要时重新读取
适合:
volatile uint8_t dma_done;
例如:
volatile uint8_t dma_done = 0;
void HAL_UART_RxCpltCallback(...)
{
dma_done = 1;
}
主循环:
if(dma_done)
{
dma_done = 0;
ProcessData();
}
但:
volatile
不能解决:
D-Cache一致性
Buffer越界
DMA不可访问内存
数据宽度错误
并发修改Buffer
UART时序错误
所以不要遇到DMA异常就随便加volatile。
二十、一个典型错误案例
假设STM32通过UART DMA接收设备数据:
uint8_t rx_buf[256];
HAL_UART_Receive_DMA(
&huart1,
rx_buf,
sizeof(rx_buf)
);
IDLE中断:
void USART1_IRQHandler(void)
{
if(__HAL_UART_GET_FLAG(
&huart1,
UART_FLAG_IDLE))
{
__HAL_UART_CLEAR_IDLEFLAG(&huart1);
HAL_UART_DMAStop(&huart1);
uint16_t len =
sizeof(rx_buf)
- __HAL_DMA_GET_COUNTER(
huart1.hdmarx);
ProcessData(rx_buf, len);
HAL_UART_Receive_DMA(
&huart1,
rx_buf,
sizeof(rx_buf)
);
}
HAL_UART_IRQHandler(&huart1);
}
看起来似乎没有问题。
但这里就可能隐藏多个风险。
风险一
先执行:
HAL_UART_DMAStop()
然后才读取NDTR。
某些情况下计数状态可能变化。
更稳妥的思路是:
先读取NDTR
再停止DMA
风险二
执行:
ProcessData()
时间太长。
这时候DMA已经停止。
如果串口继续收到数据:
数据可能丢失
风险三
处理完以后才重新启动DMA:
HAL_UART_Receive_DMA()
中间存在接收空窗期。
更好的架构应该让DMA尽快恢复。
例如:
IDLE
↓
计算长度
↓
迅速切换Buffer/重新开启DMA
↓
后续任务慢慢解析
而不是:
停止DMA
↓
复杂协议解析
↓
打印日志
↓
CRC计算
↓
重新开启DMA
二十一、推荐UART DMA + IDLE的设计方式
对于不定长协议,例如:
Modbus RTU
自定义串口协议
GPS
4G模块
传感器数据
推荐:
UART
↓
DMA Circular / ReceiveToIdle
↓
IDLE事件
↓
获取有效长度
↓
复制/切换Buffer
↓
消息队列
↓
协议解析Task
HAL库中很多STM32系列已经支持:
HAL_UARTEx_ReceiveToIdle_DMA()
例如:
HAL_UARTEx_ReceiveToIdle_DMA(
&huart1,
rx_buf,
RX_BUF_SIZE
);
回调:
void HAL_UARTEx_RxEventCallback(
UART_HandleTypeDef *huart,
uint16_t Size)
{
if(huart->Instance == USART1)
{
ProcessData(
rx_buf,
Size
);
}
}
这种方式通常比自己手动处理IDLE更加方便。
当然,具体行为还要结合STM32系列和HAL版本确认。
二十二、DMA异常推荐排查顺序
遇到DMA数据异常,我推荐不要一开始就不断修改CubeMX。
可以按照下面的顺序排查。
第一步:先确认源数据是不是正确
UART:
示波器
逻辑分析仪
串口抓包
SPI:
MOSI
MISO
CLK
CS
I²C:
SCL
SDA
ACK
Address
ADC:
万用表
示波器
如果外部信号本身就错:
DMA当然不可能得到正确数据
第二步:直接查看外设寄存器
例如UART:
RDR
ISR
SR
DR
ADC:
DR
ISR
SPI:
DR
SR
确认:
外设自己到底有没有拿到正确数据
如果外设寄存器就错:
重点查外设
如果外设寄存器正确,但RAM错误:
重点查DMA
第三步:检查DMA NDTR
DMA中很重要的寄存器:
NDTR
表示:
还剩多少数据没有传输
如果DMA长度:
100
现在:
NDTR = 60
说明:
已经搬运40个
如果NDTR完全不变:
100
说明DMA可能根本没收到请求。
重点检查:
外设DMA Request
DMA Enable
Channel/Request
DMA mapping
二十三、第四步:查看Buffer实际内存
不要只看打印结果。
直接使用IDE的Memory窗口查看:
rx_buf地址
例如:
0x20001000
观察:
DMA运行前
DMA运行中
DMA完成后
如果RAM实际内容正确:
但是程序打印错误
那么问题可能在:
Cache
字符串结束符
printf
解析函数
Buffer越界
而不是DMA。
这是一个非常实用的调试方法。
二十四、第五步:检查Buffer是否越界
例如:
uint8_t rx_buf[100];
却启动:
HAL_UART_Receive_DMA(
&huart1,
rx_buf,
200
);
DMA会继续往后写。
结果可能覆盖:
其他变量
任务栈
控制变量
指针
RTOS对象
最终表现可能非常离谱:
DMA异常
程序跑飞
HardFault
任务死掉
变量莫名改变
实际上就是:
Memory Corruption
所以一定要确认:
DMA Length <= Buffer Size
二十五、第六步:检查数据宽度
确认:
Peripheral Data Width
Memory Data Width
变量类型
例如ADC:
Half Word
Half Word
uint16_t
UART:
Byte
Byte
uint8_t
二十六、第七步:检查内存位置
尤其是STM32H7:
Buffer在哪个RAM?
DMA能否访问?
使用:
printf("%p\r\n", rx_buf);
或者IDE:
Watch
Memory
Map File
确定变量地址。
二十七、第八步:检查Cache
如果芯片存在:
D-Cache
一定检查:
Clean
Invalidate
32-byte Cache Line
地址对齐
尤其是STM32H7。
很多看似:
DMA随机异常
实际上都是Cache一致性问题。
二十八、第九步:检查CPU是否同时访问Buffer
问自己三个问题:
DMA正在写Buffer时,
CPU会不会读?
DMA正在读Buffer时,
CPU会不会改?
中断和Task是否同时访问?
如果答案是:
会
就可能产生竞争。
解决方式:
双Buffer
环形Buffer
锁
状态机
消息队列
临界区
二十九、第十步:检查外设时序
例如RS485:
DE什么时候拉高?
DE什么时候拉低?
SPI:
CS什么时候拉低?
什么时候拉高?
ADC:
Trigger频率是多少?
采样时间够不够?
UART:
DMA有没有提前打开?
IDLE有没有清除?
ORE有没有出现?
DMA只是传输数据。
外设时序不对,DMA一样只能搬运错误结果。
三十、一个完整DMA故障定位思维导图
可以按照下面这条路线理解:
DMA数据异常
│
┌───────────┴───────────┐
│ │
源数据错误 源数据正确
│ │
查硬件 ↓
查协议 查外设寄存器
│
┌───────────┴───────────┐
│ │
寄存器错 寄存器对
│ │
查外设 ↓
查DMA
│
┌────────────────────────┼───────────────┐
│ │ │
NDTR Buffer Memory
│ │ │
DMA是否工作 是否越界 DMA可访问?
│
↓
Cache
│
↓
并发
│
↓
时序
不要把所有问题都压缩成:
DMA错了
三十一、STM32 DMA常见问题速查表
| 现象 | 优先检查 |
|---|---|
| DMA完全没数据 | DMA Request、Channel、Stream、Enable |
| 第一次正常第二次不工作 | Normal模式未重新启动 |
| 数据一直不变化 | DMA未重启、Cache、ADC未连续转换 |
| 数据整体错位 | UART残留数据、SPI Dummy Byte |
| 偶尔少第一个字节 | DMA启动太晚 |
| 最后一个字节错误 | UART TC、SPI BSY |
| DMA中断正常但数据旧 | D-Cache |
| Debug正常Run异常 | 时序、竞争、Buffer生命周期 |
| H7 DMA完全异常 | DMA不可访问DTCM |
| ADC DMA数值异常 | ADC采样时间、Rank、DMA宽度 |
| 数据重复 | Circular覆盖、解析逻辑错误 |
| 字符串后面乱码 | 没有\0 |
| DMA发送过程中乱码 | CPU同时修改TX Buffer |
| 接收大数据随机异常 | Buffer越界、处理速度不足 |
| RTOS下偶发异常 | 中断优先级、竞争、任务处理不及时 |
三十二、字符串DMA还有一个非常容易忽略的问题
假设UART DMA收到:
HELLO
Buffer:
uint8_t rx_buf[100];
DMA实际只写入:
H E L L O
也就是:
48 45 4C 4C 4F
DMA不会自动帮你补:
00
如果直接:
printf("%s\r\n", rx_buf);
printf会一直往后找:
'\0'
因此可能打印:
HELLOxxxxxxxxxxxx
然后你以为:
DMA后面的数据怎么全乱了?
实际上DMA根本没问题。
正确做法:
rx_buf[len] = '\0';
前提是Buffer多预留一个字节:
uint8_t rx_buf[RX_SIZE + 1];
例如:
if(len < RX_SIZE)
{
rx_buf[len] = '\0';
}
三十三、不要用printf判断所有DMA问题
printf本身可能带来新的问题。
例如:
printf("DMA RX:%s\r\n", rx_buf);
如果printf底层同样使用:
UART
甚至也是:
UART DMA
就可能发生冲突。
另外printf速度较慢。
115200bps情况下,大量打印日志会严重影响实时性。
所以调试高速DMA时建议结合:
IDE Watch
Memory窗口
逻辑分析仪
示波器
SWO
RTT
状态变量
错误计数器
而不是只依赖printf。
三十四、推荐增加DMA调试变量
实际工程中可以定义:
typedef struct
{
uint32_t rx_count;
uint32_t tx_count;
uint32_t dma_error;
uint32_t uart_error;
uint32_t idle_count;
uint32_t overflow_count;
} DMA_Debug_t;
DMA_Debug_t dma_debug;
例如:
void HAL_UART_RxCpltCallback(
UART_HandleTypeDef *huart)
{
dma_debug.rx_count++;
}
错误回调:
void HAL_UART_ErrorCallback(
UART_HandleTypeDef *huart)
{
dma_debug.uart_error++;
}
DMA错误:
dma_debug.dma_error++;
这样运行一段时间后,能够明显帮助判断:
到底是DMA没完成,
还是UART本身报错,
还是Buffer处理不及时。
比不停打印日志更加可靠。
三十五、工程中推荐的DMA设计原则
为了减少DMA随机异常,建议遵循下面几个原则。
1. DMA Buffer尽量使用全局变量或static
推荐:
static uint8_t uart_rx_buf[256];
不推荐:
void test(void)
{
uint8_t buf[256];
HAL_UART_Receive_DMA(
&huart1,
buf,
256
);
}
2. DMA进行过程中不要随意修改Buffer
TX:
DMA完成前CPU不要改
RX:
DMA正在写的时候CPU避免处理同一片区域
3. 高速数据推荐双Buffer
例如:
Buffer A ← DMA
Buffer B ← CPU
交换
Buffer B ← DMA
Buffer A ← CPU
4. 中断里面不要做复杂处理
不要:
void DMA_IRQHandler(void)
{
CRC_Calculate();
ParseProtocol();
printf();
SaveFlash();
}
推荐:
void DMA_IRQHandler(void)
{
dma_event = 1;
}
然后:
Task/Main Loop
负责处理。
5. H7/F7必须建立Cache意识
开发STM32H7时,一看到:
DMA
Ethernet
SDMMC
USB
Camera
ADC
SPI
就应该条件反射想到:
Cache一致性
MPU
内存区域
Cache Line
三十六、一套实战DMA排查Checklist
以后遇到STM32 DMA异常,可以直接按照下面检查。
DMA基础配置
- DMA Stream / Channel / Request是否正确
- DMA方向是否正确
- Peripheral Increment是否正确
- Memory Increment是否开启
- Peripheral Width是否正确
- Memory Width是否正确
- Normal / Circular模式是否符合需求
- DMA中断是否正常进入
数据层
- Buffer大小是否足够
- DMA Length是否超过Buffer
- Buffer变量类型是否匹配DMA宽度
- 字符串是否补
\0 - CPU是否同时修改DMA Buffer
内存层
- Buffer是否位于DMA可访问RAM
- STM32H7/F7是否存在D-Cache问题
- 是否执行Clean/Invalidate
- Cache地址是否正确对齐
外设层
- UART是否存在ORE/FE/NE
- SPI是否存在Dummy Byte
- SPI CS时序是否正确
- UART是否真正等待TC
- ADC Rank顺序是否正确
- ADC采样时间是否足够
时序层
- DMA是否在数据到来前启动
- DMA重新启动是否存在空窗期
- CPU处理速度是否跟得上DMA
- Circular模式是否覆盖未处理数据
- 中断优先级是否合理
软件架构
- DMA Buffer是否使用局部变量
- 中断中是否进行了耗时处理
- 多任务是否同时访问Buffer
- 是否需要双Buffer
- 是否需要Ring Buffer
三十七、总结
STM32使用DMA以后出现数据异常,最容易犯的错误就是:
一看到DMA数据不对,就开始反复修改DMA配置。
实际上,一个完整DMA数据链路是:
外部信号
↓
外设
↓
外设寄存器
↓
DMA Request
↓
DMA
↓
总线
↓
RAM
↓
Cache
↓
CPU
↓
应用程序
所以DMA数据异常可能来自任何一层。
尤其需要重点关注下面这些问题:
① D-Cache一致性
② DMA无法访问某些RAM
③ 数据宽度与变量类型不匹配
④ Memory Increment配置错误
⑤ Normal/Circular模式理解错误
⑥ DMA完成≠外设真正完成
⑦ UART/SPI外设残留状态
⑧ IDLE接收长度计算错误
⑨ CPU与DMA同时访问Buffer
⑩ DMA使用了生命周期已经结束的局部变量
其中在STM32H7项目中,最应该首先怀疑的是:
Cache
+
内存区域
而在STM32F1/F4等项目中,则更应该重点排查:
Buffer
+
数据宽度
+
外设时序
+
DMA重启
+
并发访问
一个非常实用的DMA排查原则是:
先证明外设数据正确,再证明DMA确实搬运了数据,最后再检查CPU读取的数据是否和RAM一致。
这样就可以把一个看似复杂的“DMA随机异常”,逐层缩小到某一个具体环节。
DMA并不可怕。
真正困难的是:
不要只盯着DMA,而要看完整的数据链路。
写在最后
在STM32实际项目中,UART DMA、ADC DMA、SPI DMA的很多“玄学问题”,最后往往都不是DMA控制器坏了,而是Cache、Buffer、时序、外设状态或者软件架构的问题。
如果你遇到:
DMA第一次正常,第二次异常
DMA数据一直不更新
DMA收到的数据错位
DMA偶尔少一个字节
Debug正常、Run异常
STM32H7 DMA读取到旧数据

477

被折叠的 条评论
为什么被折叠?



