引言

在嵌入式图形系统中,SDRAM 常被用作帧缓冲(显存),以支持高分辨率 LCD。STM32F4 系列(如 STM32F429/439)内置 FMC 控制器可外接 SDRAM,但若未正确配置 MPU,系统性能可能骤降 50% 以上。本文通过一个真实案例,剖析 MPU 配置错误如何导致显存访问效率低下,并提供可复用的解决方案。

MPU 与 SDRAM 访问原理

1. Cortex-M4 MPU 基础

MPU 是 Cortex-M4 内核的可选组件,用于定义内存区域的访问权限和属性。每个区域由基地址、大小和属性寄存器控制。关键属性包括:

  • TEX (Type Extension): 与 C/B 位共同决定缓存策略
  • C (Cacheable): 是否允许缓存
  • B (Bufferable): 是否允许写缓冲

对于 SDRAM 区域,常见配置有:

| 场景 | TEX | C | B | 效果 | |------|-----|---|---|------| | 普通数据 | 0 | 1 | 1 | 写回缓存,性能高 | | 显存(DMA 访问) | 0 | 0 | 1 | 写缓冲,无缓存,避免不一致 | | 错误配置 | 0 | 1 | 0 | 写直达,但可能引发总线阻塞 |

2. 为什么显存需要特殊配置

LCD 控制器(LTDC)通过 DMA 直接读取 SDRAM 中的像素数据。若 MPU 将 SDRAM 配置为 Cacheable,CPU 写入显存的数据可能滞留在 Cache 中,而 LTDC DMA 直接访问物理 SDRAM,导致显示内容陈旧(撕裂)。反之,若配置为 Non-cacheable 但 Bufferable,CPU 写操作会进入写缓冲,虽无缓存一致性问题,但若缓冲策略不当,可能造成总线占用过高。

问题现象与排查

1. 症状描述

某项目使用 STM32F429 + 外部 SDRAM(1MB)作为 480x272 RGB 屏显存。初始代码未配置 MPU,系统运行正常。后来为了提升 CPU 访问速度,添加了 MPU 配置,将 SDRAM 区域设为 Cacheable(TEX=0, C=1, B=1),结果帧率从 60fps 骤降至 20fps,且画面出现随机条纹。

2. 根因分析

  • 缓存一致性问题:CPU 写入显存后,LTDC DMA 读取旧数据,导致显示错误。
  • 性能骤降:为解决撕裂,开发者尝试在每次刷新后执行 SCB_CleanDCache(),但该操作会强制将整个 Cache 写回 SDRAM,耗时巨大(约 10ms),严重拖慢帧率。
  • 总线冲突:CleanDCache 期间,CPU 和 LTDC 同时访问 SDRAM,产生大量仲裁等待。

标准配置步骤

1. 确定 SDRAM 地址范围

在 STM32F4 上,SDRAM 通常映射在 Bank1/SDRAM 区域,地址从 0xC0000000 开始。例如,使用 1MB SDRAM,区域为 0xC0000000 ~ 0xC00FFFFF。

2. 配置 MPU 区域

推荐配置:Non-cacheable, Bufferable(TEX=0, C=0, B=1),并启用写缓冲以提升 CPU 写性能,同时避免缓存一致性问题。

void MPU_Config(void) {
    MPU_Region_InitTypeDef MPU_InitStruct;

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

    // 配置 SDRAM 区域为 Non-cacheable, Bufferable
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = 0xC0000000;
    MPU_InitStruct.Size = MPU_REGION_SIZE_1MB;
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE;
    MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
    MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
    MPU_InitStruct.Number = MPU_REGION_NUMBER0;
    MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;

    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    // 使能 MPU
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

3. 初始化顺序

在系统启动早期(SystemInit 后)调用 MPU_Config(),确保在 SDRAM 初始化前生效。

int main(void) {
    HAL_Init();
    SystemClock_Config();
    MPU_Config();  // 先配置 MPU
    SDRAM_Init();  // 再初始化 SDRAM
    LTDC_Init();
    // ...
}

完整代码示例

以下是一个完整的 MPU 配置函数,包含错误检查:

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

    // 禁用 MPU
    HAL_MPU_Disable();

    // 配置 SDRAM 区域(0xC0000000, 1MB)
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = 0xC0000000;
    MPU_InitStruct.Size = MPU_REGION_SIZE_1MB;
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE;
    MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
    MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
    MPU_InitStruct.Number = MPU_REGION_NUMBER0;
    MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;

    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    // 使能 MPU,默认背景区域为特权不可访问
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);

    // 验证配置(可选)
    uint32_t attr = MPU->RBAR & MPU_RBAR_ATTR_MASK;
    if (attr != 0x03000000) { // 检查 TEX=0, C=0, B=1
        Error_Handler();
    }
}

注意事项

  • 区域大小必须为 2 的幂:MPU 区域大小支持 32B 到 4GB,但必须与地址对齐。若 SDRAM 大小不是 2 的幂,需拆分为多个区域或使用更大的区域(注意越界风险)。
  • 不要使用 Cacheable 配置:除非你使用双缓冲并手动管理缓存一致性,否则强烈建议 Non-cacheable。
  • 写缓冲的影响:Bufferable 允许 CPU 写操作异步完成,但若后续有读操作,需确保内存屏障(__DSB())保证顺序。
  • LTDC 的 DMA 访问:LTDC 使用 AHB 总线,不受 MPU 影响,但缓存不一致问题仍会出现,因此 Non-cacheable 是安全选择。
  • 性能对比:实测 Non-cacheable + Bufferable 比无 MPU 配置(默认全缓存)性能略低(约 5%),但远优于因 CleanDCache 导致的骤降。

总结

MPU 配置是 STM32F4 使用 SDRAM 作为显存时的关键优化点。错误的缓存策略会导致性能骤降和显示异常。通过将 SDRAM 区域配置为 Non-cacheable 且 Bufferable,可以避免缓存一致性问题,同时保持较好的写入性能。本文提供的配置代码可直接集成到项目中,帮助开发者快速排查类似问题。记住:在嵌入式系统中,性能优化往往需要深入理解硬件架构,而非盲目添加缓存。