一、问题背景:D-Cache 与 SDRAM 的“速度鸿沟”

STM32F4 系列(如 STM32F407)内置 Cortex-M4 内核,主频高达 168MHz,而外部 SDRAM(如 W9825G6KH)的访问延迟通常在几十纳秒量级。为了提升性能,内核集成了可选的 D-Cache(数据缓存),将频繁访问的数据暂存于高速 SRAM 中。然而,这种“缓存”机制在 DMA、外设或 CPU 直接访问 SDRAM 时,会引发数据不一致(Inconsistency)——即 CPU 看到的缓存数据与 SDRAM 中的真实数据不同步。

1.1 硬件原理:Cache 行与写策略

  • D-Cache 以“行”(Line)为单位管理数据,STM32F4 的 Cache 行大小通常为 32 字节。
  • 写策略分为 Write-through(写直达)和 Write-back(写回)。STM32F4 的 D-Cache 默认采用 Write-back 模式,即写操作只更新 Cache,不立即写回 SDRAM,直到 Cache 行被替换或显式 Clean 操作。
  • 读操作采用 Read-allocate:若数据不在 Cache 中,则从 SDRAM 加载整个 Cache 行。

1.2 不一致的典型场景

  • CPU 写 SDRAM,DMA 读 SDRAM:CPU 写入的数据可能还在 Cache 中,DMA 直接访问 SDRAM 读到旧数据。
  • DMA 写 SDRAM,CPU 读 SDRAM:DMA 更新了 SDRAM,但 CPU 的 Cache 中仍是旧副本,导致读回脏数据。
  • 外设(如 LTDC 显示控制器)直接读取 SDRAM 帧缓冲:若 CPU 修改帧缓冲后未 Clean,显示可能花屏。

二、软件修正策略:volatile 不够,屏障来凑

许多开发者误以为使用 volatile 就能解决一切,但 volatile 仅告诉编译器不要优化对该变量的访问,它无法阻止硬件 Cache 的行为。真正需要的是:

  • 内存屏障指令DMB(数据内存屏障)确保屏障前的内存访问完成后,屏障后的访问才开始;DSB(数据同步屏障)等待所有内存访问完成。
  • Cache 维护操作:通过 CP15 协处理器指令(或 CMSIS 函数)执行 Clean(写回)和 Invalidate(失效)。

2.1 核心原则

  • CPU 写 SDRAM 后,若其他主机(DMA/外设)要读,必须 Clean 对应 Cache 区域。
  • 其他主机写 SDRAM 后,CPU 读之前,必须 Invalidate 对应 Cache 区域。
  • 在访问共享数据前后,插入 DMB/DSB 指令,防止编译器和 CPU 乱序。

三、实战配置与代码示例

3.1 硬件初始化(简化)

假设使用 STM32F407 的 FMC 接口驱动 SDRAM,并开启 D-Cache。初始化代码略,重点在于 Cache 配置:

// 使能 D-Cache(在 SystemInit 后调用)
SCB_EnableDCache();

// 配置 MPU 将 SDRAM 区域设置为“非缓存”或“写-through”?
// 注意:若将 SDRAM 配置为 Non-cacheable,则无需手动维护,但性能下降。
// 本示例演示 Cacheable 模式下的手动维护。

3.2 数据一致性修正函数

使用 CMSIS 提供的函数(core_cm4.h):

// 清理(写回)指定地址区域
void cache_clean(uint32_t addr, uint32_t size) {
    // 注意:地址需按 32 字节对齐,大小向上取整
    uint32_t start = addr & ~0x1F;
    uint32_t end = (addr + size + 31) & ~0x1F;
    for (uint32_t a = start; a < end; a += 32) {
        SCB_CleanDCache_by_Addr((uint32_t*)a, 32);
    }
    DSB(); // 确保 Clean 完成
}

// 失效(丢弃)指定地址区域
void cache_invalidate(uint32_t addr, uint32_t size) {
    uint32_t start = addr & ~0x1F;
    uint32_t end = (addr + size + 31) & ~0x1F;
    for (uint32_t a = start; a < end; a += 32) {
        SCB_InvalidateDCache_by_Addr((uint32_t*)a, 32);
    }
    DSB();
}

3.3 实战场景:CPU 写数据,DMA 发送

#define BUF_ADDR  0xC0000000  // SDRAM 地址(FMC 映射)
#define BUF_SIZE  1024

uint8_t *buffer = (uint8_t*)BUF_ADDR;

// CPU 填充数据
for (int i = 0; i < BUF_SIZE; i++) {
    buffer[i] = i & 0xFF;
}

// 关键:写回 Cache,确保 SDRAM 中的数据最新
cache_clean((uint32_t)buffer, BUF_SIZE);

// 启动 DMA 传输(从 SDRAM 读取)
DMA_Start_Transmit(buffer, BUF_SIZE);

3.4 实战场景:DMA 接收数据,CPU 读取

// DMA 完成中断中
void DMA_IRQHandler(void) {
    // 先失效 Cache,使 CPU 从 SDRAM 重新加载
    cache_invalidate((uint32_t)buffer, BUF_SIZE);

    // 现在可以安全读取 buffer
    uint8_t first = buffer[0];
    // 处理数据...
}

3.5 使用 volatile 和屏障的补充

在共享变量(如标志位)上使用 volatile 防止编译器优化,但还需配合屏障保证顺序:

volatile uint8_t data_ready = 0;

// 中断中:
void DMA_IRQHandler(void) {
    cache_invalidate(...);
    DMB(); // 确保 Invalidate 完成后再置标志
    data_ready = 1;
}

// 主循环:
while (!data_ready); // volatile 保证每次读取内存
DMB(); // 确保读取标志后,后续访问不重排
// 安全读取数据

四、注意事项与常见陷阱

  • 对齐问题:Cache 操作要求地址 32 字节对齐,否则可能误伤相邻数据。建议将共享缓冲区按 __ALIGNED(32) 定义。
  • 性能权衡:频繁 Clean/Invalidate 会降低性能。若数据量小且访问频繁,可考虑将 SDRAM 区域配置为 Write-through 或 Non-cacheable(通过 MPU)。
  • DMA 描述符:DMA 的描述符(如链表)若在 SDRAM 中,同样需要维护一致性。
  • 多核/中断:在中断和主循环间共享数据时,务必使用屏障和 volatile,防止乱序。
  • 调试技巧:若出现随机数据错误,先尝试关闭 D-Cache 验证是否问题消失,再逐步定位。

五、总结

STM32F4 的 D-Cache 与 SDRAM 数据一致性是嵌入式开发中的“隐形杀手”。理解硬件原理后,通过 Cache Clean/Invalidate 操作配合内存屏障指令,即可精准修正。记住:volatile 只是编译器层面的约束,硬件一致性必须靠 Cache 维护指令。希望本文能助你写出健壮的代码,远离“幽灵数据”的困扰。