一、问题根源:D-Cache 与 SDRAM 的“缓存陷阱”

STM32F4 系列(如 STM32F407、F429)内置 4KB 或 8KB 的 D-Cache,用于加速 CPU 对片内 SRAM 的访问。但当外部 SDRAM 被映射到内存空间(如 Bank1 的 0xC0000000 地址段)时,D-Cache 默认会缓存 SDRAM 数据。这引发两个典型问题:

  • CPU 写入后,DMA 读取到旧数据:CPU 修改 SDRAM 中的缓冲区,但数据仍留在 Cache 中,尚未写回 SDRAM,DMA 外设直接访问 SDRAM 时读到的是旧值。
  • DMA 写入后,CPU 读到脏数据:DMA 从外设接收数据写入 SDRAM,但 Cache 中仍保留着旧副本,CPU 读取时命中 Cache,得到过时数据。

根本原因:D-Cache 以 32 字节(Cache Line)为单位进行读写,而 SDRAM 的访问延迟远高于片内 SRAM,导致缓存策略与 DMA 的“非缓存”访问路径不一致。

二、解法一:直接禁用 D-Cache(简单粗暴)

原理

通过设置 SCB->SACR 寄存器中的 DCACHE 位,关闭整个 D-Cache。所有内存访问直接落到 SDRAM,彻底消除一致性问题,但代价是 CPU 访问 SDRAM 的性能下降(每次读写都需等待 SDRAM 时序)。

配置步骤

  1. 在系统初始化时(如 main 函数开头)调用 SCB_DisableDCache()
  2. 确保所有 DMA 和 CPU 访问均在禁用后执行。

代码示例

#include "stm32f4xx.h"

void SystemInit_CacheDisable(void) {
    // 关闭 D-Cache(同时关闭 I-Cache 可选)
    SCB_DisableDCache();
    // 可选:关闭 I-Cache 以简化调试
    // SCB_DisableICache();
}

int main(void) {
    SystemInit_CacheDisable();
    // 初始化 SDRAM 和 DMA...
    // 所有数据访问直接操作 SDRAM
    while(1) {
        // 业务逻辑
    }
}

优缺点

  • 优点:实现简单,无需考虑 Cache 维护,适合对性能要求不高的应用。
  • 缺点:SDRAM 访问性能显著下降(可能降低 30%-50%),且无法利用 Cache 加速。

三、解法二:软件维护 Cache 行(精细控制)

原理

保持 D-Cache 启用,但在 DMA 传输前后,手动执行 Cache 清理(Clean)或失效(Invalidate)操作。使用 CMSIS 提供的函数:

  • SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize):将指定地址区域的 Cache 行写回 SDRAM。
  • SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize):使指定地址区域的 Cache 行失效,下次读取强制从 SDRAM 加载。

注意:地址必须按 32 字节对齐,大小需向上取整到 32 的倍数。

配置步骤

  1. 保持 D-Cache 启用(默认开启)。
  2. 在 CPU 写入 SDRAM 缓冲区后,调用 Clean 函数确保数据写回。
  3. 在 DMA 写入 SDRAM 缓冲区后,调用 Invalidate 函数使 Cache 失效。

代码示例

#include "stm32f4xx.h"

#define BUFFER_SIZE 1024  // 字节数
#define CACHE_LINE_SIZE 32

// 对齐到 32 字节的缓冲区(需在链接脚本中放置到 SDRAM 区域)
__attribute__((aligned(32))) uint8_t sdram_buffer[BUFFER_SIZE];

void DMA_Read_From_SDRAM(uint8_t *dest, uint32_t len) {
    // 使 Cache 失效,确保读取最新数据(DMA 可能已写入)
    SCB_InvalidateDCache_by_Addr((uint32_t *)sdram_buffer, len);
    // 执行 DMA 读取(从 SDRAM 到外设)
    // ... DMA 配置和启动 ...
}

void DMA_Write_To_SDRAM(uint8_t *src, uint32_t len) {
    // 执行 DMA 写入(从外设到 SDRAM)
    // ... DMA 配置和启动 ...
    // 等待 DMA 完成
    // 清理 Cache,将 CPU 可能缓存的旧数据写回(但此处 CPU 未写,可省略)
    // 但为了安全,可清理整个区域
    SCB_CleanDCache_by_Addr((uint32_t *)sdram_buffer, len);
}

int main(void) {
    // 初始化 SDRAM 和 DMA...
    // 注意:D-Cache 保持启用
    while(1) {
        // 使用缓冲区
    }
}

注意事项

  • 必须确保缓冲区地址和大小对齐到 32 字节,否则函数内部会断言或行为未定义。
  • 频繁调用 Cache 维护函数会增加开销,但比禁用 Cache 性能好。
  • 若多个缓冲区交叉,需分别维护。

四、解法三:使用 MPU 配置内存属性(硬件级隔离)

原理

利用 Cortex-M4 的 MPU(内存保护单元),将 SDRAM 区域配置为“非缓存”(Normal memory, Non-cacheable)属性。这样,CPU 访问 SDRAM 时绕过 D-Cache,而片内 SRAM 仍可享受缓存加速。MPU 提供区域划分,可精确控制哪些地址段缓存。

配置步骤

  1. 使能 MPU(MPU_Control 寄存器)。
  2. 配置一个区域,基地址为 SDRAM 起始地址,大小为 SDRAM 容量,属性设为 Normal, Non-cacheable。
  3. 使能该区域,并设置优先级。

代码示例

#include "stm32f4xx.h"

#define SDRAM_BASE_ADDR  0xC0000000
#define SDRAM_SIZE       0x2000000  // 32MB,需按实际调整

void MPU_Config_SDRAM_NonCacheable(void) {
    // 1. 禁用 MPU
    MPU->CTRL = 0;
    
    // 2. 配置区域 0:SDRAM
    MPU->RNR = 0;  // 使用区域 0
    MPU->RBAR = SDRAM_BASE_ADDR;
    // 设置属性:Normal memory, Non-cacheable, 可读写,执行权限可设
    MPU->RASR = (0x0 << 0)   |  // 禁用指令访问(可选)
                (0x1 << 1)   |  // 允许读
                (0x1 << 2)   |  // 允许写
                (0x0 << 3)   |  // 非可执行(可选)
                (0x1 << 4)   |  // 允许访问(Enable)
                (0x0 << 5)   |  // 非缓存(TEX=0, C=0, B=0)
                (0x0 << 8)   |  // 区域大小编码,需计算
                (0x0 << 16)  |  // 子区域禁用
                (0x0 << 24);    // 优先级
    
    // 计算区域大小编码:2^(SIZE+1) 字节,SIZE = log2(size)-1
    // 例如 32MB => SIZE = 25-1 = 24,但需按 MPU 规则编码
    // 简化:使用宏或手动计算
    uint32_t size_code = 24; // 假设 32MB,实际需根据 SDRAM 大小调整
    MPU->RASR |= (size_code << 1) | (1 << 0); // 设置 SIZE 和 ENABLE
    
    // 3. 使能 MPU,使用默认内存映射作为背景
    MPU->CTRL = (1 << 0) | (1 << 2); // ENABLE=1, PRIVDEFENA=1
    
    // 4. 内存屏障,确保配置生效
    __DSB();
    __ISB();
}

int main(void) {
    // 初始化 SDRAM 控制器...
    MPU_Config_SDRAM_NonCacheable();
    // 现在 SDRAM 区域访问不经过 D-Cache,DMA 和 CPU 直接操作 SDRAM
    while(1) {
        // 业务逻辑
    }
}

注意事项

  • 区域大小必须是 2 的幂,且基地址需对齐到区域大小。
  • 若 SDRAM 容量不是 2 的幂,需拆分为多个区域。
  • MPU 配置后,所有对该区域的访问均绕过 Cache,但片内 SRAM 仍可缓存,性能均衡。

五、三种方案对比与选型建议

| 方案 | 性能影响 | 实现复杂度 | 适用场景 | |------|----------|------------|----------| | 禁用 D-Cache | 高(SDRAM 访问慢) | 极低 | 原型验证、低速率应用 | | 软件维护 Cache | 中(维护开销) | 中 | 需要缓存加速,但 DMA 交互频繁且可控 | | MPU 配置非缓存 | 低(仅 SDRAM 无缓存) | 高 | 高性能应用,需同时使用 SRAM 缓存和 SDRAM 大容量存储 |

选型建议

  • 若项目对实时性要求不高,且 SDRAM 访问频率低,选方案一。
  • 若 SDRAM 缓冲区固定且 DMA 操作规律,选方案二,可保留缓存加速。
  • 若系统复杂,多个外设访问 SDRAM,且需保证一致性,选方案三,硬件隔离最可靠。

六、总结

D-Cache 与 SDRAM 的一致性是 STM32F4 开发中的经典问题。理解 Cache 行机制和 DMA 访问路径是解决关键。三种解法各有优劣,开发者应根据实际需求权衡。在实际工程中,推荐优先考虑 MPU 方案,它从硬件层面隔离了缓存,避免软件维护的遗漏风险,同时保留 SRAM 的缓存性能。无论选择哪种,务必在初始化阶段就明确配置,并测试 DMA 与 CPU 交互的边界情况。