一、问题现象与背景

某项目基于 STM32F429,使用外部 SDRAM(如 W9825G6KH)作为 LTDC 的显存,分辨率 800x480,RGB565。系统运行正常,但偶尔出现花屏:表现为屏幕局部颜色错乱、闪烁,或整屏随机噪点,尤其在 DMA2D 搬运图像或 CPU 写入显存时更频繁。

初步排查:SDRAM 初始化时序正确,读写测试通过;LTDC 配置无误。问题指向 MPU 对 SDRAM 区域的缓存策略配置。

二、MPU 与 SDRAM 显存的关系

STM32F4 的 Cortex-M4 内核带有 MPU,可对内存区域设置访问权限和属性,其中最关键的是 Cache 策略(Write-Back / Write-Through / Non-cacheable)。

SDRAM 作为显存时,存在两个访问方:

  • CPU 或 DMA2D:写入图像数据到显存;
  • LTDC 控制器:持续从显存读取像素刷新屏幕。

若 MPU 将 SDRAM 配置为 Write-Back 模式,CPU 写入的数据会暂存在 Cache 中,不会立即写回 SDRAM。而 LTDC 直接读取 SDRAM,导致它看到的是旧数据,从而产生花屏。

三、典型错误配置

常见错误代码(如使用 STM32CubeMX 默认配置或手动设置):

MPU_Region_InitTypeDef MPU_InitStruct;

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_REGION_BUFFERABLE;
MPU_InitStruct.IsCacheable = MPU_REGION_CACHEABLE;  // 错误:设为 Cacheable
MPU_InitStruct.IsShareable = MPU_REGION_NOT_SHAREABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL1;  // 可能组合出 Write-Back
MPU_InitStruct.SubRegionDisable = 0x00;
MPU_InitStruct.DisableExec = MPU_REGION_DISABLE_EXEC;

HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);

上述配置中,IsCacheable = MPU_REGION_CACHEABLETypeExtField = MPU_TEX_LEVEL1 会得到 Write-Back 策略,导致数据滞留 Cache。

四、排查步骤

  1. 确认 SDRAM 读写正确:先禁用 MPU,用简单的内存写读测试(如写入 0xAA55 再读回)验证 SDRAM 硬件正常。
  2. 检查 LTDC 配置:确认显存地址、像素格式、行偏移等无误。
  3. 观察花屏触发条件:若仅在 CPU 写显存后出现,且过一会儿恢复(Cache 回写),则高度怀疑 Cache 一致性问题。
  4. 临时禁用 MPU 测试:将 MPU 配置为 Non-cacheable 或直接关闭 MPU,若花屏消失,则确认是 MPU 配置问题。

五、修复方案

正确做法:将 SDRAM 显存区域配置为 Write-Through 或 Non-cacheable,保证 CPU 写入立即生效。推荐使用 Write-Through,兼顾性能与一致性。

修复后的 MPU 配置代码:

void MPU_Config_SDRAM_As_Framebuffer(void)
{
    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    HAL_MPU_Disable();

    // 配置 SDRAM 区域(0xC0000000,大小 8MB)
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = 0xC0000000;
    MPU_InitStruct.Size = MPU_REGION_SIZE_8MB;
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.IsBufferable = MPU_REGION_NOT_BUFFERABLE;  // 关键
    MPU_InitStruct.IsCacheable = MPU_REGION_CACHEABLE;        // 允许 Cache,但配合 TEX 设为 Write-Through
    MPU_InitStruct.IsShareable = MPU_REGION_NOT_SHAREABLE;
    MPU_InitStruct.Number = MPU_REGION_NUMBER0;
    MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;  // 与 C=1, B=0 组合为 Write-Through
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.DisableExec = MPU_REGION_DISABLE_EXEC;

    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    // 可选:配置其他区域(如内部 SRAM)为 Write-Back 以提升性能

    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

关键点:TypeExtField = MPU_TEX_LEVEL0IsCacheable=1IsBufferable=0 时,内存属性为 Write-Through,CPU 写入会同时更新 Cache 和 SDRAM,保证 LTDC 读取到最新数据。

六、完整示例:初始化 SDRAM 并配置 MPU

// 初始化 SDRAM(使用 HAL 库,假设已配置 FMC)
extern void SDRAM_Init(void);

void System_Init(void)
{
    HAL_Init();
    SystemClock_Config();
    
    // 1. 初始化 SDRAM
    SDRAM_Init();  // 配置 FMC 和 SDRAM 控制器
    
    // 2. 配置 MPU 针对 SDRAM 区域
    MPU_Config_SDRAM_As_Framebuffer();
    
    // 3. 初始化 LTDC 和 DMA2D
    // LTDC_Init();
    // DMA2D_Init();
}

// 测试:向显存写入颜色数据
void FillScreen(uint16_t color)
{
    uint32_t fb_addr = 0xC0000000;
    for (uint32_t i = 0; i < 800*480; i++)
    {
        *((uint16_t*)fb_addr + i) = color;
    }
}

七、注意事项

  • 不要随意将显存设为 Write-Back:除非你每次写完后手动 SCB_CleanDCache(),但会降低效率且易遗漏。
  • SDRAM 刷新与 Cache 无关:MPU 配置不影响 SDRAM 自刷新,但影响 CPU 访问路径。
  • 多区域配置:如果 SDRAM 同时用于其他数据(如帧缓冲、堆),建议划分不同 MPU 区域,显存区域用 Write-Through,其他区域可用 Write-Back 提升性能。
  • 调试技巧:使用硬件调试器观察 SDRAM 内存值,对比 CPU 写入值与实际内存值,可快速判断 Cache 是否回写。
  • CubeMX 配置:在 CubeMX 中,MPU 设置里选择“Write-Through”或“Non-cacheable”即可,避免手动错误。

八、总结

MPU 配置在 SDRAM 显存场景中至关重要。通过将显存区域设为 Write-Through 策略,保证了 CPU 写入与 LTDC 读取的一致性,彻底解决花屏问题。理解 MPU 的 Cache 属性是嵌入式开发进阶的必备技能,希望本文能帮助你少走弯路。