一、为什么 D-Cache 会与 SDRAM 产生冲突?

STM32F4 系列(如 STM32F407/429)内置 1MB RAM,但许多应用需要外扩 SDRAM(如 W9825G6KH)。CPU 访问 SDRAM 的速度远低于内部 SRAM,而 Cortex-M4 的 D-Cache(数据缓存)正是为缓解这一瓶颈而生。然而,D-Cache 是**写回(Write-Back)**策略的缓存,意味着 CPU 修改数据时先写入 Cache,延迟到缓存行被替换时才真正写回 SDRAM。若外设(如 DMA、LTDC 控制器)直接访问 SDRAM,而 Cache 中仍有未写回的数据,就会导致外设读到旧值,产生数据不一致。

1.1 缓存行与一致性本质

  • D-Cache 以 32 字节 为一行(Cache Line)管理数据。
  • 当 CPU 写一个变量时,整个缓存行被标记为“脏”(Dirty),但 SDRAM 中的对应地址并未更新。
  • 若 DMA 从 SDRAM 读取该地址,它绕过 Cache,直接访问物理内存,因此拿到的是旧数据。

1.2 解决方案:MPU 配置内存属性

MPU(内存保护单元)允许为不同内存区域定义缓存策略。关键属性有:

  • Write-Back (WBWA):写回,允许缓存行延迟写回,性能高,但需手动维护一致性。
  • Write-Through (WT):写通,每次写操作同时更新 Cache 和 SDRAM,性能略低,但外设总能读到最新数据。
  • Non-Cacheable:禁止缓存,直接访问 SDRAM,最安全但性能最差。

核心原则:若 SDRAM 区域被 CPU 和 DMA/外设共享,应配置为 Write-Through 或 Non-Cacheable;若仅 CPU 访问,可配置为 Write-Back 以获得最佳性能。

二、MPU 配置实战:将 SDRAM 设为 Write-Through

下面以 STM32F429 驱动外部 SDRAM(地址 0xC0000000,大小 8MB)为例,演示如何通过 MPU 配置其缓存策略。

2.1 硬件初始化(简化)

// SDRAM 初始化(使用 FMC 外设)
void SDRAM_Init(void) {
    // 配置 FMC 引脚、时序等(省略具体寄存器操作)
    // 使能 SDRAM 时钟,设置列选、行选等
}

2.2 MPU 配置代码

#include "mpu_armv7.h"

void MPU_Config_SDRAM_WT(void) {
    // 1. 禁用 MPU
    MPU->CTRL = 0;
    // 2. 配置区域 0:SDRAM 区域
    MPU->RNR = 0;                      // 使用区域 0
    MPU->RBAR = 0xC0000000;            // 基地址
    // 3. 配置属性:Write-Through,可读可写,非执行
    //    TEX=0, C=1, B=1 -> Write-Through (WT)
    //    AP=0b011 -> 全权限,XN=1 -> 禁止执行
    MPU->RASR = (0 << 19) |            // TEX=0
                (1 << 18) |            // C=1
                (1 << 16) |            // B=1
                (0b011 << 24) |        // AP: 全权限
                (1 << 28) |            // XN: 禁止执行
                (0x1F << 1) |          // SIZE: 2^(1+1) = 4KB? 实际需计算
                (1 << 0);              // ENABLE
    // 注意:SIZE 字段需根据实际大小设置。8MB = 2^23,故 SIZE=22(0x16)
    // 修正:MPU->RASR = (0 << 19) | (1 << 18) | (1 << 16) | (0b011 << 24) | (1 << 28) | (22 << 1) | 1;
    // 4. 使能 MPU
    MPU->CTRL = 1;
    __DSB();
}

重要:SIZE 字段的计算公式为 SIZE = log2(区域大小) - 1。8MB = 2^23,所以 SIZE=22。上述代码中注释已修正。

2.3 完整初始化流程

int main(void) {
    HAL_Init();
    SystemClock_Config();
    SDRAM_Init();
    MPU_Config_SDRAM_WT();   // 必须在使能 Cache 之前配置 MPU
    SCB_EnableDCache();       // 使能 D-Cache
    // 之后即可正常使用 SDRAM
}

三、踩坑记录:那些年我们遇到的 Bug

3.1 坑 1:DMA 传输数据错乱

现象:使用 DMA 从 SDRAM 读取图像数据到 LCD 显存,图像出现随机条纹。

原因:SDRAM 区域默认被配置为 Write-Back,CPU 写入图像数据后,DMA 立即读取,但 Cache 尚未写回,导致 DMA 读到旧数据。

解决:将 SDRAM 配置为 Write-Through,或在使用 DMA 前手动调用 SCB_CleanDCache()

3.2 坑 2:MPU 配置后系统 HardFault

现象:配置 MPU 后,程序一访问 SDRAM 就进入 HardFault。

原因:MPU 区域大小设置错误,或基地址未对齐。MPU 要求区域基地址必须按区域大小对齐(如 8MB 区域基地址需 8MB 对齐)。

解决:检查 SDRAM 基地址是否 0xC0000000(通常满足),并正确计算 SIZE 字段。另外,确保 MPU 区域不重叠。

3.3 坑 3:性能反而下降

现象:配置为 Write-Through 后,CPU 访问 SDRAM 速度变慢,甚至不如无缓存。

原因:Write-Through 每次写操作都直接写 SDRAM,若频繁写小变量,开销巨大。

解决:将频繁读写的变量放在内部 SRAM,SDRAM 仅存放大数据块(如帧缓冲),并考虑使用 Write-Back + 手动维护。

四、进阶:使用 Write-Back 并手动维护一致性

若追求性能,可配置为 Write-Back,并在关键点手动清理缓存。

// 配置为 Write-Back(TEX=1, C=1, B=1)
MPU->RASR = (1 << 19) | (1 << 18) | (1 << 16) | (0b011 << 24) | (1 << 28) | (22 << 1) | 1;

// 在 DMA 读取前,将缓存行写回 SDRAM
SCB_CleanDCache_by_Addr((uint32_t*)buffer, size);

// 在 DMA 写入后,使缓存行失效,以便 CPU 读取新数据
SCB_InvalidateDCache_by_Addr((uint32_t*)buffer, size);

注意SCB_CleanDCache_by_AddrSCB_InvalidateDCache_by_Addr 需要地址 32 字节对齐,大小也需对齐到 32 的倍数。

五、总结与建议

  • 默认配置:若 SDRAM 仅 CPU 访问,使用 Write-Back 性能最佳;若与 DMA 共享,优先 Write-Through。
  • 调试技巧:遇到随机性数据错误,先检查缓存一致性,用逻辑分析仪或断点观察数据变化。
  • MPU 配置顺序:必须先配置 MPU 再使能 D-Cache,否则 MPU 设置不生效。
  • 内存对齐:所有缓存操作函数要求地址和大小对齐到 32 字节,否则会触发断言或错误。

通过合理配置 MPU,你可以在享受 D-Cache 性能提升的同时,避免数据不一致的陷阱。希望本文能帮你少走弯路,让嵌入式开发更顺畅。