STM32F4 系列 D-Cache 与 SDRAM 数据一致性问题的三种实战修复策略
问题根源:D-Cache 与 SDRAM 的“缓存错觉”
STM32F4 系列(如 STM32F407/427/429)内置了 4KB 或 8KB 的 D-Cache,用于加速 CPU 对内存的访问。然而,D-Cache 是写回(write-back)策略,意味着 CPU 写入的数据可能暂存在 Cache 中,并未立即同步到外部 SDRAM。当外设(如 DMA、LTDC 控制器)直接访问 SDRAM 时,它们看到的是旧数据,而 CPU 却认为数据已更新,导致数据不一致。
典型场景:
- 使用 LTDC 驱动 LCD,帧缓冲位于 SDRAM,CPU 更新帧缓冲后,LTDC 仍显示旧画面。
- 使用 DMA 从 SDRAM 搬运数据到外设,但 CPU 刚写入的数据还在 Cache 中,DMA 读到的是过期数据。
策略一:软件手动维护 Cache(Clean & Invalidate)
原理
最直接的方法是在关键操作前后,手动调用 CMSIS 提供的 Cache 维护函数,强制将 Cache 中的数据写回 SDRAM(Clean),或使 Cache 行失效(Invalidate),以便下次读取时从 SDRAM 重新加载。
配置步骤
- 确保在
system_stm32f4xx.c中启用了 D-Cache(SCB_EnableDCache())。 - 在 CPU 写入 SDRAM 数据后,调用
SCB_CleanDCache()或按地址范围清理。 - 在 DMA 写入 SDRAM 后,CPU 读取前,调用
SCB_InvalidateDCache()。
代码示例
#include "stm32f4xx_hal.h"
// 假设 SDRAM 帧缓冲地址为 0xC0000000,大小 320*240*2 字节
#define FRAME_BUFFER_ADDR 0xC0000000
#define FRAME_BUFFER_SIZE (320*240*2)
void UpdateFrameBuffer(uint8_t *data) {
// CPU 写入数据到帧缓冲
memcpy((void*)FRAME_BUFFER_ADDR, data, FRAME_BUFFER_SIZE);
// 关键:将 Cache 中数据写回 SDRAM
SCB_CleanDCache_by_Addr((uint32_t*)FRAME_BUFFER_ADDR, FRAME_BUFFER_SIZE);
}
void ReadFrameBuffer(uint8_t *dest) {
// 如果 DMA 或外设修改了 SDRAM,CPU 读取前需使 Cache 失效
SCB_InvalidateDCache_by_Addr((uint32_t*)FRAME_BUFFER_ADDR, FRAME_BUFFER_SIZE);
memcpy(dest, (void*)FRAME_BUFFER_ADDR, FRAME_BUFFER_SIZE);
}
注意事项
- 地址必须按 32 字节对齐(Cache line 大小),否则无效。
- 频繁调用会影响性能,适合低频操作。
- 务必包含
core_cm4.h或通过 HAL 库间接包含。
策略二:MPU 配置 SDRAM 区域为 Non-Cacheable
原理
通过 MPU(Memory Protection Unit)将 SDRAM 所在的地址区域设置为“不可缓存”或“写通”(write-through),这样 CPU 访问 SDRAM 时直接绕过 D-Cache,从根本上避免一致性问题。适合对性能要求不高但要求数据绝对一致的场景。
配置步骤
- 启用 MPU 并配置一个区域,覆盖 SDRAM 地址范围。
- 设置属性为
MPU_REGION_NO_CACHE(不缓存)或MPU_REGION_WRITE_THROUGH。 - 在系统初始化时调用 MPU 配置函数。
代码示例
#include "stm32f4xx_hal.h"
void MPU_Config(void) {
MPU_Region_InitTypeDef MPU_InitStruct;
__HAL_RCC_MPU_CLK_ENABLE();
HAL_MPU_Disable();
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0xC0000000; // SDRAM 起始地址
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_DISABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
int main(void) {
HAL_Init();
MPU_Config();
// 后续 SDRAM 访问无需手动维护 Cache
}
注意事项
- 需要根据 SDRAM 实际大小调整区域大小,且地址必须对齐。
- 禁用 Cache 会降低 CPU 访问 SDRAM 的速度,但外设访问更可靠。
- MPU 配置必须在启用 D-Cache 之前或之后均可,但建议在系统初始化早期完成。
策略三:DMA 双缓冲 + Cache 协同
原理
对于高频数据流(如 ADC 采样、LCD 刷新),手动维护 Cache 会拖慢速度。双缓冲策略:CPU 写入 Buffer A,DMA 从 Buffer B 读取,交替使用。在切换缓冲时,只对当前使用的缓冲做 Cache 维护,减少维护频率。同时,利用 DMA 的传输完成中断来同步数据。
配置步骤
- 在 SDRAM 中分配两个缓冲区(Buffer A 和 B)。
- CPU 写 Buffer A 时,Buffer B 正被 DMA 使用。
- 写完后 Clean Buffer A 的 Cache,然后切换 DMA 源地址到 Buffer A。
- DMA 完成中断中,Invalidate Buffer B 的 Cache(因为 DMA 可能修改了 B),并通知 CPU 处理。
代码示例
#define BUFFER_SIZE 1024
uint8_t bufferA[BUFFER_SIZE] __attribute__((section(".sdram")));
uint8_t bufferB[BUFFER_SIZE] __attribute__((section(".sdram")));
volatile uint8_t dma_done = 0;
void DMA_Transfer_Start(uint8_t *src) {
// 配置 DMA 从 src 搬运数据到外设(如 UART)
// 此处省略具体 DMA 初始化
}
void CPU_Process_Data(uint8_t *buf) {
// 处理数据
}
void DMA_IRQHandler(void) {
if (dma_done) {
// 上一次传输完成,处理 Buffer B(假设 B 刚被 DMA 写入)
SCB_InvalidateDCache_by_Addr((uint32_t*)bufferB, BUFFER_SIZE);
CPU_Process_Data(bufferB);
// 切换 DMA 源到 Buffer A,准备下一次
DMA_Transfer_Start(bufferA);
} else {
// 处理 Buffer A
SCB_InvalidateDCache_by_Addr((uint32_t*)bufferA, BUFFER_SIZE);
CPU_Process_Data(bufferA);
DMA_Transfer_Start(bufferB);
}
dma_done = !dma_done;
}
void main_loop(void) {
// 初始化 DMA 和缓冲
// CPU 写 bufferA 或 bufferB 后,调用 SCB_CleanDCache_by_Addr 写回
// 然后启动 DMA
}
注意事项
- 双缓冲需要更多内存,但能显著提高吞吐量。
- 确保 DMA 传输完成中断优先级高于 CPU 写操作,避免竞态。
- 缓冲地址需按 32 字节对齐,否则 Cache 维护无效。
总结与选型建议
- 低频操作(如按键响应、配置更新):策略一(手动 Clean/Invalidate)最简单,代码侵入小。
- 高性能且数据一致性要求高(如 LCD 显示):策略二(MPU 非缓存)最稳妥,但牺牲一点 CPU 性能。
- 高频数据流(如音频、ADC):策略三(双缓冲)最佳,平衡性能与一致性。
实际项目中,可组合使用:例如用 MPU 将 SDRAM 设为写通,同时配合双缓冲减少 Cache 维护开销。务必在开发初期就考虑 Cache 一致性,否则后期调试会非常痛苦。希望本文能帮你彻底解决 STM32F4 与 SDRAM 的“缓存错觉”问题。