STM32H7 D-Cache与DMA一致性实战:MPU配置与clean/invalidate踩坑记录

一、为什么H7必须开D-Cache?

STM32H7的Cortex-M7内核主频可达480MHz,但Flash和SRAM的访问速度远低于内核。若关闭D-Cache,CPU访问SRAM的等待周期会严重拖累性能。开启D-Cache后,频繁访问的数据被缓存在内核附近,实测CoreMark可提升30%以上。

但D-Cache引入了一个经典问题:CPU看到的是Cache中的副本,而DMA直接访问物理内存。当两者操作同一块缓冲区时,数据不一致就发生了。

二、Cache一致性问题原理

  • 写通(Write-Through) vs 写回(Write-Back):H7的D-Cache默认是写回模式。CPU写数据时只更新Cache,不立即写回SRAM,DMA读到的就是旧数据。
  • 读分配(Read Allocate):CPU读数据时,若Cache未命中,会从SRAM加载到Cache。若DMA在此期间更新了SRAM,CPU读到的仍是旧Cache。

典型场景:ADC+DMA采集数据到缓冲区,CPU读取缓冲区。若DMA写SRAM后CPU读Cache,就会得到旧值。

三、MPU配置:划定非缓存区域

最稳妥的方案是用MPU将DMA缓冲区设为Non-Cacheable,彻底避免一致性问题。

配置步骤

  1. 使能MPU:SCB_EnableMPU() 前先 SCB_EnableICache()SCB_EnableDCache()
  2. 定义MPU区域:基地址为缓冲区地址,大小需2的幂且≥32字节。
  3. 设置属性:TEX=1, C=0, B=0, S=1 表示Shareable Device,即非缓存。
#include "stm32h7xx.h"

// 假设DMA缓冲区定义在DTCM之外,如AXI SRAM
__attribute__((section(".dma_buffer"), aligned(32)))
uint8_t dma_buffer[1024];

void MPU_Config(void)
{
    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    HAL_MPU_Disable();

    // 配置DMA缓冲区区域为非缓存
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = (uint32_t)dma_buffer;
    MPU_InitStruct.Size = MPU_REGION_SIZE_1KB; // 根据实际大小调整
    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);
}

注意:链接脚本中需将 .dma_buffer 段放到AXI SRAM(0x24000000)等可被MPU覆盖的区域,不能放在DTCM(0x20000000),因为DTCM不支持Cache,也无需MPU配置。

四、Clean/Invalidate操作:动态维护一致性

若缓冲区必须缓存(如频繁CPU读写),则需在DMA传输前后手动维护Cache。

  • Clean:将Cache中已修改的数据写回SRAM。用于CPU写→DMA读之前。
  • Invalidate:丢弃Cache中数据,强制下次从SRAM加载。用于DMA写→CPU读之前。
// CPU准备数据发给DMA前:Clean
SCB_CleanDCache_by_Addr((uint32_t *)dma_buffer, sizeof(dma_buffer));

// DMA接收完成后,CPU读取前:Invalidate
SCB_InvalidateDCache_by_Addr((uint32_t *)dma_buffer, sizeof(dma_buffer));

关键点:地址必须32字节对齐,长度建议向上取整到32字节倍数。

五、完整实战:ADC+DMA+Cache

以ADC连续采样为例,DMA循环写入缓冲区,CPU定时读取。

#define ADC_BUF_LEN 256
__attribute__((section(".dma_buffer"), aligned(32)))
static uint16_t adc_buf[ADC_BUF_LEN];

void ADC_DMA_Init(void)
{
    // ADC和DMA初始化略,DMA配置为循环模式,目标地址adc_buf
    // ...
    HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_buf, ADC_BUF_LEN);
}

void Process_ADC_Data(void)
{
    // 读取前Invalidate,确保拿到DMA最新数据
    SCB_InvalidateDCache_by_Addr((uint32_t *)adc_buf, sizeof(adc_buf));

    uint32_t sum = 0;
    for (int i = 0; i < ADC_BUF_LEN; i++) {
        sum += adc_buf[i];
    }
    float avg = (float)sum / ADC_BUF_LEN;
    // 使用avg...
}

若采用MPU非缓存方案,则无需调用Invalidate,直接读取即可。

六、踩坑记录与注意事项

  • 坑1:缓冲区跨Cache行。Cache行32字节,若缓冲区未对齐,clean/invalidate会波及相邻数据。务必使用aligned(32)
  • 坑2:DTCM不能配MPU。DTCM地址0x20000000默认非缓存,但MPU无法覆盖,DMA也无法访问DTCM(部分型号DMA1可访问,但DMA2不行)。DMA缓冲区请放AXI SRAM。
  • 坑3:Invalidate丢失未写回数据。若CPU刚写过缓冲区,立即Invalidate会丢弃修改。正确顺序:先Clean再Invalidate,或确保无写操作。
  • 坑4:多缓冲区共享Cache行。两个DMA缓冲区若在同一32字节行内,操作一个会影响另一个。用__attribute__((aligned(32)))隔离。
  • 坑5:中断中调用Cache维护函数SCB_*DCache_by_Addr执行时间较长,在高速中断中频繁调用可能导致实时性下降。建议在任务级处理。

七、总结

STM32H7的D-Cache是性能利器,但必须谨慎处理与DMA的一致性。首选MPU非缓存方案,简单可靠;若必须缓存,则严格遵循“CPU写后Clean,DMA写后Invalidate”的原则,并注意对齐和边界。掌握这些,就能在H7上既跑得快又不出错。