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,彻底避免一致性问题。
配置步骤
- 使能MPU:
SCB_EnableMPU()前先SCB_EnableICache()和SCB_EnableDCache()。 - 定义MPU区域:基地址为缓冲区地址,大小需2的幂且≥32字节。
- 设置属性:
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上既跑得快又不出错。