引言

在嵌入式开发中,STM32F4 系列凭借其强大的处理能力和丰富的外设,成为许多高性能应用的首选。然而,当开发者启用 D-Cache(数据缓存)并外接 SDRAM 时,常常会遇到数据不一致的“幽灵 bug”——例如 DMA 传输的数据在 CPU 读取时出现乱码,或者 CPU 写入的数据在 DMA 发送时丢失。这些问题往往源于 D-Cache 与 SDRAM 之间的数据同步机制缺失。本文将从原理出发,介绍三种工程解法,并给出具体实现。

问题根源:D-Cache 与 SDRAM 的“双轨制”

D-Cache 是 CPU 与主存(如 SDRAM)之间的高速缓存,用于减少访问延迟。当 CPU 读取数据时,会先检查缓存;若命中,则直接返回缓存数据,而不会访问 SDRAM。同样,CPU 写入数据时,可能只更新缓存(写回策略),而延迟写入 SDRAM。

对于普通内部 SRAM,由于总线一致性机制,问题不明显。但 SDRAM 通常挂在外部总线(如 FMC)上,且 DMA 控制器直接访问 SDRAM,不经过 D-Cache。这导致两种典型冲突:

  • CPU 写,DMA 读:CPU 写入数据后,数据可能仍留在缓存中,DMA 从 SDRAM 读取到的是旧数据。
  • DMA 写,CPU 读:DMA 将新数据写入 SDRAM,但 CPU 读取时缓存中仍是旧数据。

解法一:直接关闭 D-Cache(简单粗暴)

原理

关闭 D-Cache 后,CPU 所有读写都直接访问 SDRAM,彻底消除缓存不一致问题。代价是性能下降,尤其对于频繁访问 SDRAM 的应用(如图形缓冲、音频流)。

配置步骤

  1. 在系统初始化时,禁用 D-Cache。
  2. 确保所有代码路径中不再启用 D-Cache。

代码示例

#include "stm32f4xx.h"

void Disable_DCache(void) {
    // 禁用 D-Cache
    SCB_DisableDCache();
    // 可选:清空缓存,确保无残留数据
    SCB_CleanDCache();
}

int main(void) {
    // 系统初始化...
    Disable_DCache();
    // 后续代码正常访问 SDRAM
    // ...
}

注意事项

  • 适用于对性能要求不高的场景,或 SDRAM 访问频率极低时。
  • 若同时使用 I-Cache,建议保留 I-Cache,仅关闭 D-Cache,以减小性能损失。

解法二:使用 MPU 配置非缓存区域(精准控制)

原理

MPU(内存保护单元)允许将特定内存区域配置为“不可缓存”或“写直达”。通过将 SDRAM 区域设置为非缓存,CPU 访问该区域时直接读写 SDRAM,而其他区域仍可享受缓存加速。

配置步骤

  1. 初始化 MPU,设置 SDRAM 区域的属性为“Normal, Non-Cacheable”。
  2. 启用 MPU。
  3. 确保 SDRAM 的地址和大小正确。

代码示例

#include "stm32f4xx.h"

void MPU_Config(void) {
    // 禁用 MPU
    MPU->CTRL = 0;
    
    // 配置 SDRAM 区域(假设基地址 0xC0000000,大小 8MB)
    MPU->RNR = 0;  // 使用区域 0
    MPU->RBAR = 0xC0000000;  // 基地址
    // 设置区域大小(8MB)和属性:Normal, Non-Cacheable
    MPU->RASR = (0x01 << 0) |  // Enable
                (0x03 << 1) |  // Size: 2^3 = 8MB? 实际需按编码,此处示例
                (0x00 << 8) |  // TEX=0, C=0, B=0 -> Non-Cacheable
                (0x01 << 16) | // Shareable? 根据需求
                (0x00 << 24);  // 其他属性
    
    // 启用 MPU
    MPU->CTRL = 1;
    __DSB();
}

int main(void) {
    // 初始化 SDRAM...
    MPU_Config();
    // 现在访问 SDRAM 区域时,不会经过 D-Cache
    // ...
}

注意:RASR 寄存器的 Size 字段编码需参考参考手册,例如 8MB 对应 0x13(实际编码为 (log2(size)-1))。上例仅为示意,实际开发请查阅数据手册。

注意事项

  • 需要精确计算区域大小和属性,避免覆盖其他内存。
  • 适用于 SDRAM 区域访问频繁但不需要缓存加速的场景,如帧缓冲、DMA 缓冲区。
  • 若 SDRAM 中部分数据需要缓存,可划分多个区域,分别设置不同属性。

解法三:手动维护缓存一致性(灵活高效)

原理

保留 D-Cache 启用,但在关键操作前后手动清理或使缓存失效。具体操作:

  • CPU 写,DMA 读:在启动 DMA 前,调用 SCB_CleanDCache() 将缓存数据写回 SDRAM。
  • DMA 写,CPU 读:在 DMA 完成后,调用 SCB_InvalidateDCache() 使缓存失效,强制从 SDRAM 重新加载。

配置步骤

  1. 保持 D-Cache 启用。
  2. 在每次 DMA 传输前/后,调用相应的缓存维护函数。

代码示例

#include "stm32f4xx.h"

// 示例:使用 DMA 从 SDRAM 读取数据到内存缓冲区
void DMA_Read_From_SDRAM(uint32_t *sdram_buf, uint32_t *dest, uint32_t size) {
    // 使缓存失效,确保读取到最新数据(DMA 可能已写入)
    SCB_InvalidateDCache_by_Addr((uint32_t*)dest, size);
    
    // 启动 DMA 传输(从 SDRAM 到 dest)
    // ... DMA 配置和启动代码 ...
    
    // 等待 DMA 完成
    while (DMA_GetFlagStatus(...) == RESET);
    
    // 此时 dest 中的数据已由 DMA 写入,但缓存可能仍为旧数据,因此再次使缓存失效
    SCB_InvalidateDCache_by_Addr((uint32_t*)dest, size);
}

// 示例:使用 DMA 从内存缓冲区写入 SDRAM
void DMA_Write_To_SDRAM(uint32_t *src, uint32_t *sdram_buf, uint32_t size) {
    // 将 CPU 写入的数据从缓存刷到 SDRAM
    SCB_CleanDCache_by_Addr((uint32_t*)src, size);
    
    // 启动 DMA 传输(从 src 到 SDRAM)
    // ... DMA 配置和启动代码 ...
    
    // 等待 DMA 完成
    while (DMA_GetFlagStatus(...) == RESET);
}

注意事项

  • 必须确保地址对齐(通常 32 字节对齐),否则无效。
  • 频繁调用缓存维护函数会带来性能开销,应只在必要处使用。
  • 适用于 SDRAM 访问模式复杂、需要兼顾性能与正确性的场景。

总结与选型建议

| 方法 | 优点 | 缺点 | 适用场景 | |------|------|------|----------| | 关闭 D-Cache | 简单可靠 | 性能下降 | 低性能要求或调试阶段 | | MPU 非缓存区域 | 精准控制,不影响其他区域 | 配置复杂,需仔细计算 | SDRAM 作为缓冲区,访问频繁 | | 手动维护一致性 | 灵活,性能较好 | 需开发者手动管理,易遗漏 | 复杂应用,需平衡性能与正确性 |

在实际工程中,建议优先考虑 MPU 方案,因为它既保证了 SDRAM 访问的正确性,又不牺牲其他内存区域的缓存性能。若开发周期紧张,可先用关闭 D-Cache 快速验证功能,再优化。无论选择哪种方法,务必在项目初期就考虑数据一致性问题,避免后期调试的噩梦。