引言

在嵌入式 GUI 开发中,STM32F4 配合外部 SDRAM 作为显存是常见做法,尤其当分辨率较高时,内部 SRAM 往往不够用。但很多开发者会遇到一个棘手问题:显示画面随机花屏、条纹或局部错乱,且程序逻辑看似完全正确。这类问题往往不是 LCD 驱动或时序问题,而是 MPU(Memory Protection Unit)配置不当 导致 Cache 与 SDRAM 数据不一致。

本文将基于 STM32F429 平台,深入分析 MPU 在 SDRAM 显存场景下的关键作用,并给出可落地的解决方案。

1. 问题现象与初步定位

1.1 典型现象

  • 画面初始化时正常,运行一段时间后出现随机色块或条纹。
  • 使用 DMA2D 搬运图像时,偶发数据错位。
  • 开启 D-Cache 后问题加剧,关闭 D-Cache 后问题消失。

1.2 初步排查方向

  • 检查 SDRAM 时序(刷新率、行列延迟)——通常正常。
  • 检查 LTDC 与 DMA2D 配置——通常无误。
  • 检查内存访问冲突——发现 SDRAM 区域被多个外设访问,但未配置 MPU。

关键点:STM32F4 的 D-Cache 默认是开启的(在 SystemInit 后),而 SDRAM 是外部存储器,默认被配置为 Write-back 模式,这会导致 Cache 与 SDRAM 内容不一致。

2. 原理剖析:MPU、Cache 与 SDRAM 的关系

2.1 为什么需要 MPU?

MPU 是 Cortex-M4 内核提供的存储保护单元,除了访问权限控制外,更重要的是定义内存区域的 Cache 策略。对于外部 SDRAM,我们需要明确告诉 CPU:

  • 该区域是否可缓存(Cacheable)?
  • 写策略是 Write-back 还是 Write-through?
  • 是否允许 DMA 访问?

2.2 默认配置的陷阱

STM32F4 启动代码(startup_stm32f429xx.s)中,SystemInit 会设置默认的 MPU 配置,但通常不包含外部 SDRAM 区域。此时,SDRAM 被当作普通内存,D-Cache 默认以 Write-back 策略工作。

当 CPU 写入显存时,数据先进入 Cache,而 DMA2D 或 LTDC 直接读取 SDRAM 物理地址,就会读到旧数据,导致花屏。

2.3 正确策略:Write-through 或 Non-cacheable

对于显存这类需要 CPU 与 DMA 共享的数据区域,推荐使用 Write-through 策略,确保 CPU 写入时同步更新 SDRAM。或者干脆配置为 Non-cacheable,但会牺牲性能。

3. 配置步骤与代码实现

3.1 硬件环境

  • MCU:STM32F429ZIT6
  • SDRAM:IS42S16400J(1M x 16bit x 4 banks),映射地址 0xC0000000
  • LCD:RGB565 接口,分辨率 480x272

3.2 MPU 配置代码

以下代码在 main 函数初始化阶段调用,配置 SDRAM 区域为 Write-through 策略。

#include "stm32f4xx_hal.h"

void MPU_Config(void)
{
    MPU_Region_InitTypeDef MPU_InitStruct;

    // 禁用 MPU 进行配置
    HAL_MPU_Disable();

    // 配置 SDRAM 区域:基地址 0xC0000000,大小 8MB(实际可用 4MB)
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = 0xC0000000;
    MPU_InitStruct.Size = MPU_REGION_SIZE_8MB; // 根据实际 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_LEVEL0; // TEX=000,配合 C=1, B=1 得到 Write-through
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE;

    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    // 使能 MPU
    HAL_MPU_Enable(MPU_CONTROL_PRIVILEGED_DEFAULT);
}

// 在 main 中调用
int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MPU_Config(); // 必须在初始化 SDRAM 和 LTDC 之前
    // ... 其他初始化
}

关键参数说明

  • TypeExtField = MPU_TEX_LEVEL0IsCacheable=1, IsBufferable=1 时,内存属性为 Write-through, no write allocate,适合显存。
  • 若希望完全禁用 Cache,可设置 IsCacheable = MPU_ACCESS_NOT_CACHEABLE,但性能会下降。

3.3 验证与测试

配置后,运行图形测试程序(如绘制渐变条、随机色块),观察是否还有花屏。同时可测量性能,确认 Write-through 对帧率影响可接受。

4. 完整示例:SDRAM 初始化 + MPU 配置

以下是一个简化的初始化流程,展示 MPU 与 SDRAM 的配合。

void System_Init(void)
{
    // 1. 时钟配置(略)
    // 2. MPU 配置(必须在 SDRAM 使用前)
    MPU_Config();

    // 3. SDRAM 初始化(使用 HAL 库)
    SDRAM_Init(); // 内部包含 FMC 配置和 SDRAM 时序

    // 4. LTDC 初始化(略)
    // 5. 测试:向显存写入数据并读取验证
    uint16_t *framebuffer = (uint16_t *)0xC0000000;
    for (uint16_t i = 0; i < 480*272; i++) {
        framebuffer[i] = 0xF800; // 红色
    }
    // 若配置正确,屏幕应显示纯红色,无花屏
}

5. 常见误区与注意事项

  • 误区1:MPU 配置必须在 SDRAM 初始化之前。实际上顺序无关,但必须在任何访问 SDRAM 之前完成,否则可能产生总线错误。
  • 误区2:所有外部内存都配置为 Write-back。对于显存,Write-back 会导致 DMA 读取旧数据,必须用 Write-through 或 Non-cacheable。
  • 注意事项1:MPU 区域大小必须覆盖整个 SDRAM,否则未覆盖部分仍使用默认策略。
  • 注意事项2:如果使用 RTOS,需确保 MPU 配置在任务调度前完成
  • 注意事项3:调试时可用硬件断点观察 Cache 操作,或使用 SCB_CleanDCache() 手动刷新,但治标不治本。

6. 总结

STM32F4 使用 SDRAM 作为显存时,MPU 配置是决定系统稳定性的关键。通过将 SDRAM 区域配置为 Write-through 策略,可以避免 CPU 与 DMA 之间的数据不一致,彻底解决花屏问题。本文提供的配置代码可直接移植到实际项目中,建议开发者根据具体 SDRAM 大小调整区域尺寸,并在项目初期就加入 MPU 配置,避免后期排查的烦恼。

希望这篇文章能帮你少走弯路。如果你有类似问题,欢迎交流!