一、问题现象:为什么数据会“隐性”损坏?
在 STM32H7 上使用 DMA 收发数据时,常遇到以下诡异现象:
- 发送:DMA 发送的数据偶尔是旧值,或部分字节错乱
- 接收:DMA 收到的数据读出来不对,但用调试器看内存又是对的
- 概率性故障:改代码、加打印、换优化等级后现象变化
根本原因:Cortex-M7 的 D-Cache 与 DMA 直接内存访问之间缺乏硬件一致性。DMA 看到的是物理内存(SRAM),CPU 看到的是 Cache 中的副本。若不做正确的 Cache 维护,两者数据就会“各说各话”。
二、原理:M7 Cache 与 DMA 的冲突点
2.1 写通 vs 写回
STM32H7 的 D-Cache 默认是写回(Write-Back) 策略:
- CPU 写数据 → 只写入 Cache,标记为 dirty,不立即写回 SRAM
- 此时启动 DMA 发送 → DMA 从 SRAM 读到的还是旧数据
2.2 读分配
CPU 读数据时,若 Cache 未命中,会从 SRAM 加载整行(Cache Line,通常 32 字节)到 Cache。
- DMA 把新数据写入 SRAM 后,CPU 读到的可能仍是 Cache 中的旧行
- 更危险的是:若该行是 dirty 的,后续被替换时会覆盖掉 DMA 刚写入的数据
2.3 关键结论
-
CPU 写、DMA 读(发送方向):必须
Clean,把 Cache 写回 SRAM -
DMA 写、CPU 读(接收方向):必须
Invalidate,丢弃 Cache 旧副本 - 顺序错误或遗漏,都会导致数据损坏
三、正确操作顺序与 API
CMSIS 提供 SCB_CleanDCache_by_Addr 和 SCB_InvalidateDCache_by_Addr。
3.1 发送(CPU 写 → DMA 读)
// 1. CPU 填充缓冲区
memcpy(tx_buf, src, len);
// 2. Clean:确保数据写回 SRAM
SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, len);
// 3. 启动 DMA 发送
HAL_UART_Transmit_DMA(&huart1, tx_buf, len);
3.2 接收(DMA 写 → CPU 读)
// 1. 启动 DMA 接收前,先 Invalidate,丢弃旧 Cache
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, len);
HAL_UART_Receive_DMA(&huart1, rx_buf, len);
// 2. 等待 DMA 完成(中断/轮询)
// 3. 再次 Invalidate,确保读到 DMA 写入的新数据
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, len);
// 4. 此时 CPU 读取才是正确的
process(rx_buf, len);
注意:接收方向不能 Clean!Clean 会把 CPU 可能存在的旧 dirty 行写回,覆盖 DMA 数据。
四、完整示例:UART DMA 收发
#include "stm32h7xx_hal.h"
#define BUF_SIZE 64
// 必须 32 字节对齐,且大小为 Cache Line 整数倍
__attribute__((aligned(32))) uint8_t tx_buf[BUF_SIZE];
__attribute__((aligned(32))) uint8_t rx_buf[BUF_SIZE];
void uart_send_dma(uint8_t *data, uint16_t len)
{
memcpy(tx_buf, data, len);
SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, len);
HAL_UART_Transmit_DMA(&huart1, tx_buf, len);
}
void uart_start_receive(void)
{
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, BUF_SIZE);
HAL_UART_Receive_DMA(&huart1, rx_buf, BUF_SIZE);
}
// DMA 接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, BUF_SIZE);
// 现在可以安全读取 rx_buf
process_data(rx_buf, BUF_SIZE);
}
五、常见错误与排查清单
5.1 典型错误
- 只 Clean 不 Invalidate:接收方向读到旧数据
- 只 Invalidate 不 Clean:发送方向 DMA 发出旧数据
- 顺序颠倒:先启动 DMA 再 Clean/Invalidate,为时已晚
-
地址未对齐:
SCB_*_by_Addr要求 32 字节对齐,否则可能误伤相邻数据 - 长度非 Cache Line 整数倍:维护操作可能覆盖相邻变量
5.2 排查步骤
- 确认缓冲区用
__attribute__((aligned(32)))对齐 - 发送前:
Clean→ 启动 DMA - 接收前:
Invalidate→ 启动 DMA;接收后:再Invalidate - 用
SCB_InvalidateDCache()全量失效做对比测试,若问题消失则确认是 Cache 一致性 - 检查 MPU 配置:可将 DMA 缓冲区所在 SRAM 区域配置为 Non-Cacheable,一劳永逸
5.3 替代方案:MPU 配置 Non-Cacheable
MPU_Region_InitTypeDef MPU_Init;
HAL_MPU_Disable();
MPU_Init.Enable = MPU_REGION_ENABLE;
MPU_Init.BaseAddress = 0x30000000; // D2 SRAM
MPU_Init.Size = MPU_REGION_SIZE_64KB;
MPU_Init.Attributes = MPU_ACCESS_NOT_CACHEABLE |
MPU_ACCESS_NOT_BUFFERABLE;
MPU_Init.SubRegionDisable = 0;
HAL_MPU_ConfigRegion(&MPU_Init);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
六、注意事项
- Cache Line 是 32 字节,所有 DMA 缓冲区按 32 字节对齐,长度取 32 的整数倍
-
中断中调用 Cache 维护函数:
SCB_*是同步操作,耗时短,但避免在极高频率中断中频繁调用 - 多缓冲区场景:每个缓冲区独立维护,不要漏掉任何一个
- DMA 双缓冲(Double Buffer):切换缓冲区时同样要维护 Cache
- 调试器影响:调试时可能强制内存访问,掩盖问题,务必脱机测试
-
编译器优化:
volatile不能解决 Cache 一致性问题,必须用 Cache 维护或 MPU
七、总结
STM32H7 的 Cache 与 DMA 一致性问题是嵌入式开发中的经典“隐性 Bug”。记住口诀:发送先 Clean,接收先 Invalidate,接收后再 Invalidate。缓冲区对齐 32 字节,长度取整。若追求简单可靠,直接用 MPU 把 DMA 区域设为 Non-Cacheable。掌握这些,外设收发数据损坏问题将迎刃而解。