STM32F4 D-Cache 与 SDRAM 数据一致性:根源剖析与三种修复策略

问题根源:Cache 写回策略与 DMA 的冲突

STM32F4 系列(如 STM32F407、F429)内置了 D-Cache(数据缓存),用于加速 CPU 对内存的访问。D-Cache 默认采用**写回(Write-back)**策略:CPU 写数据时,仅更新 Cache 行,并标记为脏(Dirty),数据不会立即写入主存(如 SDRAM)。当 DMA 外设(如摄像头、以太网)直接访问 SDRAM 时,它绕过 Cache,直接读写物理内存。这导致两个问题:

  • DMA 读取到旧数据:CPU 刚写入 SDRAM 的数据还滞留在 Cache 中,DMA 从 SDRAM 读到的仍是旧值。
  • DMA 写入被覆盖:DMA 更新了 SDRAM 内容,但 Cache 中仍保留旧副本,CPU 后续读取 Cache 时得到过期数据。

这种不一致性在高速数据传输场景下尤为致命,可能引发图像花屏、网络丢包或控制逻辑错乱。

修复策略一:Cache 清理与失效(软件控制)

最直接的方法是手动维护 Cache 一致性。在关键操作前后,调用 CMSIS 提供的函数:

  • SCB_CleanDCache():将脏 Cache 行写回主存。
  • SCB_InvalidateDCache():使 Cache 行失效,强制 CPU 下次从主存读取。

配置步骤

  1. 确保已启用 D-Cache(在 SystemInit 或主函数中调用 SCB_EnableDCache())。
  2. 在 CPU 写数据后、DMA 读取前,执行 Clean 操作。
  3. 在 DMA 写数据后、CPU 读取前,执行 Invalidate 操作。

代码示例

// 假设 buffer 位于 SDRAM,长度为 1024 字节
uint8_t buffer[1024] __attribute__((section(".sdram")));

// CPU 写入数据
memcpy(buffer, src, sizeof(buffer));

// 清理 Cache,确保数据写回 SDRAM
SCB_CleanDCache();

// 启动 DMA 传输(读取 buffer)
DMA_Start_Read(buffer);

// 等待 DMA 完成
while (DMA_IsBusy());

// 使 Cache 失效,确保 CPU 读取最新数据
SCB_InvalidateDCache();

// CPU 读取数据
process_data(buffer);

注意:Clean 和 Invalidate 会阻塞 CPU,频繁调用会降低性能。建议按 Cache 行大小(32 字节)对齐缓冲区,并尽量批量操作。

修复策略二:MPU 配置非缓存区域(硬件隔离)

通过内存保护单元(MPU)将 SDRAM 区域配置为 非缓存(Non-cacheable),使 CPU 访问 SDRAM 时直接读写主存,绕过 D-Cache。此方法彻底避免一致性问题,但牺牲了缓存带来的性能提升。

配置步骤

  1. 初始化 MPU,设置区域属性为 Normal memory, Non-cacheable
  2. 将 SDRAM 的地址范围(如 0xC0000000 起始,大小 8MB)配置为独立区域。
  3. 确保 MPU 区域优先级正确,避免与默认配置冲突。

代码示例

void MPU_Config_SDRAM_NonCacheable(void) {
    MPU_Region_InitTypeDef MPU_InitStruct;

    // 禁用 MPU 进行配置
    HAL_MPU_Disable();

    // 配置 SDRAM 区域(基址 0xC0000000,大小 8MB)
    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_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);

    // 使能 MPU
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

注意:非缓存区域访问速度较慢,若 SDRAM 用于帧缓冲等高频读写场景,需评估性能影响。另外,MPU 配置需在启用 D-Cache 之前完成。

修复策略三:DMA 与 CPU 协同管理(双缓冲)

采用双缓冲机制,让 DMA 和 CPU 交替使用不同内存区域,避免同时访问同一地址。此策略无需频繁 Cache 操作,适合持续数据流场景。

原理

  • 分配两个缓冲区 A 和 B,均位于 SDRAM。
  • CPU 处理缓冲区 A 时,DMA 填充缓冲区 B。
  • 切换时,仅需对当前缓冲区执行一次 Clean/Invalidate,且可流水线化。

配置步骤

  1. 定义两个缓冲区,并确保它们按 Cache 行对齐(32 字节)。
  2. 使用 DMA 中断或回调,在传输完成时切换缓冲区。
  3. 在切换前,对旧缓冲区执行 Clean,对新缓冲区执行 Invalidate。

代码示例

#define BUFFER_SIZE 2048
uint8_t buffer_A[BUFFER_SIZE] __attribute__((aligned(32), section(".sdram")));
uint8_t buffer_B[BUFFER_SIZE] __attribute__((aligned(32), section(".sdram")));

volatile uint8_t current_buffer = 0; // 0: A, 1: B

void DMA_Transfer_Complete_Callback(void) {
    // 清理旧缓冲区(DMA 即将写入)
    if (current_buffer == 0) {
        SCB_CleanDCache_by_Addr((uint32_t*)buffer_A, BUFFER_SIZE);
        // 启动 DMA 填充 buffer_A
        DMA_Start_Receive(buffer_A, BUFFER_SIZE);
    } else {
        SCB_CleanDCache_by_Addr((uint32_t*)buffer_B, BUFFER_SIZE);
        DMA_Start_Receive(buffer_B, BUFFER_SIZE);
    }

    // 使新缓冲区失效(CPU 即将读取)
    if (current_buffer == 0) {
        SCB_InvalidateDCache_by_Addr((uint32_t*)buffer_B, BUFFER_SIZE);
        process_data(buffer_B);
    } else {
        SCB_InvalidateDCache_by_Addr((uint32_t*)buffer_A, BUFFER_SIZE);
        process_data(buffer_A);
    }

    current_buffer = !current_buffer;
}

注意:使用 SCB_CleanDCache_by_AddrSCB_InvalidateDCache_by_Addr 可精确操作指定地址范围,避免全缓存清理的开销。确保缓冲区地址和大小对齐到 32 字节。

总结与选型建议

  • 策略一(手动 Clean/Invalidate)简单直接,适合低频、小数据量场景,但需注意性能损耗。
  • 策略二(MPU 非缓存)彻底消除问题,适合对一致性要求极高、性能要求不苛刻的应用。
  • 策略三(双缓冲)兼顾性能与一致性,适合持续数据流(如音视频、网络),但实现复杂度较高。

实际项目中,可结合使用:例如,将关键控制结构放在非缓存区域,而大数据缓冲区采用双缓冲。务必在开发初期就规划好内存布局,避免后期调试的噩梦。

最后,建议在调试时启用 D-Cache 的监视功能(如 D-Cache 错误中断),并利用逻辑分析仪或仿真器观察总线行为,以验证一致性修复是否彻底。