STM32H7 D-Cache与DMA一致性实战:Clean/Invalidate误用导致的数据错乱排查实录

一、问题现象:ADC数据随机跳变

最近在调试一个STM32H750项目时,遇到一个诡异问题:使用ADC+DMA采集1000点数据,发现数组前半部分正常,后半部分偶尔出现随机值,且复现概率约30%。关闭D-Cache后问题消失,但CPU性能大幅下降。

初步判断是D-Cache与DMA的一致性问题。STM32H7的Cortex-M7内核带有16KB D-Cache,默认写回策略。DMA直接访问SRAM,不经过Cache,导致CPU看到的缓存数据与物理内存不一致。

二、原理:为什么需要Clean/Invalidate

  • Clean(清理):将Cache中已修改的数据写回物理内存,保证DMA读到最新数据。
  • Invalidate(无效化):将Cache中对应行标记为无效,强制CPU下次读取时从物理内存重新加载,保证CPU读到DMA写入的新数据。

关键原则:

  • CPU写、DMA读:在启动DMA传输前,对数据缓冲区执行Clean操作。
  • DMA写、CPU读:在DMA传输完成后,对数据缓冲区执行Invalidate操作。
  • 双向传输:先Clean再Invalidate(顺序不能反!)。

三、排查过程:误用Invalidate导致数据丢失

原代码在ADC DMA启动前调用了SCB_InvalidateDCache_by_Addr(),意图“清空缓存”。但此时缓冲区中已有CPU写入的旧数据(未Clean),Invalidate直接丢弃了这些脏数据,导致DMA读到的是内存中的随机值。

正确做法:启动DMA前应执行Clean,而不是Invalidate。

四、正确配置步骤

1. 使能D-Cache

SCB_EnableICache();
SCB_EnableDCache();

2. 配置MPU将DMA缓冲区设为Non-Cacheable(推荐)

MPU_Region_InitTypeDef MPU_InitStruct = {0};
HAL_MPU_Disable();
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0x30000000; // D2 SRAM
MPU_InitStruct.Size = MPU_REGION_SIZE_64KB;
MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
MPU_InitStruct.IsShareable = MPU_ACCESS_SHAREABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL1;
MPU_InitStruct.SubRegionDisable = 0x00;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);

3. 若必须使用Cacheable内存,手动维护

#define ADC_BUF_SIZE 1000
__attribute__((aligned(32))) uint16_t adc_buf[ADC_BUF_SIZE];

// 启动DMA前:Clean(确保内存数据最新)
SCB_CleanDCache_by_Addr((uint32_t*)adc_buf, ADC_BUF_SIZE * 2);
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf, ADC_BUF_SIZE);

// DMA完成中断中:Invalidate(丢弃缓存,强制重读)
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
    SCB_InvalidateDCache_by_Addr((uint32_t*)adc_buf, ADC_BUF_SIZE * 2);
    // 此时CPU读取adc_buf才能得到DMA写入的新数据
}

五、完整示例:ADC+DMA+Cache维护

#include "stm32h7xx_hal.h"

#define BUF_SIZE 256
__attribute__((aligned(32))) uint16_t adc_data[BUF_SIZE];
ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;

void ADC_DMA_Init(void)
{
    // ... ADC和DMA初始化代码省略
    HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_data, BUF_SIZE);
}

// 每次启动采集前调用
void Start_ADC_Capture(void)
{
    SCB_CleanDCache_by_Addr((uint32_t*)adc_data, BUF_SIZE * 2);
    HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_data, BUF_SIZE);
}

// DMA传输完成回调
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
    SCB_InvalidateDCache_by_Addr((uint32_t*)adc_data, BUF_SIZE * 2);
    // 处理adc_data
}

六、注意事项

  • 地址必须32字节对齐:Cortex-M7 Cache行大小为32字节,操作地址和长度需按32字节对齐,否则可能误伤相邻数据。
  • 长度向上取整SCB_*DCache_by_Addr的长度参数应为32的倍数,建议(size + 31) & ~31
  • 避免在中断中频繁Invalidate:Invalidate会丢弃缓存,可能影响性能,建议结合MPU将DMA缓冲区设为Non-Cacheable。
  • DMA描述符本身也要维护:若使用链表模式,描述符内存同样需要Clean。
  • 多核场景:STM32H7双核间共享内存需额外考虑Cache一致性,建议使用Non-Cacheable区域或硬件信号量。

七、总结

D-Cache与DMA的一致性问题是STM32H7开发中的高频坑点。核心口诀:CPU写DMA读先Clean,DMA写CPU读后Invalidate,双向操作先Clean后Invalidate。最稳妥的方案是通过MPU将DMA缓冲区配置为Non-Cacheable,从根源上避免一致性问题。若必须使用Cacheable内存,务必严格遵循维护顺序并保证32字节对齐。