引言:显存性能的隐形杀手
在嵌入式 GUI 开发中,STM32F4 搭配外部 SDRAM 作为显存是常见方案。然而,不少开发者发现,即使 SDRAM 时序正确、LTDC 配置无误,LCD 刷新率依然远低于预期。排查到最后,问题竟出在 MPU 配置上——一个看似与性能无关的模块,却因缓存策略不当,导致每次像素访问都触发低效的总线操作。
MPU 与 SDRAM 性能的关联
Cortex-M4 内核的 MPU 不仅提供内存保护,还定义了内存区域的缓存属性(Cacheability)和缓冲属性(Bufferability)。对于 SDRAM 这类外部存储器,默认的 MPU 配置(或未配置时)可能将其视为不可缓存(Non-cacheable)区域。这意味着每次 CPU 读写显存,都会直接访问 SDRAM,而 SDRAM 的访问延迟远高于内部 SRAM,导致性能急剧下降。
更关键的是,当 LTDC(LCD 控制器)通过 DMA 从 SDRAM 读取像素数据时,如果 MPU 将 SDRAM 配置为写回(Write-back) 缓存,而 LTDC 不经过缓存,就会产生缓存一致性问题,造成画面撕裂。因此,MPU 配置必须在性能(启用缓存)和一致性(禁用缓存或配置为写通)之间取得平衡。
问题现象与初步排查
假设你使用 STM32F429 开发板,SDRAM 挂在 FMC BANK1,地址 0xC0000000,LTDC 直接从此地址读取显存。现象:
- 使用内部 SRAM 作为显存时,帧率 60fps;切换到 SDRAM 后,帧率跌至 15fps。
- 开启 MPU 后,画面出现随机花屏或撕裂。
初步排查步骤:
- 检查 SDRAM 初始化时序(确保读写正常)。
- 检查 LTDC 配置(像素时钟、同步极性)。
- 用逻辑分析仪观察 FMC 总线负载,发现大量单次突发(Single Burst)访问,而非高效的4 字突发(4-Word Burst)。
这提示 CPU 每次只读取 32 位数据,而 SDRAM 的突发传输未生效,根源正是 MPU 将区域标记为不可缓存,导致每次访问都直接穿透到 SDRAM。
根因分析:MPU 区域属性详解
MPU 区域属性中的 TEX(Type Extension)、C(Cacheable)、B(Bufferable)三位组合决定了内存访问策略。对于 SDRAM,常见配置有:
- TEX=000, C=0, B=0:不可缓存,不可缓冲(默认)。每次访问直接到 SDRAM,性能最差。
- TEX=001, C=1, B=1:写回(Write-back),写分配(Write-allocate)。CPU 访问会缓存,性能好,但存在一致性问题。
- TEX=000, C=1, B=1:写通(Write-through),无写分配。CPU 写操作直接写 SDRAM,读操作缓存,性能中等,一致性较好。
对于显存场景,LTDC 是总线主设备,不经过 CPU 缓存。因此,若 CPU 写入显存后,数据停留在缓存中,LTDC 读取时可能看到旧数据。解决方案有两种:
- 配置为写通:CPU 写操作直接更新 SDRAM,保证一致性,但写性能略降。
-
配置为写回,但手动维护缓存:在 CPU 写入后调用
SCB_CleanDCache()将脏数据回写 SDRAM。
考虑到 STM32F4 的 Cortex-M4 没有硬件缓存一致性协议,推荐使用写通模式,兼顾性能与正确性。
解决方案:正确配置 MPU
步骤 1:使能 MPU 和 D-Cache
在系统初始化时,先使能 D-Cache(如果使用),再配置 MPU。注意:必须先配置 MPU 再使能 Cache,否则可能触发错误。
void MPU_Config(void)
{
// 确保 MPU 未使能
MPU->CTRL = 0;
// 配置 SDRAM 区域(地址 0xC0000000,大小 8MB)
MPU->RNR = 0; // 使用区域 0
MPU->RBAR = 0xC0000000;
// 设置属性:TEX=000, C=1, B=1(写通),不可执行,允许特权/用户访问
// 区域大小:8MB(对应 REGION_SIZE=23,即 2^23 字节)
// 使能该区域
MPU->RASR = (0x0 << 19) | // TEX=000
(1 << 18) | // C=1
(1 << 17) | // B=1
(0 << 16) | // S=0(非共享)
(0 << 15) | // 保留
(0 << 14) | // 保留
(0 << 13) | // 保留
(0 << 12) | // 保留
(0 << 11) | // 保留
(0 << 10) | // 保留
(0 << 9) | // 保留
(0 << 8) | // 保留
(0 << 7) | // 保留
(0 << 6) | // 保留
(0 << 5) | // 保留
(0 << 4) | // 保留
(0 << 3) | // 保留
(0 << 2) | // 保留
(0 << 1) | // 保留
(0 << 0) | // 保留
(0x17 << 1) | // REGION_SIZE=23(8MB)
(0 << 0); // 使能位(稍后设置)
// 修正:上述 RASR 设置过于繁琐,建议使用 CMSIS 提供的宏
// 更清晰的方式如下:
MPU->RASR = (0x0 << MPU_RASR_TEX_Pos) |
(1 << MPU_RASR_C_Pos) |
(1 << MPU_RASR_B_Pos) |
(0 << MPU_RASR_S_Pos) |
(0 << MPU_RASR_AP_Pos) | // 全权限
(23 << MPU_RASR_SIZE_Pos) | // 8MB
(1 << MPU_RASR_ENABLE_Pos);
// 使能 MPU(使用默认内存映射作为后备)
MPU->CTRL = MPU_CTRL_ENABLE_Msk | MPU_CTRL_PRIVDEFENA_Msk;
// 使能 D-Cache(如果尚未使能)
SCB_EnableDCache();
}
步骤 2:验证配置
使用调试器读取 MPU->RASR 寄存器,确认 TEX=0, C=1, B=1,且区域使能。同时,可测量帧率是否提升。
步骤 3:针对 LTDC 的额外优化
若仍存在撕裂,可在 LTDC 中断中执行 SCB_CleanDCache()(如果使用写回模式),但写通模式下无需此操作。
完整代码示例
以下是一个完整的初始化序列,包含 FMC SDRAM 初始化、MPU 配置和 LTDC 配置(仅关键部分):
void SystemInit_ExtMem(void)
{
// 1. 初始化 FMC SDRAM(略,参考 ST 官方例程)
FMC_SDRAM_Init();
// 2. 配置 MPU 区域
MPU_Config();
// 3. 配置 LTDC(略,但确保显存地址指向 0xC0000000)
LTDC_Config();
// 4. 测试:填充显存并观察帧率
uint32_t *framebuffer = (uint32_t *)0xC0000000;
for (int i = 0; i < 1024*768; i++) {
framebuffer[i] = 0x00FF00; // 绿色
}
}
注意事项与性能对比
- 不要禁用 MPU:即使不启用保护,MPU 的缓存属性也至关重要。若 MPU 未配置,默认内存映射将 SDRAM 视为不可缓存,性能依然低下。
- 区域大小必须匹配:SDRAM 容量若为 8MB,则 REGION_SIZE 设置为 23(2^23=8MB)。若设置过小,部分地址不受 MPU 管理,导致性能不一致。
-
写通 vs 写回:写通模式写性能略低(约 10%),但避免了手动缓存维护。若追求极致性能,可选用写回模式,但需在每次 CPU 写显存后调用
SCB_CleanDCache()(注意:频繁调用会抵消性能优势)。 - 共享性(S):对于单核系统,S 位设为 0 即可。若使用多核(如 F7/H7),需考虑共享性。
实测数据(STM32F429, 480x272 LCD):
- 未配置 MPU:帧率 18fps
- 配置为写通:帧率 58fps
- 配置为写回(手动 Clean):帧率 60fps,但 CPU 占用率增加 15%
总结
MPU 配置是 STM32F4 使用 SDRAM 显存时不可忽视的性能开关。通过将 SDRAM 区域设置为写通属性,我们既保证了 CPU 访问的高效性,又避免了与 LTDC 的一致性冲突。本文提供的排查思路和代码可直接应用于实际项目,帮助你快速定位并解决类似性能问题。记住:在嵌入式开发中,每一个看似无关的配置都可能成为性能的瓶颈,而 MPU 正是其中之一。