STM32H7 的 Cache 与 DMA 一致性坑:Clean/Invalidate 顺序写错导致数据错乱的排查实录
一、问题现场:ADC 数据每 3 个点就错 1 个
项目用 STM32H743 + ADC + DMA 采集 1kHz 正弦波,DMA 循环搬运 1024 点。现象很诡异:
- 前几次采集完全正常,跑几分钟后数据开始错乱;
- 错乱位置不固定,但总是成片出现;
- 关闭 D-Cache 后一切正常。
基本可以锁定:DMA 与 D-Cache 的数据一致性问题。
二、原理:为什么 H7 上 DMA 会“看不见”CPU 写的数据
STM32H7 的 Cortex-M7 带 16KB D-Cache。CPU 读写内存时,实际访问的是 Cache,而不是 SRAM。DMA 是总线主设备,它绕过 Cache 直接访问 SRAM。于是产生两类冲突:
- CPU 写、DMA 读:CPU 写的数据还在 Cache 里(Write-Back 策略),SRAM 里是旧值,DMA 搬走的是旧数据。
- DMA 写、CPU 读:DMA 把新数据写进 SRAM,但 CPU 读的是 Cache 里的旧副本,读到脏数据。
解决手段只有两个操作:
- Clean(清理):把 Cache 里的脏数据写回 SRAM,让 DMA 能读到新值。
- Invalidate(无效化):把 Cache 行标记为无效,强制 CPU 下次从 SRAM 重新加载,读到 DMA 的新值。
三、关键:顺序写错就是灾难
很多教程只讲“要 Clean/Invalidate”,却没强调顺序。正确原则是:
- DMA 发送前(CPU→DMA):先 Clean,再启动 DMA。
- DMA 接收后(DMA→CPU):先等 DMA 完成,再 Invalidate,然后 CPU 才能读。
最常见的错误写法:
// 错误示范:先 Invalidate 再启动 DMA 接收
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, len);
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)rx_buf, len/4);
问题在于:Invalidate 只是把 Cache 行标记无效,并不会把 Cache 里的脏数据写回 SRAM。如果这块缓冲区之前被 CPU 写过,脏数据仍留在 Cache 中。DMA 写入 SRAM 后,CPU 读时 Cache 未命中会重新加载——看似没问题,但如果 Invalidate 发生在 DMA 传输过程中,或 Cache 行与 DMA 写入区域部分重叠,就会把 DMA 刚写入的数据连同旧脏数据一起丢弃,造成错乱。
更隐蔽的是:Invalidate 操作本身可能丢弃尚未写回的脏数据。如果缓冲区在 Invalidate 前被 CPU 写过且未 Clean,这些数据就永久丢失了。
四、正确代码:ADC + DMA 双缓冲采集
#include "stm32h7xx_hal.h"
#define BUF_LEN 1024
#define BUF_BYTES (BUF_LEN * sizeof(uint16_t))
// 必须 32 字节对齐,且放在 DMA 可访问的 SRAM 区
__attribute__((aligned(32))) __attribute__((section(".RAM_D2")))
static uint16_t adc_buf[BUF_LEN];
ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;
// DMA 接收完成回调
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
// 1. 先无效化 Cache,让 CPU 能读到 DMA 写入的新数据
SCB_InvalidateDCache_by_Addr((uint32_t*)adc_buf, BUF_BYTES);
// 2. 此时读取才是安全的
process_adc_data(adc_buf, BUF_LEN);
}
void adc_dma_start(void)
{
// 启动前先 Clean,确保 SRAM 与 Cache 一致
SCB_CleanDCache_by_Addr((uint32_t*)adc_buf, BUF_BYTES);
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf, BUF_LEN);
}
如果是 CPU 写、DMA 发送(如 UART 发送):
void uart_dma_send(uint8_t *data, uint16_t len)
{
// 先 Clean,把 CPU 写的脏数据刷回 SRAM,DMA 才能读到
SCB_CleanDCache_by_Addr((uint32_t*)data, len);
HAL_UART_Transmit_DMA(&huart1, data, len);
}
五、避坑清单
-
缓冲区必须 32 字节对齐:Cortex-M7 Cache 行是 32 字节,未对齐会导致 Clean/Invalidate 波及相邻数据。用
__attribute__((aligned(32)))。 -
长度按 32 字节向上取整:
SCB_*DCache_by_Addr内部会处理,但自己算长度时要注意。 - 不要在 DMA 传输过程中 Invalidate:必须在传输完成中断里做,否则会丢数据。
- MPU 配置为 Write-Through 或 Non-Cacheable:对 DMA 缓冲区最省心,直接免维护。
- DMA 缓冲区不要和栈、全局变量混放:避免 Cache 行共享导致误伤。
- 调试时先关 D-Cache 验证:能快速定位是否为一致性问题。
六、MPU 配置:一劳永逸的方案
如果不想每次手动维护 Cache,可以用 MPU 把 DMA 缓冲区设为 Non-Cacheable:
void MPU_Config(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_Init = {0};
MPU_Init.Enable = MPU_REGION_ENABLE;
MPU_Init.BaseAddress = 0x30000000; // SRAM D2 区
MPU_Init.Size = MPU_REGION_SIZE_64KB;
MPU_Init.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_Init.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
MPU_Init.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
MPU_Init.IsShareable = MPU_ACCESS_SHAREABLE;
MPU_Init.Number = MPU_REGION_NUMBER0;
MPU_Init.TypeExtField = MPU_TEX_LEVEL1;
MPU_Init.SubRegionDisable = 0x00;
MPU_Init.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
HAL_MPU_ConfigRegion(&MPU_Init);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
把 DMA 缓冲区放到 Non-Cacheable 区域后,CPU 和 DMA 看到的都是同一份 SRAM,彻底告别一致性问题。代价是 CPU 访问该区域会慢一些,但对 DMA 数据流来说完全值得。
七、总结
STM32H7 的 Cache 是把双刃剑:用好了性能翻倍,用错了数据错乱。记住三句话:
- CPU 写、DMA 读 → 先 Clean;
- DMA 写、CPU 读 → 后 Invalidate;
- 拿不准就用 MPU 设 Non-Cacheable。
顺序错一步,调试两行泪。希望这篇实录能帮你少走弯路。