# 引言 在 STM32F4 系列(如 STM32F429/439)中,D-Cache 的引入显著提升了 CPU 访问内部 SRAM 的速度,但当外部 SDRAM 介入时,数据一致性(Cache Coherency)问题便成为开发者的噩梦。本文以一个真实项目为例,讲述因 D-Cache 未正确处理导致的显示花屏、数据错乱问题,并给出系统性的 flush/invalidate 策略。 # D-Cache 与 SDRAM 的冲突根源 ## 1. Cache 的工作原理 D-Cache 是 CPU 与主存(如 SDRAM)之间的高速缓存,以 32 字节(或 64 字节)为一行(Line)存储数据。当 CPU 读取数据时,若命中 Cache,则直接返回;否则从主存加载整个 Cache Line 到 Cache。写入时,根据策略不同,可能只更新 Cache(写回 Write-back)或同时更新主存(写通 Write-through)。 ## 2. 问题场景 - **CPU 写数据到 SDRAM,但 DMA 外设读取时拿到旧数据**:因为 CPU 写入的数据可能还停留在 Cache 中,尚未写回 SDRAM。 - **DMA 写入 SDRAM,但 CPU 读取时拿到旧数据**:因为 Cache 中保留了旧数据,CPU 读取时命中 Cache,未感知 DMA 的更新。 ## 3. 为什么 STM32F4 默认关闭 D-Cache? STM32F4 的 D-Cache 默认是关闭的,因为其设计初衷是用于内部 SRAM 加速,而外部存储器(如 SDRAM)的时序和共享访问(如 DMA)容易引发一致性问题。开启后,必须由软件管理一致性。 # 实战踩坑记录 ## 项目背景 使用 STM32F429 驱动 4.3 寸 RGB 屏,显存位于外部 SDRAM(地址 0xC0000000),同时使用 DMA2D 进行图形加速,LTDC 控制器直接读取 SDRAM 刷新屏幕。 ## 现象 - 屏幕显示花屏,部分区域出现残影。 - 通过 CPU 写入的图形数据,DMA2D 读取时偶尔错误。 - 调试时发现,关闭 D-Cache 后一切正常,但性能下降明显。 ## 根因分析 1. CPU 写入显存数据到 SDRAM,但数据留在 D-Cache 中,LTDC 读取 SDRAM 时未看到最新数据。 2. DMA2D 从 SDRAM 读取源数据时,CPU 之前写入的数据可能未写回,导致 DMA 拿到旧数据。 3. 反之,DMA2D 写入结果到 SDRAM,但 CPU 读取时命中 Cache 中的旧数据。 # 解决方案:flush 与 invalidate 策略 ## 1. 配置 MPU 区域 首先,将 SDRAM 区域配置为 Write-back 模式,并允许 Cache 使用。使用 `MPU_Config()` 函数设置区域属性。 ```c 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; MPU_InitStruct.Size = MPU_REGION_SIZE_4MB; // 根据 SDRAM 大小调整 MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS; MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE; MPU_InitStruct.IsCacheable = MPU_ACCESS_CACHEABLE; MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE; MPU_InitStruct.Number = MPU_REGION_NUMBER0; MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL1; // Write-back, no write allocate MPU_InitStruct.SubRegionDisable = 0x00; MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE; HAL_MPU_ConfigRegion(&MPU_InitStruct); HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT); } ``` ## 2. 开启 D-Cache 在系统初始化时,调用 `SCB_EnableDCache()` 开启 D-Cache。注意,开启后所有内存访问都经过 Cache,包括 SDRAM。 ```c SCB_EnableDCache(); ``` ## 3. 数据一致性操作 ### 3.1 CPU 写数据到 SDRAM,供外设(如 DMA2D、LTDC)读取 在 CPU 写入完成后,必须调用 `SCB_CleanDCache()` 或 `SCB_CleanDCache_by_Addr()` 将 Cache 中的数据写回 SDRAM。 ```c // 假设写入 100 字节到 0xC0000000 uint32_t addr = 0xC0000000; for (int i = 0; i < 100; i++) { *(uint8_t *)(addr + i) = i; } // 将 Cache 中该区域的数据写回 SDRAM SCB_CleanDCache_by_Addr((uint32_t *)addr, 100); ``` ### 3.2 外设(如 DMA)写入数据到 SDRAM,供 CPU 读取 在 DMA 传输完成后,必须调用 `SCB_InvalidateDCache()` 或 `SCB_InvalidateDCache_by_Addr()` 使 Cache 中的旧数据失效,强制 CPU 从 SDRAM 重新加载。 ```c // DMA 传输完成中断中 uint32_t addr = 0xC0000000; SCB_InvalidateDCache_by_Addr((uint32_t *)addr, 100); // 现在 CPU 读取 addr 时,会从 SDRAM 获取最新数据 ``` ### 3.3 全量操作 如果数据量较大或区域不连续,可以直接使用全量操作(注意性能开销): ```c SCB_CleanDCache(); // 写回所有脏数据 SCB_InvalidateDCache(); // 使所有 Cache 行无效 ``` ## 4. 完整示例:DMA 传输与 Cache 维护 以下是一个典型场景:CPU 准备数据,通过 DMA 发送到 SDRAM,然后 DMA 从 SDRAM 读取结果。 ```c // 缓冲区地址(SDRAM) #define BUF_ADDR 0xC0000000 #define BUF_SIZE 1024 void DMA_Transfer_Example(void) { // 1. CPU 写入数据到 SDRAM for (int i = 0; i < BUF_SIZE; i++) { *((uint8_t *)BUF_ADDR + i) = i; } // 2. 写回 Cache,确保数据在 SDRAM 中 SCB_CleanDCache_by_Addr((uint32_t *)BUF_ADDR, BUF_SIZE); // 3. 启动 DMA 读取 SDRAM 数据(例如 DMA2D 或普通 DMA) // ... 配置 DMA 并启动 // 4. 等待 DMA 完成(中断或轮询) // ... // 5. 使 Cache 失效,以便 CPU 读取 DMA 写入的新数据 SCB_InvalidateDCache_by_Addr((uint32_t *)BUF_ADDR, BUF_SIZE); // 6. 现在可以安全读取 BUF_ADDR 的数据 } ``` # 注意事项 - **地址对齐**:`SCB_CleanDCache_by_Addr` 和 `SCB_InvalidateDCache_by_Addr` 的地址必须 32 字节对齐,长度也建议对齐到 32 的倍数,否则可能操作不完整。 - **性能权衡**:频繁的 flush/invalidate 会降低性能,建议在批量操作后执行,而非每次小写入。 - **DMA 描述符**:如果使用 DMA 描述符,描述符本身也可能被 Cache 缓存,需要同样处理。 - **中断安全**:在中断中执行 Cache 操作时,注意优先级和嵌套,避免死锁。 - **调试技巧**:若怀疑一致性问题,可临时关闭 D-Cache 验证,但最终必须正确实现维护。 # 总结 STM32F4 的 D-Cache 是一把双刃剑,正确使用能大幅提升性能,但忽视一致性则会导致随机故障。通过配置 MPU 区域为 Write-back,并在关键操作前后调用 `SCB_CleanDCache` 和 `SCB_InvalidateDCache`,可以彻底解决 SDRAM 与 Cache 的数据冲突。记住:**CPU 写后要 Clean,外设写后要 Invalidate**。希望本文能帮助你少走弯路,写出稳定高效的嵌入式代码。