引言:一次诡异的 SDRAM 数据错乱

在嵌入式开发中,STM32F4 系列(如 STM32F407、F429)凭借其高性能和丰富外设广受欢迎。当项目需要大容量存储时,外扩 SDRAM 是常见选择。然而,当你启用 D-Cache 后,可能会遇到这样的问题:DMA 从 SDRAM 读取的数据偶尔是旧的,或者 CPU 写入的数据 DMA 看不到,甚至程序随机跑飞。这就是典型的 D-Cache 与 SDRAM 数据一致性问题。

1. 原理剖析:为什么会出现数据不一致?

1.1 D-Cache 的工作机制

D-Cache(数据缓存)是 CPU 与主存(SDRAM)之间的小容量高速缓存。当 CPU 访问内存时,首先检查 Cache 是否命中;若命中,则直接操作 Cache 行(通常为 32 字节),不会立即更新 SDRAM。这种“写回(Write-back)”策略提高了性能,但也带来了隐患。

1.2 不一致的根源

  • CPU 写,DMA 读:CPU 修改了 Cache 中的数据,但数据尚未写回 SDRAM。此时 DMA 从 SDRAM 读取,得到的是旧数据。
  • DMA 写,CPU 读:DMA 将新数据写入 SDRAM,但 Cache 中仍保留旧副本。CPU 读取时命中 Cache,得到的是过时数据。
  • 代码执行:如果代码或常量数据存放在 SDRAM,且被 Cache 缓存,DMA 更新后同样会导致执行错误。

2. 踩坑实录:我的项目经历

在开发一个基于 STM32F429 的音频采集系统时,我使用 SDRAM 作为音频缓冲区。启用 D-Cache 后,ADC 通过 DMA 将数据写入 SDRAM,CPU 读取处理。起初一切正常,但运行几分钟后,音频出现爆音,调试发现部分数据块是旧数据。排查半天,最终定位到 D-Cache 未失效(Invalidate)导致。后来在另一项目中,CPU 将图像数据写入 SDRAM,然后 DMA 输出到 LCD,出现图像撕裂,原因是 Cache 未清理(Clean)。

3. 解决方案:三种配置策略

3.1 方案一:关闭 D-Cache(最简单,但性能损失大)

直接不启用 D-Cache,所有访问都直达 SDRAM。适合对性能要求不高的场景。

// 在 main 函数初始化时,不调用 SCB_EnableDCache() 即可
// 或者显式关闭(默认关闭)
SCB_DisableDCache();

优点:无一致性烦恼。 缺点:CPU 访问 SDRAM 速度大幅下降,可能成为性能瓶颈。

3.2 方案二:软件维护 Cache(常用,需精细控制)

在关键操作前后,手动执行 Cache 清理(Clean)或无效化(Invalidate)。

3.2.1 核心函数

// 清理:将 Cache 中修改的数据写回 SDRAM
SCB_CleanDCache();
// 无效化:丢弃 Cache 中的数据,下次从 SDRAM 重新读取
SCB_InvalidateDCache();
// 清理并无效化
SCB_CleanInvalidateDCache();
// 按地址范围操作(注意:地址需 32 字节对齐,长度需为 32 的倍数)
SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize);
SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize);

3.2.2 典型场景代码

场景 A:CPU 写数据,DMA 输出(如 LCD 显示)

// 假设 buffer 在 SDRAM,长度为 1024 字节,且 32 字节对齐
uint8_t buffer[1024] __attribute__((aligned(32)));

// CPU 填充数据
fill_buffer(buffer);

// 关键:将 Cache 中的数据写回 SDRAM
SCB_CleanDCache_by_Addr((uint32_t*)buffer, 1024);

// 启动 DMA 传输(从 SDRAM 读取)
DMA_Start_Transmit(buffer, size);

场景 B:DMA 写入数据,CPU 读取(如 ADC 采集)

// 启动 DMA 接收(写入 SDRAM)
DMA_Start_Receive(buffer, size);
// 等待 DMA 完成
while(DMA_GetFlag() == 0);

// 关键:使 Cache 失效,强制从 SDRAM 重新加载
SCB_InvalidateDCache_by_Addr((uint32_t*)buffer, 1024);

// 现在可以安全读取 buffer
process_data(buffer);

3.2.3 注意事项

  • 对齐与长度:地址必须 32 字节对齐,长度必须是 32 的倍数,否则行为未定义。
  • 性能开销:频繁清理/无效化会降低性能,应尽量批量操作。
  • 中断安全:在中断中维护 Cache 时,注意临界区保护。

3.3 方案三:MPU 配置(推荐,硬件自动处理)

通过 MPU(内存保护单元)将 SDRAM 区域配置为“非缓存”或“写直达”属性,让硬件自动保证一致性。

3.3.1 配置步骤

  1. 使能 MPU:在系统初始化时开启 MPU。
  2. 配置区域:设置 SDRAM 基地址、大小、访问权限和缓存属性。
  3. 设置缓存策略:选择“非缓存”或“写直达,读分配”。

3.3.2 示例代码(基于 STM32F4 HAL 库)

void MPU_Config(void)
{
    MPU_Region_InitTypeDef MPU_InitStruct;

    // 使能 MPU
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);

    // 配置 SDRAM 区域(假设基地址 0xC0000000,大小 8MB)
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = 0xC0000000;
    MPU_InitStruct.Size = MPU_REGION_SIZE_8MB;
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
    MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;  // 非缓存
    MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
    MPU_InitStruct.Number = MPU_REGION_NUMBER0;
    MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE;

    HAL_MPU_ConfigRegion(&MPU_InitStruct);
}

// 在 main 中调用
int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MPU_Config();  // 先配置 MPU
    // 初始化 SDRAM 等外设
    // 注意:D-Cache 可以正常使能,但 SDRAM 区域不会被缓存
    SCB_EnableDCache();
    // ...
}

3.3.3 优点与注意

  • 硬件自动处理:无需手动维护 Cache,代码简洁。
  • 性能折中:非缓存区域访问速度稍慢,但比关闭 D-Cache 好(其他区域仍可缓存)。
  • 配置顺序:MPU 必须在使能 D-Cache 之前配置,否则无效。
  • 区域重叠:确保 SDRAM 区域不与其它 MPU 区域冲突。

4. 总结与最佳实践

  • 优先使用 MPU 方案:对于 SDRAM 等大块内存,配置为非缓存或写直达,从根源解决一致性问题。
  • 若使用软件维护:务必注意地址对齐和长度倍数,并在每次 DMA 传输前后正确调用清理/无效化。
  • 调试技巧:若出现随机数据错误,先检查 Cache 配置,再检查 DMA 描述符。
  • 性能考量:如果 SDRAM 访问频繁且对性能要求高,可考虑使用“写直达”策略(TEX=0,C=1,B=0),但需硬件支持。

最后,记住一句口诀:“DMA 前 Clean,DMA 后 Invalidate”。掌握 D-Cache 与 SDRAM 的一致性配置,你的嵌入式项目将更加稳定可靠。