一、问题现象: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_LEVEL1IsCacheable = 1IsBufferable = 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_AddrSCB_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 开发中避免同类“坑”。