引言:伪影不只是硬件问题

在嵌入式GUI开发中,STM32F4搭配外部SDRAM(如W9825G6KH)作为显存是常见方案。然而,当画面出现随机条纹、局部花屏或更新残留时,工程师往往首先怀疑SDRAM时序或布线。但一个隐蔽的软件因素——MPU配置——可能才是罪魁祸首。Cortex-M4内核默认对SDRAM区域采用写透(Write-Through)缓存策略,这会导致CPU与DMA(如LTDC或DMA2D)访问同一内存时产生数据不一致,从而在刷新过程中出现伪影。

原理:MPU如何影响SDRAM访问

1. Cortex-M4的缓存与MPU角色

Cortex-M4没有L1数据缓存(部分型号如F7有),但MPU仍控制着内存区域的访问属性和缓存策略。对于外部存储器(如SDRAM),MPU可设置以下关键位:

  • TEX[2:0]:与C/B位组合决定缓存类型。
  • C(Cacheable):是否允许缓存。
  • B(Bufferable):是否允许写缓冲。

默认情况下,MPU未配置时,SDRAM区域采用**Write-Through(写透)**策略(C=1, B=0),即CPU写操作直接穿透到SDRAM,但读操作可能缓存。这看似安全,却存在隐患:当DMA(如LTDC)在后台读取显存时,CPU新写入的数据若仍在写缓冲中(Write-Back模式),DMA可能读到旧数据;反之,CPU读缓存可能错过DMA更新的数据。

2. 伪影产生的具体场景

  • LTDC刷新:LTDC持续从SDRAM读取像素数据。若CPU更新显存后,写操作未及时到达SDRAM(因写缓冲),LTDC可能读到混合新旧数据,形成撕裂或条纹。
  • DMA2D搬运:使用DMA2D进行图形加速时,若CPU与DMA2D并发访问同一区域,缓存不一致导致数据错乱。

配置步骤:正确设置MPU区域

1. 确定SDRAM地址范围

以STM32F429 Discovery板为例,SDRAM映射在Bank1的0xC0000000,大小8MB。

2. 编写MPU初始化函数

void MPU_Config(void)
{
    MPU_Region_InitTypeDef MPU_InitStruct;

    // 禁用MPU以进行配置
    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.SubRegionDisable = 0;
    MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL1;  // TEX=001
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
    MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
    MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;  // C=0
    MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE;    // B=1

    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    // 使能MPU
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

关键点:设置IsCacheable = MPU_ACCESS_NOT_CACHEABLE(C=0)和IsBufferable = MPU_ACCESS_BUFFERABLE(B=1),即Write-Back, No Write-Allocate策略。这样CPU写操作会先进入写缓冲,但读操作不缓存,确保DMA与CPU看到的数据一致。

3. 在系统初始化中调用

main()函数中,HAL_Init()之后、初始化SDRAM和LTDC之前调用MPU_Config()

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MPU_Config();  // 必须在SDRAM初始化前
    SDRAM_Init();
    LTDC_Init();
    // ...
}

完整代码示例:结合SDRAM和LTDC

以下代码片段展示了MPU配置与SDRAM初始化的整合(基于STM32CubeF4):

// 包含头文件
#include "stm32f4xx_hal.h"

// SDRAM句柄
SDRAM_HandleTypeDef hsdram1;

// MPU配置函数(如上)
void MPU_Config(void) { /* 同上 */ }

// SDRAM初始化(简化)
void SDRAM_Init(void)
{
    // 配置FMC控制器,时序参数略
    hsdram1.Instance = FMC_SDRAM_DEVICE;
    // ... 初始化代码
}

// 主程序
int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MPU_Config();
    SDRAM_Init();
    
    // 启用LTDC(假设已配置)
    LTDC_Init();
    
    // 清除屏幕为蓝色
    uint32_t *pBuf = (uint32_t*)0xC0000000;
    for (uint32_t i = 0; i < (800*480); i++) {
        pBuf[i] = 0x00FF0000; // 蓝色
    }
    // 启动LTDC
    HAL_LTDC_Start(&hltdc);
    while(1);
}

注意事项与调试技巧

  • MPU配置时机:必须在SDRAM初始化之前,否则SDRAM控制器可能已按默认策略运行。
  • 区域大小对齐:MPU区域大小必须是2的幂次,且基地址对齐。8MB区域需基地址0xC0000000对齐。
  • 其他外设区域:若使用DMA2D,其目标区域也需同样配置;若使用FMC的NOR/PSRAM,可单独配置。
  • 调试方法:若伪影仍存在,可尝试将IsBufferable设为MPU_ACCESS_NOT_BUFFERABLE(B=0),强制写透,但性能下降。也可通过逻辑分析仪对比SDRAM读写时序。
  • 性能权衡:Write-Back策略提升CPU写性能,但需注意在DMA读取前执行__DSB()(数据同步屏障)确保写缓冲排空。

结语

MPU配置是STM32高性能图形应用中的隐形开关。通过正确设置SDRAM区域的缓存属性,可以避免因CPU与DMA访问不一致导致的伪影问题。希望本文的排查思路能帮助开发者少走弯路,让画面如丝般顺滑。