引言

STM32F4 系列(如 STM32F429、STM32F407)凭借其强大的性能和丰富的外设,被广泛应用于嵌入式视觉、音频处理等场景。当程序规模增大,内部 SRAM 不够用时,外扩 SDRAM 成为常见选择。然而,很多开发者发现:数据写入 SDRAM 后,读回却不对;或者 DMA 搬运的数据总是“差一拍”。这些问题背后,往往隐藏着 Cache 一致性的陷阱。

1. 原理剖析:Cache 与 SDRAM 的“爱恨情仇”

1.1 Cortex-M4 的 Cache 架构

STM32F4 基于 Cortex-M4 内核,部分型号(如 STM32F429/439)带有 4KB I-Cache 和 4KB D-Cache。Cache 是 CPU 与主存(包括 SDRAM)之间的高速缓存,用于减少访问延迟。

  • D-Cache:缓存数据读写,默认是写回(write-back)策略,即数据先写入 Cache,标记为脏(dirty),直到被替换或显式 clean 才写回主存。
  • I-Cache:缓存指令,通常无需手动维护。

1.2 问题根源

当 CPU 通过 AHB 总线访问 SDRAM 时,如果开启了 D-Cache,CPU 读写的数据会先经过 Cache。这导致两个典型问题:

  • 写不一致:CPU 写入 SDRAM 的数据可能还滞留在 Cache 中,外部设备(如 DMA)直接访问 SDRAM 时读不到最新数据。
  • 读不一致:DMA 将新数据写入 SDRAM,但 CPU 读取时命中了 Cache 中的旧数据,导致数据陈旧。

1.3 为什么 SDRAM 尤其容易触发?

SDRAM 访问速度远慢于内部 SRAM,且通常位于外部存储器映射区(0xC0000000 附近)。当程序频繁读写 SDRAM 时,Cache 命中率低,替换频繁,脏数据写回时机不可控,问题更容易暴露。

2. 排查步骤:定位 Cache 一致性 Bug

2.1 典型现象

  • 程序运行结果随机出错,但调试时(关闭优化)又正常。
  • 使用 DMA 从 SDRAM 搬运数据到外设,数据错位或全零。
  • 多任务环境下,一个任务写入的数据,另一个任务读不到。

2.2 快速验证方法

  1. 关闭 D-Cache:在启动代码中注释掉 SCB_EnableDCache(),观察问题是否消失。若消失,则基本确定是 Cache 一致性问题。
  2. 使用逻辑分析仪:抓取 SDRAM 的读写时序,对比实际数据。
  3. 打印调试:在关键读写点前后,强制 Clean 或 Invalidate,看数据是否恢复。

2.3 确认 Cache 配置

检查是否启用了 MPU 并正确配置了 SDRAM 区域的 Cache 策略。默认情况下,Cortex-M4 的 D-Cache 对所有可缓存区域采用 write-back 策略,但我们可以通过 MPU 将 SDRAM 区域设置为 write-through 或 non-cacheable。

3. 解决方案:三管齐下

3.1 方案一:配置 MPU 设置 SDRAM 为 Write-Through

Write-Through 策略下,CPU 写数据时会同时更新 Cache 和主存,保证外部设备能看到最新数据,但牺牲部分性能。

// 配置 MPU 区域:SDRAM 基地址 0xC0000000,大小 8MB
void MPU_Config_SDRAM_WriteThrough(void)
{
    MPU_Region_InitTypeDef MPU_InitStruct;
    
    __HAL_RCC_MPU_CLK_ENABLE();
    
    HAL_MPU_Disable();
    
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = 0xC0000000;
    MPU_InitStruct.Size = MPU_REGION_SIZE_8MB;
    MPU_InitStruct.SubRegionDisable = 0;
    MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.DisableExec = MPU_REGION_ENABLE_EXEC;
    MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
    MPU_InitStruct.IsCacheable = MPU_ACCESS_CACHEABLE;  // 允许缓存
    MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE; // 写直通
    
    HAL_MPU_ConfigRegion(&MPU_InitStruct);
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

注意IsBufferable 设置为 MPU_ACCESS_NOT_BUFFERABLE 即 write-through。但需注意,Cortex-M4 的 MPU 对 write-through 的支持有限,实际效果可能仍为 write-back。因此,更可靠的是方案二。

3.2 方案二:软件维护 Cache(推荐)

在关键操作前后,手动执行 Clean(写回)或 Invalidate(失效)操作。

3.2.1 使用 CMSIS 函数

#include "core_cm4.h"

// 写数据到 SDRAM 后,确保数据写回
void SDRAM_Write_With_Clean(uint32_t *addr, uint32_t *data, uint32_t len)
{
    // 写入数据(假设已通过普通赋值或 memcpy)
    memcpy(addr, data, len * 4);
    
    // Clean D-Cache:将指定地址范围的数据写回主存
    SCB_CleanDCache_by_Addr((uint32_t *)addr, (int32_t)(len * 4));
}

// 从 SDRAM 读取数据前,使 Cache 失效,强制从主存加载
void SDRAM_Read_With_Invalidate(uint32_t *addr, uint32_t *buf, uint32_t len)
{
    // Invalidate D-Cache:丢弃 Cache 中的旧数据
    SCB_InvalidateDCache_by_Addr((uint32_t *)addr, (int32_t)(len * 4));
    
    // 读取数据
    memcpy(buf, addr, len * 4);
}

3.2.2 注意事项

  • 地址对齐:CMSIS 函数要求地址和长度按 32 字节对齐(Cache line 大小)。若不对齐,需手动处理边界。
  • 性能开销:频繁 Clean/Invalidate 会降低效率,建议按块操作,而非逐字节。
  • DMA 场景:在启动 DMA 传输前 Clean(若 DMA 从内存读),在 DMA 完成后 Invalidate(若 DMA 写入内存)。

3.3 方案三:彻底关闭 D-Cache(不推荐)

直接禁用 D-Cache 可避免一致性问题,但会大幅降低 SDRAM 访问性能,仅用于调试或对性能无要求的场景。

SCB_DisableDCache();

4. 完整示例:SDRAM + DMA 传输的 Cache 维护

以下代码演示了从 SDRAM 缓冲区通过 DMA 发送数据到 UART,并正确处理 Cache。

// 假设 SDRAM 缓冲区地址 0xC0001000,长度 256 字节
#define SDRAM_BUF_ADDR  0xC0001000
#define BUF_LEN         256

// 初始化 SDRAM 和 DMA 后,发送数据
void SDRAM_DMA_Send(void)
{
    // 1. 确保 SDRAM 中的数据是最新的(如果之前 CPU 写过)
    SCB_CleanDCache_by_Addr((uint32_t *)SDRAM_BUF_ADDR, BUF_LEN);
    
    // 2. 配置 DMA 从 SDRAM 读取数据到 UART(此处省略具体配置)
    // DMA_Config(SDRAM_BUF_ADDR, UART_TX_ADDR, BUF_LEN);
    
    // 3. 启动 DMA
    // DMA_Start();
    
    // 4. 等待 DMA 完成(中断或轮询)
    // while(DMA_Busy());
    
    // 5. 如果 DMA 写入了 SDRAM(例如接收),需 Invalidate
    // SCB_InvalidateDCache_by_Addr((uint32_t *)SDRAM_BUF_ADDR, BUF_LEN);
}

5. 常见误区与注意事项

  • 误区一:只 Clean 不 Invalidate:读操作前必须 Invalidate,否则可能读到旧数据。
  • 误区二:忽略对齐:CMSIS 函数要求 32 字节对齐,否则行为未定义。建议在定义 SDRAM 缓冲区时使用 __attribute__((aligned(32)))
  • 误区三:在中断中做 Cache 维护:Clean/Invalidate 操作耗时较长,应避免在中断上下文执行,否则影响实时性。
  • 误区四:MPU 配置后不验证:不同型号的 MPU 行为可能略有差异,建议通过实际测试确认。

6. 总结

Cache 一致性是 STM32F4 使用 SDRAM 时不可回避的问题。通过理解 Cache 工作原理,合理配置 MPU,并在关键路径上执行软件维护,可以彻底解决数据错乱问题。推荐采用“MPU 设置非缓存 + 软件按需维护”的组合策略,兼顾性能与可靠性。希望本文能帮你少走弯路,让 SDRAM 真正成为你的得力助手。