STM32F4 D-Cache 与 SDRAM 数据一致性:从踩坑到硬件屏障指令的正确插入时机

一、问题背景:D-Cache 的“甜蜜陷阱”

STM32F4 系列(如 STM32F429/439)内置了 4KB 的 D-Cache 和 I-Cache,用于加速对内部 SRAM 和外部存储器的访问。然而,当外部 SDRAM 被映射到内存空间(如 Bank1 的 0xC0000000 起始地址)时,D-Cache 的写回(Write-back)策略会引入严重的一致性风险。

典型踩坑场景

  • 使用 DMA 将数据从 SDRAM 搬运到外设,但 CPU 先前写入 SDRAM 的数据仍滞留在 Cache 中,DMA 读取到的是陈旧数据。
  • 外设(如摄像头)通过 DMA 将数据写入 SDRAM,CPU 随后读取时却命中 Cache 中的旧值。
  • 多核系统中,CPU0 写数据到 SDRAM,CPU1 读取时因各自 Cache 不同步而数据错乱。

二、原理剖析:为什么需要硬件屏障?

2.1 D-Cache 的工作模式

STM32F4 的 D-Cache 采用**写回(Write-back)**策略:CPU 写操作先更新 Cache 行(Line),仅当缓存行被替换或显式 Clean 操作时才写回 SDRAM。这虽然减少了总线访问,但导致 SDRAM 中的数据可能滞后于 CPU 的视角。

2.2 一致性问题的根源

  • CPU 写 → DMA 读:CPU 写数据到 SDRAM 地址,但数据停留在 Cache。DMA 直接访问 SDRAM,读到的仍是旧数据。
  • DMA 写 → CPU 读:DMA 将新数据写入 SDRAM,但 CPU 的 Cache 中可能保留了旧数据,导致 CPU 读操作命中 Cache 而得到错误值。

2.3 硬件屏障指令的作用

ARM Cortex-M4 提供了两条内存屏障指令:

  • DMB(Data Memory Barrier):确保此指令之前的所有内存访问(读/写)完成后,才执行后续的内存访问。
  • DSB(Data Synchronization Barrier):确保此指令之前的所有内存访问完成后,才执行任何后续指令(包括非内存访问)。

在 Cache 操作中,屏障指令用于保证 Cache 维护操作(如 Clean/Invalidate)的顺序性,防止编译器或硬件乱序执行导致维护失效。

三、配置步骤:SDRAM 与 D-Cache 的初始化

3.1 硬件环境

  • MCU:STM32F429ZIT6(主频 180MHz)
  • SDRAM:IS42S16400J(1M×16×4 banks),挂载在 FMC Bank1
  • 开发环境:STM32CubeIDE + HAL 库

3.2 初始化顺序(关键!)

  1. 使能 D-Cache(在启动代码中,SystemInit 之后)
  2. 初始化 FMC 和 SDRAM(通过 MX_FMC_Init()MX_SDRAM_Init()
  3. 配置 MPU(Memory Protection Unit)为 SDRAM 区域设置缓存策略

重要:MPU 必须配置为 Write-back, Write-allocate 模式,否则 D-Cache 对 SDRAM 无效。

// MPU 配置示例:SDRAM 区域(0xC0000000, 8MB)
void MPU_Config(void)
{
    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    HAL_MPU_Disable();

    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = 0xC0000000;
    MPU_InitStruct.Size = MPU_REGION_SIZE_8MB;
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE;
    MPU_InitStruct.IsCacheable = MPU_ACCESS_CACHEABLE;
    MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
    MPU_InitStruct.Number = MPU_REGION_NUMBER0;
    MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL1;  // Write-back, Write-allocate
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE;

    HAL_MPU_ConfigRegion(&MPU_InitStruct);
    HAL_MPU_Enable(MPU_CONTROL_HRDM_ENABLE);
}

四、核心代码:Cache 维护与屏障指令的插入时机

4.1 CPU 写 SDRAM 后,DMA 读之前

场景:CPU 将图像数据写入 SDRAM,然后启动 DMA 传输到 LCD。

// 将数据写入 SDRAM 缓冲区
uint32_t *buf = (uint32_t *)0xC0000000;
for (int i = 0; i < 1024; i++) {
    buf[i] = i * 3;
}

// 关键步骤:Clean D-Cache,将脏数据写回 SDRAM
SCB_CleanDCache_by_Addr((uint32_t *)buf, 4096);

// 插入数据内存屏障,确保 Clean 操作完成
__DMB();

// 启动 DMA 传输(从 SDRAM 读取)
HAL_DMA_Start_IT(&hdma_memtomem_dma2_stream0, (uint32_t)buf, (uint32_t)lcd_buf, 1024);

4.2 DMA 写 SDRAM 后,CPU 读之前

场景:DMA 从 ADC 采集数据存入 SDRAM,CPU 随后处理。

// 启动 DMA 传输(写入 SDRAM)
HAL_DMA_Start_IT(&hdma_adc1, (uint32_t)&adc_value, (uint32_t)buf, 512);

// 等待 DMA 传输完成(中断或轮询)
while (HAL_DMA_GetState(&hdma_adc1) != HAL_DMA_STATE_READY);

// 关键步骤:Invalidate D-Cache,使缓存行失效,强制从 SDRAM 重新加载
SCB_InvalidateDCache_by_Addr((uint32_t *)buf, 2048);

// 插入数据同步屏障,确保 Invalidate 操作完成
__DSB();

// 现在 CPU 可以安全读取 buf 中的数据
uint32_t sum = 0;
for (int i = 0; i < 512; i++) {
    sum += buf[i];
}

4.3 双核共享 SDRAM(如 Cortex-M4 + Cortex-M7)

场景:M4 核写数据,M7 核读数据。

// M4 核(写者)
void M4_WriteData(uint32_t *addr, uint32_t *data, uint32_t len)
{
    memcpy(addr, data, len * 4);
    SCB_CleanDCache_by_Addr(addr, len * 4);
    __DSB();  // 确保 Clean 完成,并同步到 SDRAM
    // 通知 M7 核(通过共享标志或中断)
    *shared_flag = 1;
    __DMB();  // 确保标志写入在数据之后可见
}

// M7 核(读者)
void M7_ReadData(uint32_t *addr, uint32_t *out, uint32_t len)
{
    // 等待共享标志
    while (*shared_flag != 1);
    __DMB();  // 确保标志读取在数据读取之前
    SCB_InvalidateDCache_by_Addr(addr, len * 4);
    __DSB();
    memcpy(out, addr, len * 4);
}

五、注意事项与常见误区

  • 屏障指令不能替代 Cache 维护:DMB/DSB 只保证内存访问顺序,不负责数据写回或失效。必须先执行 Clean/Invalidate,再用屏障确保其完成。
  • 地址对齐SCB_CleanDCache_by_AddrSCB_InvalidateDCache_by_Addr 要求地址按 32 字节对齐,长度也需为 32 的倍数,否则需手动处理边界。
  • 避免频繁维护:Cache 维护开销较大,建议在批量传输前后一次性处理,而非逐字节操作。
  • MPU 配置错误:若 SDRAM 区域未配置为 Cacheable,D-Cache 将不生效,但性能下降;若配置为 Write-through,则无需 Clean,但性能仍受影响。
  • 中断上下文:在中断服务函数中执行 Cache 维护时,注意中断优先级和嵌套,避免长时间阻塞。

六、总结

D-Cache 与 SDRAM 的一致性问题是 STM32F4 开发中的经典陷阱。通过理解写回策略的本质,正确使用 Clean/Invalidate 操作,并在关键节点插入 DMB/DSB 屏障,可以彻底避免数据错乱。记住口诀:“写后 Clean,读前 Invalidate,屏障紧跟其后”。希望本文能帮你少走弯路,让嵌入式开发更加稳健。