引言:D-Cache 的甜蜜与陷阱

STM32F4 系列(如 STM32F429/439)内置了 4KB 或 8KB 的 D-Cache,用于加速 CPU 对内部 SRAM 和外部存储器的访问。然而,当外部 SDRAM 被映射到 0xC0000000 区域时,D-Cache 的写回(Write-back)策略会导致 CPU 写入的数据暂存于 Cache 行中,而 DMA 控制器(如 LTDC、SDMMC)直接访问 SDRAM 时,看到的却是陈旧数据。反之,DMA 写入 SDRAM 后,CPU 可能从 Cache 中读到旧值。这种数据不一致性轻则导致显示花屏,重则引发文件系统损坏。

原理剖析:为什么 D-Cache 与 SDRAM 天生冲突

1. D-Cache 的写回机制

Cortex-M4 的 D-Cache 采用写回(Write-back)策略:CPU 执行写操作时,数据仅写入 Cache 行并标记为脏(Dirty),并不会立即更新到 SDRAM。只有当 Cache 行被替换或显式 Clean 操作时,数据才被写回。这提升了性能,但破坏了 CPU 与 DMA 之间的内存一致性。

2. SDRAM 的不可缓存区域设置

STM32F4 的 MPU(Memory Protection Unit)允许将 SDRAM 区域配置为 Write-through 或 Non-cacheable。但许多开发者为了性能,默认将整个 SDRAM 设置为 Cacheable,从而埋下隐患。

3. 硬件屏障指令的角色

Cortex-M4 提供三条内存屏障指令:

  • DMB(Data Memory Barrier):确保在 DMB 之前的所有内存访问完成后,才执行后续的内存访问。
  • DSB(Data Synchronization Barrier):等待所有内存访问完成,包括写缓冲和 Cache 操作。
  • ISB(Instruction Synchronization Barrier):清空流水线,确保后续指令从内存重新获取。

在 D-Cache 场景下,DMB 和 DSB 用于保证 Cache 维护操作(如 Clean/Invalidate)的顺序性,而 ISB 通常用于上下文切换或修改系统寄存器后。

实战踩坑:一个典型的 DMA 传输数据不一致案例

假设使用 SDRAM 作为 LCD 的帧缓冲区,并通过 DMA2D 进行图形加速。当 CPU 直接写入像素数据到 SDRAM 后,立即启动 DMA2D 读取该区域进行混合操作,结果出现花屏。原因:CPU 写入的数据仍停留在 D-Cache 中,DMA2D 从 SDRAM 读取的是旧数据。

解决方案:硬件屏障指令的正确插入点

1. 配置 MPU 将 SDRAM 设为 Write-through 或 Non-cacheable

对于帧缓冲区这类高频访问区域,建议设为 Write-through(可缓存但每次写直达 SDRAM),但性能略降。若要求严格一致,可设为 Non-cacheable。

// 配置 MPU 区域:SDRAM 基地址 0xC0000000,大小 8MB
MPU_Region_InitTypeDef MPU_InitStruct;

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_REGION_NOT_BUFFERABLE;
MPU_InitStruct.IsCacheable = MPU_REGION_NOT_CACHEABLE; // 或 WRITE_THROUGH
MPU_InitStruct.IsShareable = MPU_REGION_NOT_SHAREABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
MPU_InitStruct.SubRegionDisable = 0;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;

HAL_MPU_ConfigRegion(&MPU_InitStruct);

2. 使用 Cache 维护函数 + 屏障指令

若保持 Cacheable,则需在 CPU 写入后、DMA 启动前,执行 Clean 操作;在 DMA 完成后、CPU 读取前,执行 Invalidate 操作。

// 启用 D-Cache(在 main 函数中)
SCB_EnableDCache();

// CPU 写入帧缓冲区
for (int i = 0; i < 1024*1024; i++) {
    frame_buffer[i] = pixel_value;
}

// 关键点 1:Clean D-Cache,将脏数据写回 SDRAM
SCB_CleanDCache_by_Addr((uint32_t*)frame_buffer, buffer_size);

// 关键点 2:插入 DSB 指令,确保 Clean 操作完成
__DSB();

// 启动 DMA2D 传输(读取 SDRAM)
HAL_DMA2D_Start(&hdma2d, (uint32_t)frame_buffer, ...);

// 等待 DMA 完成...

// 关键点 3:DMA 写入 SDRAM 后,Invalidate D-Cache,使 CPU 重新从 SDRAM 读取
SCB_InvalidateDCache_by_Addr((uint32_t*)frame_buffer, buffer_size);
__DSB(); // 确保 Invalidate 完成

3. 中断与多任务场景下的屏障插入

在中断服务程序中,如果 DMA 传输完成中断需要读取 SDRAM 数据,必须在中断入口处执行 Invalidate 操作,并插入 DMB 指令,防止编译器或硬件重排内存访问。

void DMA2D_IRQHandler(void) {
    // 先 Invalidate 相关区域
    SCB_InvalidateDCache_by_Addr((uint32_t*)sdram_buffer, len);
    __DMB(); // 确保 Invalidate 完成,且后续读取不会重排

    // 现在可以安全读取 sdram_buffer
    process_data(sdram_buffer);
}

完整代码示例:SDRAM + DMA 数据一致性框架

以下代码演示了在 STM32F429 上,通过 MPU 将 SDRAM 设为 Non-cacheable,并配合屏障指令确保 DMA 安全。

#include "stm32f4xx_hal.h"

// SDRAM 基地址(Bank 1,片选 0)
#define SDRAM_BASE 0xC0000000
#define BUFFER_SIZE 1024

void MPU_Config(void) {
    MPU_Region_InitTypeDef MPU_InitStruct;
    HAL_MPU_Disable();

    // 配置 SDRAM 区域为 Non-cacheable
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = SDRAM_BASE;
    MPU_InitStruct.Size = MPU_REGION_SIZE_8MB;
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.IsBufferable = MPU_REGION_NOT_BUFFERABLE;
    MPU_InitStruct.IsCacheable = MPU_REGION_NOT_CACHEABLE;
    MPU_InitStruct.IsShareable = MPU_REGION_NOT_SHAREABLE;
    MPU_InitStruct.Number = MPU_REGION_NUMBER0;
    MPU_InitStruct.SubRegionDisable = 0;
    MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
    MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    HAL_MPU_Enable(MPU_CONTROL_MMU_ENABLE);
}

int main(void) {
    HAL_Init();
    SystemClock_Config();
    MPU_Config();
    SCB_EnableDCache(); // 仍启用 D-Cache,但 SDRAM 区域不缓存

    uint32_t *sdram_ptr = (uint32_t*)SDRAM_BASE;

    // CPU 写入(直接写 SDRAM,因为 Non-cacheable)
    for (int i = 0; i < BUFFER_SIZE; i++) {
        sdram_ptr[i] = i;
    }

    // 无需 Clean,因为写直达 SDRAM
    // 启动 DMA 读取(例如 SDMMC 或 DMA2D)
    // 由于 Non-cacheable,DMA 和 CPU 看到一致数据

    // 若使用 Cacheable 区域,则需手动 Clean/Invalidate
    // 示例:假设另一块内部 SRAM 区域是 Cacheable
    uint32_t internal_buf[BUFFER_SIZE];
    SCB_CleanDCache_by_Addr(internal_buf, sizeof(internal_buf));
    __DSB();

    // 启动 DMA 从 internal_buf 传输...

    while(1) {}
}

注意事项与最佳实践

  • 区分 Cacheable 与 Non-cacheable 区域:不要将整个 SDRAM 设为 Cacheable,除非你明确知道后果。建议将帧缓冲区、DMA 描述符等设为 Non-cacheable,而将普通数据区设为 Write-back 以提升性能。
  • Clean 和 Invalidate 的粒度SCB_CleanDCache_by_AddrSCB_InvalidateDCache_by_Addr 要求地址按 32 字节对齐,长度也需为 32 的倍数。否则会触发断言或错误操作。
  • 屏障指令的位置:Clean 后必须跟 DSB,确保数据真正写回 SDRAM;Invalidate 后跟 DSB,确保 Cache 行失效完成。在中断中,使用 DMB 防止内存访问重排。
  • 性能权衡:Non-cacheable 区域每次访问都直达 SDRAM,性能下降明显。若需高性能,可考虑使用 DMA 的 Cache 维护硬件(如 STM32H7 的 MDMA)或使用双缓冲。
  • 调试技巧:当出现数据不一致时,先检查 MPU 配置,再检查是否缺少 Clean/Invalidate。使用逻辑分析仪或断点观察 SDRAM 实际数据。

结语

D-Cache 与 SDRAM 的数据一致性是嵌入式开发中的经典难题。理解 Cortex-M4 的 Cache 行为,合理配置 MPU,并在关键路径上插入 DMB/DSB/ISB 屏障指令,是解决问题的核心。本文提供的插入点和方法已在多个项目中验证,希望能帮你少走弯路。记住:硬件屏障不是万能的,但缺失它,Bug 必然出现。