STM32F4 系列 D-Cache 与 SDRAM 数据一致性问题的三种实战修复策略

问题根源:D-Cache 与 SDRAM 的“缓存错觉”

STM32F4 系列(如 STM32F407/427/429)内置了 4KB 或 8KB 的 D-Cache,用于加速 CPU 对内存的访问。然而,D-Cache 是写回(write-back)策略,意味着 CPU 写入的数据可能暂存在 Cache 中,并未立即同步到外部 SDRAM。当外设(如 DMA、LTDC 控制器)直接访问 SDRAM 时,它们看到的是旧数据,而 CPU 却认为数据已更新,导致数据不一致。

典型场景:

  • 使用 LTDC 驱动 LCD,帧缓冲位于 SDRAM,CPU 更新帧缓冲后,LTDC 仍显示旧画面。
  • 使用 DMA 从 SDRAM 搬运数据到外设,但 CPU 刚写入的数据还在 Cache 中,DMA 读到的是过期数据。

策略一:软件手动维护 Cache(Clean & Invalidate)

原理

最直接的方法是在关键操作前后,手动调用 CMSIS 提供的 Cache 维护函数,强制将 Cache 中的数据写回 SDRAM(Clean),或使 Cache 行失效(Invalidate),以便下次读取时从 SDRAM 重新加载。

配置步骤

  1. 确保在 system_stm32f4xx.c 中启用了 D-Cache(SCB_EnableDCache())。
  2. 在 CPU 写入 SDRAM 数据后,调用 SCB_CleanDCache() 或按地址范围清理。
  3. 在 DMA 写入 SDRAM 后,CPU 读取前,调用 SCB_InvalidateDCache()

代码示例

#include "stm32f4xx_hal.h"

// 假设 SDRAM 帧缓冲地址为 0xC0000000,大小 320*240*2 字节
#define FRAME_BUFFER_ADDR  0xC0000000
#define FRAME_BUFFER_SIZE  (320*240*2)

void UpdateFrameBuffer(uint8_t *data) {
    // CPU 写入数据到帧缓冲
    memcpy((void*)FRAME_BUFFER_ADDR, data, FRAME_BUFFER_SIZE);
    
    // 关键:将 Cache 中数据写回 SDRAM
    SCB_CleanDCache_by_Addr((uint32_t*)FRAME_BUFFER_ADDR, FRAME_BUFFER_SIZE);
}

void ReadFrameBuffer(uint8_t *dest) {
    // 如果 DMA 或外设修改了 SDRAM,CPU 读取前需使 Cache 失效
    SCB_InvalidateDCache_by_Addr((uint32_t*)FRAME_BUFFER_ADDR, FRAME_BUFFER_SIZE);
    memcpy(dest, (void*)FRAME_BUFFER_ADDR, FRAME_BUFFER_SIZE);
}

注意事项

  • 地址必须按 32 字节对齐(Cache line 大小),否则无效。
  • 频繁调用会影响性能,适合低频操作。
  • 务必包含 core_cm4.h 或通过 HAL 库间接包含。

策略二:MPU 配置 SDRAM 区域为 Non-Cacheable

原理

通过 MPU(Memory Protection Unit)将 SDRAM 所在的地址区域设置为“不可缓存”或“写通”(write-through),这样 CPU 访问 SDRAM 时直接绕过 D-Cache,从根本上避免一致性问题。适合对性能要求不高但要求数据绝对一致的场景。

配置步骤

  1. 启用 MPU 并配置一个区域,覆盖 SDRAM 地址范围。
  2. 设置属性为 MPU_REGION_NO_CACHE(不缓存)或 MPU_REGION_WRITE_THROUGH
  3. 在系统初始化时调用 MPU 配置函数。

代码示例

#include "stm32f4xx_hal.h"

void MPU_Config(void) {
    MPU_Region_InitTypeDef MPU_InitStruct;
    
    __HAL_RCC_MPU_CLK_ENABLE();
    
    HAL_MPU_Disable();
    
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = 0xC0000000;  // SDRAM 起始地址
    MPU_InitStruct.Size = MPU_REGION_SIZE_8MB; // 根据实际大小调整
    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_NOT_SHAREABLE;
    MPU_InitStruct.Number = MPU_REGION_NUMBER0;
    MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
    
    HAL_MPU_ConfigRegion(&MPU_InitStruct);
    
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

int main(void) {
    HAL_Init();
    MPU_Config();
    // 后续 SDRAM 访问无需手动维护 Cache
}

注意事项

  • 需要根据 SDRAM 实际大小调整区域大小,且地址必须对齐。
  • 禁用 Cache 会降低 CPU 访问 SDRAM 的速度,但外设访问更可靠。
  • MPU 配置必须在启用 D-Cache 之前或之后均可,但建议在系统初始化早期完成。

策略三:DMA 双缓冲 + Cache 协同

原理

对于高频数据流(如 ADC 采样、LCD 刷新),手动维护 Cache 会拖慢速度。双缓冲策略:CPU 写入 Buffer A,DMA 从 Buffer B 读取,交替使用。在切换缓冲时,只对当前使用的缓冲做 Cache 维护,减少维护频率。同时,利用 DMA 的传输完成中断来同步数据。

配置步骤

  1. 在 SDRAM 中分配两个缓冲区(Buffer A 和 B)。
  2. CPU 写 Buffer A 时,Buffer B 正被 DMA 使用。
  3. 写完后 Clean Buffer A 的 Cache,然后切换 DMA 源地址到 Buffer A。
  4. DMA 完成中断中,Invalidate Buffer B 的 Cache(因为 DMA 可能修改了 B),并通知 CPU 处理。

代码示例

#define BUFFER_SIZE 1024
uint8_t bufferA[BUFFER_SIZE] __attribute__((section(".sdram")));
uint8_t bufferB[BUFFER_SIZE] __attribute__((section(".sdram")));

volatile uint8_t dma_done = 0;

void DMA_Transfer_Start(uint8_t *src) {
    // 配置 DMA 从 src 搬运数据到外设(如 UART)
    // 此处省略具体 DMA 初始化
}

void CPU_Process_Data(uint8_t *buf) {
    // 处理数据
}

void DMA_IRQHandler(void) {
    if (dma_done) {
        // 上一次传输完成,处理 Buffer B(假设 B 刚被 DMA 写入)
        SCB_InvalidateDCache_by_Addr((uint32_t*)bufferB, BUFFER_SIZE);
        CPU_Process_Data(bufferB);
        
        // 切换 DMA 源到 Buffer A,准备下一次
        DMA_Transfer_Start(bufferA);
    } else {
        // 处理 Buffer A
        SCB_InvalidateDCache_by_Addr((uint32_t*)bufferA, BUFFER_SIZE);
        CPU_Process_Data(bufferA);
        DMA_Transfer_Start(bufferB);
    }
    dma_done = !dma_done;
}

void main_loop(void) {
    // 初始化 DMA 和缓冲
    // CPU 写 bufferA 或 bufferB 后,调用 SCB_CleanDCache_by_Addr 写回
    // 然后启动 DMA
}

注意事项

  • 双缓冲需要更多内存,但能显著提高吞吐量。
  • 确保 DMA 传输完成中断优先级高于 CPU 写操作,避免竞态。
  • 缓冲地址需按 32 字节对齐,否则 Cache 维护无效。

总结与选型建议

  • 低频操作(如按键响应、配置更新):策略一(手动 Clean/Invalidate)最简单,代码侵入小。
  • 高性能且数据一致性要求高(如 LCD 显示):策略二(MPU 非缓存)最稳妥,但牺牲一点 CPU 性能。
  • 高频数据流(如音频、ADC):策略三(双缓冲)最佳,平衡性能与一致性。

实际项目中,可组合使用:例如用 MPU 将 SDRAM 设为写通,同时配合双缓冲减少 Cache 维护开销。务必在开发初期就考虑 Cache 一致性,否则后期调试会非常痛苦。希望本文能帮你彻底解决 STM32F4 与 SDRAM 的“缓存错觉”问题。