# 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 重新加载。 ### 配置步骤 1. 确保在 `system_stm32f4xx.c` 中启用了 D-Cache(`SCB_EnableDCache()`)。 2. 在 CPU 写入 SDRAM 数据后,调用 `SCB_CleanDCache()` 或按地址范围清理。 3. 在 DMA 写入 SDRAM 后,CPU 读取前,调用 `SCB_InvalidateDCache()`。 ### 代码示例 ```c #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,从根本上避免一致性问题。适合对性能要求不高但要求数据绝对一致的场景。 ### 配置步骤 1. 启用 MPU 并配置一个区域,覆盖 SDRAM 地址范围。 2. 设置属性为 `MPU_REGION_NO_CACHE`(不缓存)或 `MPU_REGION_WRITE_THROUGH`。 3. 在系统初始化时调用 MPU 配置函数。 ### 代码示例 ```c #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 的传输完成中断来同步数据。 ### 配置步骤 1. 在 SDRAM 中分配两个缓冲区(Buffer A 和 B)。 2. CPU 写 Buffer A 时,Buffer B 正被 DMA 使用。 3. 写完后 Clean Buffer A 的 Cache,然后切换 DMA 源地址到 Buffer A。 4. DMA 完成中断中,Invalidate Buffer B 的 Cache(因为 DMA 可能修改了 B),并通知 CPU 处理。 ### 代码示例 ```c #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 的“缓存错觉”问题。