一、问题现象: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 关键代码(问题版本)
// 初始化 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 区域设为写透(推荐)
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()。
// 示例: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 库)
// 初始化函数中调用 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 开发中避免同类“坑”。