# 一、问题现象:SDRAM 中的图像数据莫名“花屏” 在某基于 STM32F429 的图形显示项目中,使用 LTDC 从外部 SDRAM 读取帧缓冲数据。系统运行一段时间后,屏幕出现随机条纹和错位像素,且仅在开启 D-Cache 时出现。关闭 D-Cache 后问题消失,但性能下降明显。这强烈暗示 D-Cache 与 SDRAM 之间存在数据一致性问题。 ## 1.1 硬件环境 - MCU:STM32F429ZIT6(Cortex-M4,带 16KB D-Cache) - 外部 SDRAM:W9825G6KH,32MB,16-bit 总线,挂载在 FMC 的 Bank1 - 显示接口:LTDC,RGB565,分辨率 800x480 - 数据传输:CPU 写入图像数据到 SDRAM,LTDC 通过 DMA 读取 SDRAM 显示 ## 1.2 关键代码(问题版本) ```c // 初始化 SDRAM 和 LTDC 后,开启 D-Cache SCB_EnableDCache(); // 在 SDRAM 中写入图像数据(例如填充颜色) void FillSDRAM(uint32_t addr, uint32_t size, uint16_t color) { uint16_t *p = (uint16_t *)addr; for (uint32_t i = 0; i < size; i++) { p[i] = color; } } // 主循环中调用 FillSDRAM(0xC0000000, 800*480, 0xFFFF); // 期望全白 ``` 现象:屏幕显示并非全白,而是出现大量随机条纹。 # 二、原理剖析:D-Cache 与 SDRAM 的“各自为政” ## 2.1 D-Cache 的工作机制 Cortex-M4 的 D-Cache 是 4 路组相联,缓存行大小为 32 字节。当 CPU 访问内存地址时,首先查找 Cache,命中则直接返回,未命中则从主存(SDRAM)加载整个缓存行到 Cache。 ## 2.2 一致性问题的根源 - **写操作**:CPU 写数据时,若采用写回(Write-back)策略,数据先写入 Cache,标记为脏(Dirty),但不会立即更新 SDRAM。只有当缓存行被替换或显式清理时,才写回 SDRAM。 - **读操作**:CPU 读数据时,若 SDRAM 中的数据被 DMA 或其他外设修改,而 Cache 中仍有旧副本,则 CPU 会读到过时数据。 在本例中,CPU 通过 `FillSDRAM` 写入图像数据,数据可能残留在 Cache 中。LTDC 的 DMA 控制器直接读取 SDRAM 地址,但此时 SDRAM 中可能还是旧数据或未写回的部分数据,导致显示花屏。 ## 2.3 MPU 的作用 默认情况下,STM32F4 的 D-Cache 对所有内存区域都是开启的,且使用写回策略。我们可以通过 MPU 将 SDRAM 区域配置为“写透”(Write-through)或“不可缓存”(Non-cacheable),以规避一致性问题,但这会牺牲性能。更精细的做法是保持缓存,但在关键操作前后手动维护一致性。 # 三、实测排查:定位问题根源 ## 3.1 复现与观察 - 在 `FillSDRAM` 后,立即读取 SDRAM 地址并打印,发现数据正确(因为 CPU 读 Cache 命中)。 - 但通过调试器查看 SDRAM 内存窗口,发现大量地址数据未更新。 - 使用逻辑分析仪抓取 FMC 总线,发现写入 SDRAM 的数据不连续,且部分写入被延迟。 ## 3.2 验证假设 编写测试代码:在 `FillSDRAM` 后,调用 `SCB_CleanDCache()` 强制写回,再观察显示。结果屏幕正常。这证实了问题正是 D-Cache 未写回所致。 # 四、解决方案:正确维护 D-Cache 一致性 ## 4.1 方案一:配置 MPU 将 SDRAM 区域设为写透(推荐) ```c void MPU_Config(void) { MPU_Region_InitTypeDef MPU_InitStruct; // 禁用 MPU HAL_MPU_Disable(); // 配置 SDRAM 区域(地址 0xC0000000,大小 32MB) MPU_InitStruct.Enable = MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress = 0xC0000000; MPU_InitStruct.Size = MPU_REGION_SIZE_32MB; MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS; MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE; MPU_InitStruct.IsCacheable = MPU_ACCESS_CACHEABLE; // 允许缓存 MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE; MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL1; // 写透,读分配 MPU_InitStruct.SubRegionDisable = 0x00; MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE; HAL_MPU_ConfigRegion(&MPU_InitStruct); // 使能 MPU HAL_MPU_Enable(MPU_CONTROL_PRIVILEGED_DEFAULT); } ``` 注意:设置 `TypeExtField = MPU_TEX_LEVEL1` 且 `IsCacheable = 1`,`IsBufferable = 0`,对应写透策略。这样 CPU 写操作会同时更新 Cache 和 SDRAM,但读操作仍可命中 Cache,性能损失较小。 ## 4.2 方案二:手动维护 Cache(适用于 DMA 频繁场景) 在每次 CPU 写 SDRAM 后,调用 `SCB_CleanDCache()` 或按地址范围清理;在 DMA 写入 SDRAM 后,CPU 读取前,调用 `SCB_InvalidateDCache()`。 ```c // 示例:CPU 写数据到 SDRAM,然后通知 DMA 读取 void WriteData_ToSDRAM(uint32_t addr, uint32_t len) { // 写入数据... SCB_CleanDCache_by_Addr((uint32_t*)addr, len); // 写回 SDRAM } // 示例:DMA 从 SDRAM 读取数据后,CPU 读取前 void ReadData_FromSDRAM(uint32_t addr, uint32_t len) { SCB_InvalidateDCache_by_Addr((uint32_t*)addr, len); // 使 Cache 失效 // 然后读取 addr 数据... } ``` 注意:`SCB_CleanDCache_by_Addr` 和 `SCB_InvalidateDCache_by_Addr` 的地址参数需要 32 字节对齐,长度也需是 32 的倍数,否则需手动调整。 # 五、完整修复代码示例(基于 HAL 库) ```c // 初始化函数中调用 MPU_Config(),然后使能 D-Cache void SystemClock_Config(void); // 略 int main(void) { HAL_Init(); SystemClock_Config(); // 配置 MPU(在使能 Cache 之前) MPU_Config(); // 使能 D-Cache(注意:MPU 配置必须在使能 Cache 之前完成) SCB_EnableDCache(); // 初始化 SDRAM、LTDC 等... // 填充图像数据 FillSDRAM(0xC0000000, 800*480, 0xFFFF); // 由于 MPU 配置为写透,无需手动清理,但为保险可调用一次 SCB_CleanDCache(); while (1) { // 主循环... } } ``` # 六、注意事项与经验总结 - **MPU 配置必须在使能 D-Cache 之前**,否则无效。 - 如果使用手动维护方式,务必注意地址对齐和长度对齐(32 字节)。 - 对于 DMA 双向传输(如 ADC 采集到 SDRAM),建议使用 MPU 配置为“不可缓存”或“写透”,避免频繁手动操作。 - 调试时,可通过查看 Cache 操作后的 SDRAM 内存窗口验证一致性。 - 在 FreeRTOS 等 RTOS 环境中,多任务访问共享 SDRAM 时,需确保任务间同步,防止 Cache 操作交错。 通过本次实测,我们深刻理解了 D-Cache 与 SDRAM 的一致性机制,并掌握了两种有效修复方法。推荐优先使用 MPU 配置写透,兼顾性能与正确性。希望本文能帮助你在 STM32F4 开发中避免同类“坑”。