引言:性能与陷阱并存

STM32F4 系列(特别是带灵活存储控制器 FMC 的型号)常外挂 SDRAM 作为大容量数据缓冲。为了提升访问效率,开发者会启用内核的 D-Cache(数据缓存)。然而,D-Cache 的“写回(Write-back)”和“写分配(Write-allocate)”策略,会让 CPU 与 DMA 等外设访问同一内存区域时,产生数据不一致(Cache Coherency)问题。轻则数据错乱,重则系统死机。本文基于实际项目经验,总结一套系统性的排查与修复方法。

一、原理剖析:为什么 D-Cache 会“捣乱”?

1.1 D-Cache 的工作机制

  • 写回(Write-back):CPU 写数据时,仅更新 Cache 行,并标记为“脏”(Dirty),直到该行被替换或显式 Clean 操作时才写回 SDRAM。
  • 写分配(Write-allocate):CPU 读未命中时,先从 SDRAM 加载整行(通常 32 字节)到 Cache,再读取。

1.2 冲突场景

  • CPU 写 → DMA 读:CPU 写入数据后,数据可能还“躺”在 Cache 中,未写回 SDRAM。DMA 直接读取 SDRAM,拿到的是旧数据。
  • DMA 写 → CPU 读:DMA 将新数据写入 SDRAM,但 CPU 读取时,Cache 中可能还保留着旧数据(Cache 命中),导致 CPU 读到过期数据。

1.3 为什么 F4 系列更明显?

  • F4 内核(Cortex-M4)的 D-Cache 是可选功能,但一旦启用,所有对可缓存区域的访问都会经过 Cache。
  • SDRAM 通常被配置为“可缓存”区域(通过 MPU 或默认内存映射),因此问题极易触发。

二、问题定位:经典故障现象与排查步骤

2.1 故障现象

  • 现象 1:使用 DMA 从 SDRAM 发送数据到外设(如 DAC、LCD),数据偶尔错乱或重复。
  • 现象 2:CPU 从 SDRAM 读取 DMA 接收的数据,第一次读对,第二次读错。
  • 现象 3:程序运行一段时间后,随机死机或 HardFault。

2.2 排查步骤

  1. 确认 D-Cache 是否启用:检查启动代码或 SCB_EnableDCache() 调用。
  2. 检查内存区域属性:通过 MPU 配置确认 SDRAM 区域是否被标记为 “Cacheable” 和 “Write-back”。
  3. 复现并缩小范围:注释掉 DMA 操作,仅用 CPU 读写 SDRAM,看是否正常。若正常,则高度怀疑 Cache 一致性问题。
  4. 使用调试器观察:在关键点暂停,分别查看 SDRAM 实际数据和 CPU 寄存器/变量值,对比差异。

三、解决方案:Clean 与 Invalidate 的正确姿势

3.1 核心操作

  • Clean(清理):将 Dirty Cache 行写回 SDRAM,确保外部内存数据最新。
  • Invalidate(失效):丢弃 Cache 行,下次访问时重新从 SDRAM 加载。

3.2 操作时机

  • CPU 写 → DMA 读:在启动 DMA 前,对相关内存区域执行 Clean。
  • DMA 写 → CPU 读:在 DMA 传输完成后,对相关内存区域执行 Invalidate。

3.3 代码实现(基于 CMSIS)

#include "core_cm4.h"

// 清理指定地址和长度的数据(使脏数据写回 SDRAM)
void Cache_Clean(uint32_t addr, uint32_t len) {
    SCB_CleanDCache_by_Addr((uint32_t*)addr, (int32_t)len);
}

// 失效指定地址和长度的数据(丢弃 Cache 行)
void Cache_Invalidate(uint32_t addr, uint32_t len) {
    SCB_InvalidateDCache_by_Addr((uint32_t*)addr, (int32_t)len);
}

// 清理并失效(用于同时保证读写一致性)
void Cache_CleanInvalidate(uint32_t addr, uint32_t len) {
    SCB_CleanInvalidateDCache_by_Addr((uint32_t*)addr, (int32_t)len);
}

注意:地址必须 32 字节对齐,长度最好为 32 的倍数,否则需手动处理边界。

四、完整实战案例:DMA 从 SDRAM 发送数据

假设我们有一个音频缓冲区位于 SDRAM,地址 0xC0000000,大小 4096 字节,通过 DMA 发送到 I2S 外设。

4.1 初始化配置

// 使能 D-Cache(在 main 函数早期调用)
SCB_EnableDCache();

// 配置 MPU 将 SDRAM 区域设为 Write-back 可缓存(示例)
MPU_Region_Init(0, 0xC0000000, 0x1000000, MPU_REGION_SIZE_16MB, 
                MPU_REGION_AP_FULL, MPU_REGION_CACHEABLE_WB);

4.2 发送数据流程

#define AUDIO_BUF_ADDR  0xC0000000
#define AUDIO_BUF_SIZE  4096

uint8_t audio_data[AUDIO_BUF_SIZE] __attribute__((at(AUDIO_BUF_ADDR)));

void DMA_Send_Audio(void) {
    // 1. CPU 写入音频数据到 audio_data(假设已经填充)
    
    // 2. 关键:清理 D-Cache,确保数据写回 SDRAM
    Cache_Clean(AUDIO_BUF_ADDR, AUDIO_BUF_SIZE);
    
    // 3. 启动 DMA 传输(从 SDRAM 到 I2S)
    DMA_Start_Transfer(AUDIO_BUF_ADDR, AUDIO_BUF_SIZE);
    
    // 4. 等待 DMA 完成(中断或轮询)
    while (DMA_IsBusy());
    
    // 5. 如果 DMA 写入了数据(如从 ADC 采集),则需要 Invalidate
    // Cache_Invalidate(AUDIO_BUF_ADDR, AUDIO_BUF_SIZE);
}

4.3 注意事项

  • 对齐问题:如果缓冲区首地址未 32 字节对齐,SCB_CleanDCache_by_Addr 会触发断言(在调试版本)。建议使用 __attribute__((aligned(32))) 或调整内存分配。
  • 性能权衡:频繁 Clean/Invalidate 会降低性能,因此尽量以较大块为单位操作,或使用双缓冲机制。
  • 中断上下文:在中断中调用 Cache 操作时,确保没有嵌套中断打断,否则可能造成数据错乱。

五、进阶技巧与常见误区

5.1 使用 MPU 配置非缓存区域

如果某些 SDRAM 区域不需要高性能,可以将其配置为 “Non-cacheable” 区域,避免一致性问题。例如,将 DMA 描述符或控制块放在非缓存区域。

// 配置 MPU 区域 1:非缓存,用于 DMA 描述符
MPU_Region_Init(1, 0xC0200000, 0x1000, MPU_REGION_SIZE_4KB, 
                MPU_REGION_AP_FULL, MPU_REGION_CACHEABLE_NON);

5.2 常见误区

  • 误区 1:只 Invalidate 不 Clean。如果 CPU 之前写过数据,必须先 Clean 再 Invalidate,否则 Invalidate 会丢弃脏数据。
  • 误区 2:忽略编译器优化。编译器可能将变量缓存在寄存器中,导致 Cache 操作无效。使用 volatile 或内存屏障(__DSB())确保顺序。
  • 误区 3:在 DMA 传输进行中操作 Cache。必须等待 DMA 完全停止后再 Clean/Invalidate。

六、总结

D-Cache 与 SDRAM 的数据一致性是 STM32F4 高性能开发中的“拦路虎”,但只要理解原理,遵循“写前 Clean,读前 Invalidate”的黄金法则,并注意对齐和时序,就能轻松化解。建议在项目初期就规划好内存映射和 Cache 策略,避免后期大面积修改。希望本文能帮你从“踩坑”到“避坑”,让系统稳定运行。