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

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故障为主线,系统讲解:

  1. DMA到底是怎么搬运数据的;
  2. 为什么DMA配置正确,数据仍然可能异常;
  3. 最容易忽略的10类问题;
  4. UART、ADC、SPI使用DMA时分别需要注意什么;
  5. STM32F4和STM32H7等不同平台有哪些差异;
  6. 如何建立一套完整的DMA故障排查流程。

一、先理解DMA到底在做什么

DMA全称:

Direct Memory Access,直接存储器访问。

它最大的作用是:

在不需要CPU逐个搬运数据的情况下,实现外设和内存之间的数据传输。

例如UART接收。

如果不用DMA,CPU需要不断读取USART的数据寄存器:

c 复制代码
while (HAL_UART_Receive(&huart1, &data, 1, 100) == HAL_OK)
{
    rx_buf[index++] = data;
}

每收到一个字节,CPU都要参与。

如果使用DMA:

c 复制代码
HAL_UART_Receive_DMA(&huart1, rx_buf, 100);

后续UART收到的数据可以由DMA自动搬运到:

c 复制代码
rx_buf[100]

CPU只需要等待DMA传输完成中断。

整个流程大致为:

text 复制代码
UART接收到数据
      ↓
UART数据寄存器
      ↓
DMA检测到外设DMA请求
      ↓
DMA读取UART数据寄存器
      ↓
DMA写入RAM
      ↓
传输计数减1
      ↓
全部完成
      ↓
DMA传输完成中断

看起来DMA只是"搬运工"。

所以有一个非常重要的调试思路:

DMA数据异常,不一定意味着DMA控制器有问题。

数据从外设到应用程序,需要经过很多环节:

text 复制代码
外设信号
   ↓
外设寄存器
   ↓
DMA请求
   ↓
DMA控制器
   ↓
系统总线
   ↓
RAM
   ↓
Cache
   ↓
CPU读取
   ↓
应用程序处理

任何一个环节出问题,最终看到的结果都可能表现为:

text 复制代码
DMA数据异常

二、DMA配置正确,为什么数据还是错?

假设UART DMA配置如下:

c 复制代码
uint8_t rx_buf[100];

HAL_UART_Receive_DMA(&huart1, rx_buf, 100);

DMA已经正常开启。

DMA中断也能进入:

c 复制代码
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if(huart->Instance == USART1)
    {
        // DMA完成
    }
}

但打印:

c 复制代码
printf("%s\r\n", rx_buf);

发现:

text 复制代码
ABCDE??????

或者:

text 复制代码
ABCDEABCDE

很多人会开始检查:

text 复制代码
DMA Stream
DMA Channel
Priority
Mode
FIFO
Memory Increment
Peripheral Increment

这些当然要检查。

但如果DMA中断能正常完成,DMA计数也符合预期,那么问题很可能已经不在DMA配置本身。

下面重点介绍最常见的问题。


三、问题一:DMA缓存和CPU Cache不一致

这个问题在STM32H7、STM32F7等带D-Cache的MCU上非常典型。

例如:

c 复制代码
uint8_t rx_buf[1024];

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

DMA已经把新数据写入RAM:

text 复制代码
RAM:
AA BB CC DD

但CPU读取数据时,可能读取的是Cache中的旧数据:

text 复制代码
D-Cache:
11 22 33 44

于是出现一个非常诡异的现象:

text 复制代码
DMA看起来已经接收成功
DMA完成中断正常
RAM实际已经更新
但是程序读取到的数据还是旧的

原因是:

DMA直接操作内存,而CPU可能操作Cache。

可以把STM32H7中的情况简单理解成:

text 复制代码
                  ┌──────────────┐
CPU ─────────────→│ D-Cache      │
                  └──────┬───────┘
                         │
                         ↓
                      SRAM
                         ↑
                         │
DMA ────────────────────┘

DMA不会自动帮你刷新CPU缓存。

1. DMA接收后Invalidate Cache

例如:

c 复制代码
HAL_UART_Receive_DMA(&huart1, rx_buf, 256);

DMA完成后:

c 复制代码
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, 256);

然后CPU再读取:

c 复制代码
ProcessData(rx_buf);

完整示例:

c 复制代码
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发送:

c 复制代码
HAL_UART_Transmit_DMA(&huart1, tx_buf, length);

CPU先修改:

c 复制代码
tx_buf[0] = 0x12;
tx_buf[1] = 0x34;

新数据可能只存在Cache中,而还没有真正写入RAM。

这时候DMA直接读取RAM,就可能发送旧数据。

因此应该:

c 复制代码
SCB_CleanDCache_by_Addr(
    (uint32_t *)tx_buf,
    length
);

HAL_UART_Transmit_DMA(
    &huart1,
    tx_buf,
    length
);

可以记住一个简单规则:

text 复制代码
CPU → DMA
发送前:Clean

DMA → CPU
接收后:Invalidate

这是STM32H7 DMA异常排查时必须优先检查的一项。


四、问题二:DMA Buffer放在了DMA无法访问的内存区域

这是STM32H7项目中特别容易踩的坑。

有些STM32内部存在多块RAM,例如:

text 复制代码
ITCM
DTCM
AXI SRAM
SRAM1
SRAM2
SRAM3
SRAM4

它们并不是所有DMA都可以访问。

例如某些DMA无法直接访问DTCM。

假设:

c 复制代码
uint8_t adc_buffer[1024];

链接脚本把这个变量放到了:

text 复制代码
DTCM RAM

CPU访问它完全正常。

但DMA不一定能访问。

结果可能表现为:

text 复制代码
DMA没有写进去
DMA数据全部为0
DMA数据不更新
DMA异常

因此排查STM32H7 DMA问题时,一定要检查变量地址。

例如:

c 复制代码
printf("buffer addr = 0x%08X\r\n",
       (unsigned int)adc_buffer);

如果发现地址位于DMA不可达区域,就需要调整。

例如:

c 复制代码
__attribute__((section(".RAM_D2")))
uint8_t adc_buffer[1024];

然后在链接脚本中把:

text 复制代码
.RAM_D2

映射到DMA可以访问的SRAM区域。

所以不要只问:

DMA配置对不对?

还要问:

DMA能不能访问这个buffer?


五、问题三:DMA数据宽度和变量类型不匹配

这是非常经典的问题。

假设ADC配置为:

text 复制代码
Peripheral Data Width = Half Word
Memory Data Width     = Half Word

也就是16bit。

但程序定义:

c 复制代码
uint8_t adc_buffer[100];

这就会产生问题。

DMA每次搬运:

text 复制代码
16 bit

而数组元素只有:

text 复制代码
8 bit

最终数据排列可能和你想象的不一样。

正确做法通常是:

c 复制代码
uint16_t adc_buffer[100];

例如:

c 复制代码
HAL_ADC_Start_DMA(
    &hadc1,
    (uint32_t *)adc_buffer,
    100
);

DMA宽度对应关系:

DMA配置 建议变量
Byte uint8_t
Half Word uint16_t
Word uint32_t

UART通常:

text 复制代码
Byte

ADC通常:

text 复制代码
Half Word

某些32位外设:

text 复制代码
Word

如果数据宽度不一致,可能出现:

text 复制代码
数据错位
高低字节异常
数组间隔异常
读取值异常

六、问题四:Memory Increment没有打开

假设:

c 复制代码
uint8_t rx_buf[100];

正常情况下,DMA应该:

text 复制代码
第1字节 → rx_buf[0]
第2字节 → rx_buf[1]
第3字节 → rx_buf[2]
第4字节 → rx_buf[3]

因此:

text 复制代码
Memory Increment = Enable

如果关闭:

text 复制代码
Memory Increment = Disable

DMA可能一直往同一个地址写:

text 复制代码
第1字节 → rx_buf[0]
第2字节 → rx_buf[0]
第3字节 → rx_buf[0]
第4字节 → rx_buf[0]

最终结果:

text 复制代码
rx_buf[0] = 最后一个数据

其余内容没有正确更新。

所以DMA参数至少要确认:

text 复制代码
Peripheral Increment
Memory Increment
Peripheral Data Alignment
Memory Data Alignment
Mode
Priority

以UART RX为例,常见配置:

text 复制代码
Direction               Peripheral to Memory
Peripheral Increment    Disable
Memory Increment        Enable
Peripheral Data Width   Byte
Memory Data Width       Byte
Mode                    Normal / Circular

七、问题五:Normal模式和Circular模式理解错误

DMA常见两个工作模式:

text 复制代码
Normal
Circular

Normal模式

例如:

c 复制代码
HAL_UART_Receive_DMA(&huart1, rx_buf, 100);

DMA收到100字节后:

text 复制代码
DMA停止

如果希望再次接收,需要重新调用:

c 复制代码
HAL_UART_Receive_DMA(
    &huart1,
    rx_buf,
    100
);

很多人遇到:

text 复制代码
第一次DMA正常
第二次没有数据

第一反应是DMA坏了。

实际上可能只是DMA使用的是:

text 复制代码
Normal模式

而没有重新启动。

Circular模式

Circular模式流程:

text 复制代码
buffer[0]
   ↓
buffer[1]
   ↓
...
   ↓
buffer[99]
   ↓
buffer[0]
   ↓
继续循环

适用于:

text 复制代码
ADC连续采样
UART连续数据流
音频采样
高速传感器数据

但Circular模式也容易产生另外一个问题:

CPU正在处理Buffer时,DMA已经开始重新覆盖Buffer。

例如:

text 复制代码
DMA:
正在写第0~20字节

CPU:
还在处理上一次第0~100字节

这就出现数据竞争。

解决方案可以考虑:

text 复制代码
双缓冲
Half Transfer中断
Transfer Complete中断
Ping-Pong Buffer
Ring Buffer

八、问题六:DMA完成不等于外设传输完成

这是非常重要的一点。

以UART TX DMA为例:

c 复制代码
HAL_UART_Transmit_DMA(
    &huart1,
    tx_buf,
    100
);

DMA完成意味着:

text 复制代码
100字节已经从RAM搬到了UART

但不一定意味着:

text 复制代码
100字节已经全部从TX引脚发送出去

因为UART内部还有:

text 复制代码
TDR
Shift Register

所以实际流程:

text 复制代码
RAM
 ↓
DMA
 ↓
UART TDR
 ↓
UART Shift Register
 ↓
TX Pin

DMA完成时,最后几个bit可能还没有真正发送完成。

这个问题在RS485中尤其明显。

假设DE控制:

c 复制代码
RS485_DE_HIGH();

HAL_UART_Transmit_DMA(
    &huart1,
    tx_buf,
    len
);

然后DMA完成立即执行:

c 复制代码
RS485_DE_LOW();

最后一个字节可能直接被截断。

正确思路应该是:

text 复制代码
等待UART TC
Transmit Complete

再切换DE。

例如:

c 复制代码
while(__HAL_UART_GET_FLAG(
          &huart1,
          UART_FLAG_TC) == RESET)
{
}

RS485_DE_LOW();

所以一定要区分:

text 复制代码
DMA Transfer Complete

和:

text 复制代码
Peripheral Transfer Complete

它们不是一个概念。


九、问题七:启动DMA前外设残留旧数据

UART DMA中非常容易遇到。

例如上一帧通信出现异常:

text 复制代码
ORE
RXNE
FE
NE

UART内部状态没有清理干净。

此时重新启动:

c 复制代码
HAL_UART_Receive_DMA(...)

第一字节可能就是旧数据。

于是出现:

text 复制代码
数据整体偏移一个字节
帧头错误
第一字节异常
偶发乱码

调试时可以重点检查UART状态寄存器。

对于部分STM32系列,可以在重新启动DMA前清除错误标志。

例如HAL中根据芯片系列处理:

c 复制代码
__HAL_UART_CLEAR_OREFLAG(&huart1);

同时还需要考虑:

text 复制代码
RXNE残留
ORE溢出
IDLE状态
DMA NDTR残留

特别是使用:

text 复制代码
UART + DMA + IDLE

接收不定长数据时,这类问题非常常见。


十、问题八:UART IDLE + DMA计算长度错误

这是项目中非常高频的BUG。

假设:

c 复制代码
uint8_t rx_buf[256];

HAL_UART_Receive_DMA(
    &huart1,
    rx_buf,
    256
);

UART收到:

text 复制代码
20 Byte

随后产生IDLE中断。

此时DMA剩余计数:

text 复制代码
NDTR = 236

那么实际接收长度:

text 复制代码
256 - 236 = 20

代码:

c 复制代码
uint16_t len;

len = sizeof(rx_buf)
    - __HAL_DMA_GET_COUNTER(huart1.hdmarx);

然后:

c 复制代码
ProcessData(rx_buf, len);

如果长度计算错误,例如:

c 复制代码
len = __HAL_DMA_GET_COUNTER(...);

那就会把:

text 复制代码
236

当成实际长度。

后面的数据当然全部异常。

所以记住:

text 复制代码
实际接收长度
=
Buffer总长度
-
DMA剩余长度

即:

c 复制代码
recv_len =
    BUFFER_SIZE
    - __HAL_DMA_GET_COUNTER(hdma);

十一、问题九:Buffer被CPU和DMA同时修改

假设:

c 复制代码
uint8_t tx_buf[100];

程序:

c 复制代码
sprintf((char *)tx_buf,
        "Temperature:%d",
        temp);

HAL_UART_Transmit_DMA(
    &huart1,
    tx_buf,
    strlen((char *)tx_buf)
);

DMA开始发送。

但DMA还没有发送结束,程序又执行:

c 复制代码
sprintf((char *)tx_buf,
        "Voltage:%d",
        voltage);

那么DMA发送过程中,Buffer内容突然变了。

最终串口可能收到:

text 复制代码
TemperatVoltage:12

或者其他混乱数据。

原因非常简单:

text 复制代码
CPU修改Buffer
       ↓
同时
       ↑
DMA读取Buffer

产生竞争。

所以DMA发送Buffer在传输完成前,原则上:

不要修改。

可以采用:

text 复制代码
tx_buf1
tx_buf2

双Buffer。

例如:

c 复制代码
uint8_t tx_buf[2][256];

volatile uint8_t current_buf;

形成:

text 复制代码
CPU写Buffer0
DMA发Buffer1

下一轮

CPU写Buffer1
DMA发Buffer0

这也是典型的Ping-Pong Buffer。


十二、问题十:栈上的局部变量被DMA使用

这是一个非常隐蔽的问题。

错误示例:

c 复制代码
void SendData(void)
{
    uint8_t buf[100];

    sprintf((char *)buf,
            "Hello STM32");

    HAL_UART_Transmit_DMA(
        &huart1,
        buf,
        100
    );
}

调用:

c 复制代码
SendData();

函数返回后:

text 复制代码
buf生命周期结束

但是DMA可能仍然正在发送。

随后其他函数继续使用栈空间,把buf覆盖。

DMA再读取:

text 复制代码
buf

读到的已经不是原来的数据。

于是出现:

text 复制代码
DMA发送乱码
偶发错误
Debug正常Release异常

正确方式之一:

c 复制代码
static uint8_t buf[100];

例如:

c 复制代码
void SendData(void)
{
    static uint8_t buf[100];

    sprintf((char *)buf,
            "Hello STM32");

    HAL_UART_Transmit_DMA(
        &huart1,
        buf,
        strlen((char *)buf)
    );
}

或者定义为全局变量:

c 复制代码
uint8_t uart_tx_buf[100];

这类问题非常值得检查。


十三、为什么Debug正常,全速运行异常?

这是DMA项目中特别典型的现象。

例如:

text 复制代码
单步执行:正常
Run执行:错误

很多人会怀疑:

text 复制代码
DMA不稳定
MCU有问题

实际上这种现象通常强烈暗示:

text 复制代码
时序问题
竞争条件
缓存问题
变量生命周期
中断优先级

因为Debug单步实际上人为加入了很多延时。

比如正常运行:

text 复制代码
CPU写Buffer
↓
启动DMA
↓
CPU立即修改Buffer

DMA还没有读完。

但单步调试:

text 复制代码
CPU写Buffer
↓
暂停
↓
DMA已经传完
↓
CPU继续修改Buffer

问题被"隐藏"了。

所以:

Debug正常、Run异常,不要轻易认为是编译器问题,应优先检查时序和并发访问。


十四、ADC DMA数据异常怎么排查?

ADC + DMA是STM32项目最常见的组合之一。

例如:

c 复制代码
uint16_t adc_buf[100];

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

如果ADC数据异常,可以从下面几个方向排查。

1. ADC采样时间是否太短

ADC输入并不是理想电压源。

如果信号源阻抗比较大,而ADC采样时间太短:

text 复制代码
内部采样电容
无法充分充电

最终ADC值偏低或者不稳定。

例如:

text 复制代码
ADC_SAMPLETIME_3CYCLES

可能改成:

text 复制代码
ADC_SAMPLETIME_56CYCLES

或者更长。

DMA只是把ADC结果搬走。

真正数据错误可能来自:

text 复制代码
ADC采样阶段

而不是DMA。


2. ADC扫描顺序是否正确

例如:

text 复制代码
ADC Channel 0
ADC Channel 1
ADC Channel 2

DMA结果:

c 复制代码
adc_buf[0]
adc_buf[1]
adc_buf[2]

但如果Rank配置错误:

text 复制代码
Rank1 = CH2
Rank2 = CH0
Rank3 = CH1

程序却按照:

c 复制代码
adc_buf[0] = CH0
adc_buf[1] = CH1
adc_buf[2] = CH2

理解。

那你会感觉:

text 复制代码
DMA数据全错了

其实DMA完全正常,只是ADC通道顺序错了。


3. ADC DMA是否使用Circular

如果想持续采样:

text 复制代码
Continuous Conversion = Enable
DMA Continuous Requests = Enable
DMA Mode = Circular

如果DMA配置为Normal:

text 复制代码
采样一轮以后停止

就会出现:

text 复制代码
第一次ADC正常
之后ADC值一直不变

这也是非常典型的误判。


十五、SPI DMA数据异常怎么排查?

SPI DMA通常比UART DMA更复杂。

因为SPI是全双工接口。

发送一个字节的同时:

text 复制代码
一定会接收一个字节

例如读取SPI Flash:

text 复制代码
发送命令
发送地址
发送Dummy
接收数据

实际SPI过程:

text 复制代码
MOSI → 命令
MISO ← Dummy

MOSI → 地址
MISO ← Dummy

MOSI → Dummy
MISO ← Flash Data

所以SPI DMA接收Buffer中,前面可能天然存在:

text 复制代码
Dummy Byte

例如:

text 复制代码
RX:
00 00 00 12 34 56 78

真正有效数据:

text 复制代码
12 34 56 78

如果直接从:

c 复制代码
rx_buf[0]

开始解析,就会感觉:

text 复制代码
DMA错位

实际上是SPI协议本身导致的。

另外SPI DMA还要重点检查:

text 复制代码
CS拉低时间
CS拉高时机
TX DMA
RX DMA
BSY标志
FIFO
SPI Mode
CPOL
CPHA

尤其是:

text 复制代码
DMA完成以后立即CS拉高

也可能在最后一个bit还没发完时破坏传输。

因此应确认:

text 复制代码
SPI BSY == 0

然后再:

text 复制代码
CS = HIGH

十六、UART DMA偶尔少字节是什么原因?

例如发送:

text 复制代码
AA 55 01 02 03 04 05 06

但DMA偶尔接收到:

text 复制代码
55 01 02 03 04 05 06

少了:

text 复制代码
AA

重点检查:

text 复制代码
DMA启动是否晚于数据到达

流程如果变成:

text 复制代码
对方开始发送
      ↓
AA进入UART
      ↓
此时DMA还没开启
      ↓
程序开启DMA
      ↓
后续55 01 02...

自然丢失第一个字节。

正确思路:

text 复制代码
先开启DMA接收
↓
再允许对方发送

通信协议设计中可以使用:

text 复制代码
握手
READY信号
ACK
命令-响应

确保接收方已经准备好。


十七、DMA中断优先级也可能影响数据

在RTOS项目中尤其明显。

例如:

text 复制代码
UART DMA IRQ
ADC DMA IRQ
Ethernet IRQ
USB IRQ
FreeRTOS Task

同时存在。

如果DMA中断优先级不合理,可能造成:

text 复制代码
处理延迟
Buffer覆盖
事件响应不及时

例如Circular DMA:

text 复制代码
Half Transfer IRQ

到来以后CPU应该及时处理前半Buffer。

如果一个高优先级中断长期占用CPU:

text 复制代码
DMA继续写

最终前半Buffer可能在处理之前已经再次被覆盖。

所以RTOS系统中不仅要考虑:

text 复制代码
DMA Priority

还要考虑:

text 复制代码
NVIC Interrupt Priority

注意这两个不是一回事。


十八、DMA Priority和NVIC Priority不要混淆

CubeMX里面经常看到:

text 复制代码
DMA Priority
Low
Medium
High
Very High

它表示:

多个DMA Stream同时请求总线时,谁优先。

而:

text 复制代码
NVIC Priority

表示:

多个CPU中断同时发生时,CPU优先处理谁。

例如:

text 复制代码
DMA Priority = Very High

不代表:

text 复制代码
DMA中断优先级最高

这两个概念完全不同。


十九、volatile能不能解决DMA数据异常?

很多人一看到DMA变量就写:

c 复制代码
volatile uint8_t rx_buf[100];

但需要注意:

volatile不是DMA问题的万能解决方案。

volatile主要告诉编译器:

text 复制代码
不要假设这个变量不会改变
每次需要时重新读取

适合:

c 复制代码
volatile uint8_t dma_done;

例如:

c 复制代码
volatile uint8_t dma_done = 0;

void HAL_UART_RxCpltCallback(...)
{
    dma_done = 1;
}

主循环:

c 复制代码
if(dma_done)
{
    dma_done = 0;
    ProcessData();
}

但:

c 复制代码
volatile

不能解决:

text 复制代码
D-Cache一致性
Buffer越界
DMA不可访问内存
数据宽度错误
并发修改Buffer
UART时序错误

所以不要遇到DMA异常就随便加volatile。


二十、一个典型错误案例

假设STM32通过UART DMA接收设备数据:

c 复制代码
uint8_t rx_buf[256];

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

IDLE中断:

c 复制代码
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);
}

看起来似乎没有问题。

但这里就可能隐藏多个风险。

风险一

先执行:

c 复制代码
HAL_UART_DMAStop()

然后才读取NDTR。

某些情况下计数状态可能变化。

更稳妥的思路是:

text 复制代码
先读取NDTR
再停止DMA

风险二

执行:

c 复制代码
ProcessData()

时间太长。

这时候DMA已经停止。

如果串口继续收到数据:

text 复制代码
数据可能丢失

风险三

处理完以后才重新启动DMA:

c 复制代码
HAL_UART_Receive_DMA()

中间存在接收空窗期。

更好的架构应该让DMA尽快恢复。

例如:

text 复制代码
IDLE
 ↓
计算长度
 ↓
迅速切换Buffer/重新开启DMA
 ↓
后续任务慢慢解析

而不是:

text 复制代码
停止DMA
 ↓
复杂协议解析
 ↓
打印日志
 ↓
CRC计算
 ↓
重新开启DMA

二十一、推荐UART DMA + IDLE的设计方式

对于不定长协议,例如:

text 复制代码
Modbus RTU
自定义串口协议
GPS
4G模块
传感器数据

推荐:

text 复制代码
UART
 ↓
DMA Circular / ReceiveToIdle
 ↓
IDLE事件
 ↓
获取有效长度
 ↓
复制/切换Buffer
 ↓
消息队列
 ↓
协议解析Task

HAL库中很多STM32系列已经支持:

c 复制代码
HAL_UARTEx_ReceiveToIdle_DMA()

例如:

c 复制代码
HAL_UARTEx_ReceiveToIdle_DMA(
    &huart1,
    rx_buf,
    RX_BUF_SIZE
);

回调:

c 复制代码
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:

text 复制代码
示波器
逻辑分析仪
串口抓包

SPI:

text 复制代码
MOSI
MISO
CLK
CS

I²C:

text 复制代码
SCL
SDA
ACK
Address

ADC:

text 复制代码
万用表
示波器

如果外部信号本身就错:

text 复制代码
DMA当然不可能得到正确数据

第二步:直接查看外设寄存器

例如UART:

text 复制代码
RDR
ISR
SR
DR

ADC:

text 复制代码
DR
ISR

SPI:

text 复制代码
DR
SR

确认:

text 复制代码
外设自己到底有没有拿到正确数据

如果外设寄存器就错:

text 复制代码
重点查外设

如果外设寄存器正确,但RAM错误:

text 复制代码
重点查DMA

第三步:检查DMA NDTR

DMA中很重要的寄存器:

text 复制代码
NDTR

表示:

text 复制代码
还剩多少数据没有传输

如果DMA长度:

text 复制代码
100

现在:

text 复制代码
NDTR = 60

说明:

text 复制代码
已经搬运40个

如果NDTR完全不变:

text 复制代码
100

说明DMA可能根本没收到请求。

重点检查:

text 复制代码
外设DMA Request
DMA Enable
Channel/Request
DMA mapping

二十三、第四步:查看Buffer实际内存

不要只看打印结果。

直接使用IDE的Memory窗口查看:

text 复制代码
rx_buf地址

例如:

text 复制代码
0x20001000

观察:

text 复制代码
DMA运行前
DMA运行中
DMA完成后

如果RAM实际内容正确:

text 复制代码
但是程序打印错误

那么问题可能在:

text 复制代码
Cache
字符串结束符
printf
解析函数
Buffer越界

而不是DMA。

这是一个非常实用的调试方法。


二十四、第五步:检查Buffer是否越界

例如:

c 复制代码
uint8_t rx_buf[100];

却启动:

c 复制代码
HAL_UART_Receive_DMA(
    &huart1,
    rx_buf,
    200
);

DMA会继续往后写。

结果可能覆盖:

text 复制代码
其他变量
任务栈
控制变量
指针
RTOS对象

最终表现可能非常离谱:

text 复制代码
DMA异常
程序跑飞
HardFault
任务死掉
变量莫名改变

实际上就是:

text 复制代码
Memory Corruption

所以一定要确认:

text 复制代码
DMA Length <= Buffer Size

二十五、第六步:检查数据宽度

确认:

text 复制代码
Peripheral Data Width
Memory Data Width
变量类型

例如ADC:

text 复制代码
Half Word
Half Word
uint16_t

UART:

text 复制代码
Byte
Byte
uint8_t

二十六、第七步:检查内存位置

尤其是STM32H7:

text 复制代码
Buffer在哪个RAM?
DMA能否访问?

使用:

c 复制代码
printf("%p\r\n", rx_buf);

或者IDE:

text 复制代码
Watch
Memory
Map File

确定变量地址。


二十七、第八步:检查Cache

如果芯片存在:

text 复制代码
D-Cache

一定检查:

text 复制代码
Clean
Invalidate
32-byte Cache Line
地址对齐

尤其是STM32H7。

很多看似:

text 复制代码
DMA随机异常

实际上都是Cache一致性问题。


二十八、第九步:检查CPU是否同时访问Buffer

问自己三个问题:

text 复制代码
DMA正在写Buffer时,
CPU会不会读?

DMA正在读Buffer时,
CPU会不会改?

中断和Task是否同时访问?

如果答案是:

text 复制代码

就可能产生竞争。

解决方式:

text 复制代码
双Buffer
环形Buffer
锁
状态机
消息队列
临界区

二十九、第十步:检查外设时序

例如RS485:

text 复制代码
DE什么时候拉高?
DE什么时候拉低?

SPI:

text 复制代码
CS什么时候拉低?
什么时候拉高?

ADC:

text 复制代码
Trigger频率是多少?
采样时间够不够?

UART:

text 复制代码
DMA有没有提前打开?
IDLE有没有清除?
ORE有没有出现?

DMA只是传输数据。

外设时序不对,DMA一样只能搬运错误结果。


三十、一个完整DMA故障定位思维导图

可以按照下面这条路线理解:

text 复制代码
                DMA数据异常
                     │
         ┌───────────┴───────────┐
         │                       │
      源数据错误              源数据正确
         │                       │
      查硬件                     ↓
      查协议                  查外设寄存器
                                 │
                     ┌───────────┴───────────┐
                     │                       │
                  寄存器错                寄存器对
                     │                       │
                  查外设                     ↓
                                         查DMA
                                           │
                  ┌────────────────────────┼───────────────┐
                  │                        │               │
                NDTR                    Buffer          Memory
                  │                        │               │
              DMA是否工作             是否越界         DMA可访问?
                                           │
                                           ↓
                                         Cache
                                           │
                                           ↓
                                         并发
                                           │
                                           ↓
                                         时序

不要把所有问题都压缩成:

text 复制代码
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收到:

text 复制代码
HELLO

Buffer:

c 复制代码
uint8_t rx_buf[100];

DMA实际只写入:

text 复制代码
H E L L O

也就是:

text 复制代码
48 45 4C 4C 4F

DMA不会自动帮你补:

text 复制代码
00

如果直接:

c 复制代码
printf("%s\r\n", rx_buf);

printf会一直往后找:

text 复制代码
'\0'

因此可能打印:

text 复制代码
HELLOxxxxxxxxxxxx

然后你以为:

text 复制代码
DMA后面的数据怎么全乱了?

实际上DMA根本没问题。

正确做法:

c 复制代码
rx_buf[len] = '\0';

前提是Buffer多预留一个字节:

c 复制代码
uint8_t rx_buf[RX_SIZE + 1];

例如:

c 复制代码
if(len < RX_SIZE)
{
    rx_buf[len] = '\0';
}

三十三、不要用printf判断所有DMA问题

printf本身可能带来新的问题。

例如:

c 复制代码
printf("DMA RX:%s\r\n", rx_buf);

如果printf底层同样使用:

text 复制代码
UART

甚至也是:

text 复制代码
UART DMA

就可能发生冲突。

另外printf速度较慢。

115200bps情况下,大量打印日志会严重影响实时性。

所以调试高速DMA时建议结合:

text 复制代码
IDE Watch
Memory窗口
逻辑分析仪
示波器
SWO
RTT
状态变量
错误计数器

而不是只依赖printf。


三十四、推荐增加DMA调试变量

实际工程中可以定义:

c 复制代码
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;

例如:

c 复制代码
void HAL_UART_RxCpltCallback(
    UART_HandleTypeDef *huart)
{
    dma_debug.rx_count++;
}

错误回调:

c 复制代码
void HAL_UART_ErrorCallback(
    UART_HandleTypeDef *huart)
{
    dma_debug.uart_error++;
}

DMA错误:

c 复制代码
dma_debug.dma_error++;

这样运行一段时间后,能够明显帮助判断:

text 复制代码
到底是DMA没完成,
还是UART本身报错,
还是Buffer处理不及时。

比不停打印日志更加可靠。


三十五、工程中推荐的DMA设计原则

为了减少DMA随机异常,建议遵循下面几个原则。

1. DMA Buffer尽量使用全局变量或static

推荐:

c 复制代码
static uint8_t uart_rx_buf[256];

不推荐:

c 复制代码
void test(void)
{
    uint8_t buf[256];

    HAL_UART_Receive_DMA(
        &huart1,
        buf,
        256
    );
}

2. DMA进行过程中不要随意修改Buffer

TX:

text 复制代码
DMA完成前CPU不要改

RX:

text 复制代码
DMA正在写的时候CPU避免处理同一片区域

3. 高速数据推荐双Buffer

例如:

text 复制代码
Buffer A ← DMA
Buffer B ← CPU

交换

Buffer B ← DMA
Buffer A ← CPU

4. 中断里面不要做复杂处理

不要:

c 复制代码
void DMA_IRQHandler(void)
{
    CRC_Calculate();
    ParseProtocol();
    printf();
    SaveFlash();
}

推荐:

c 复制代码
void DMA_IRQHandler(void)
{
    dma_event = 1;
}

然后:

text 复制代码
Task/Main Loop

负责处理。


5. H7/F7必须建立Cache意识

开发STM32H7时,一看到:

text 复制代码
DMA
Ethernet
SDMMC
USB
Camera
ADC
SPI

就应该条件反射想到:

text 复制代码
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数据链路是:

text 复制代码
外部信号
   ↓
外设
   ↓
外设寄存器
   ↓
DMA Request
   ↓
DMA
   ↓
总线
   ↓
RAM
   ↓
Cache
   ↓
CPU
   ↓
应用程序

所以DMA数据异常可能来自任何一层。

尤其需要重点关注下面这些问题:

text 复制代码
① D-Cache一致性
② DMA无法访问某些RAM
③ 数据宽度与变量类型不匹配
④ Memory Increment配置错误
⑤ Normal/Circular模式理解错误
⑥ DMA完成≠外设真正完成
⑦ UART/SPI外设残留状态
⑧ IDLE接收长度计算错误
⑨ CPU与DMA同时访问Buffer
⑩ DMA使用了生命周期已经结束的局部变量

其中在STM32H7项目中,最应该首先怀疑的是:

text 复制代码
Cache
+
内存区域

而在STM32F1/F4等项目中,则更应该重点排查:

text 复制代码
Buffer
+
数据宽度
+
外设时序
+
DMA重启
+
并发访问

一个非常实用的DMA排查原则是:

先证明外设数据正确,再证明DMA确实搬运了数据,最后再检查CPU读取的数据是否和RAM一致。

这样就可以把一个看似复杂的"DMA随机异常",逐层缩小到某一个具体环节。

DMA并不可怕。

真正困难的是:

不要只盯着DMA,而要看完整的数据链路。


写在最后

在STM32实际项目中,UART DMA、ADC DMA、SPI DMA的很多"玄学问题",最后往往都不是DMA控制器坏了,而是Cache、Buffer、时序、外设状态或者软件架构的问题。

如果你遇到:

text 复制代码
DMA第一次正常,第二次异常
DMA数据一直不更新
DMA收到的数据错位
DMA偶尔少一个字节
Debug正常、Run异常
STM32H7 DMA读取到旧数据
相关推荐
电化学仪器白超1 小时前
MV-CS200-10UC相机参数配置
python·单片机·嵌入式硬件·数码相机·自动化·ltspice
我和头发拼了1 小时前
STM32--9USART收发HEX数据包
stm32·单片机·嵌入式硬件
wuyk5551 小时前
7.AVL 树:第一个自平衡二叉搜索树
开发语言·stm32·单片机·算法
国科安芯2 小时前
星载网络化控制的总线脊梁:四路CANFD在分布式载荷管理中的架构优势
分布式·单片机·嵌入式硬件·架构·系统架构·canfd·低轨卫星星座
树的枝2 小时前
【STM32】05.TIM定时器
笔记·stm32·单片机·嵌入式硬件
caimouse3 小时前
ReactOS 图形系统分析(50):画笔子系统 — pen.c
c语言·开发语言·stm32
硅农深芯3 小时前
PDN仿真显示高频阻抗大意味着什么?
单片机·嵌入式硬件·pdn·阻抗·pi仿真
lingzhilab3 小时前
零知派——STM32F103驱动ST7789与INA219太阳能充电功率实时监测系统
stm32·单片机·嵌入式硬件
wuyk55513 小时前
98.C语言易混难点:字符数组与字符串指针的底层差异
c语言·开发语言·c++·stm32·嵌入式硬件·算法