引言

在 STM32F4 系列(如 STM32F407/429)中,D-Cache 和 SDRAM 的组合常被用于高性能图形显示。然而,D-Cache 的缓存机制会导致 CPU 与 DMA 或 LCD 控制器访问同一帧缓冲时出现数据不一致,表现为画面闪烁、残影或数据错误。本文针对这一痛点,提供三种经过实战验证的维护策略,助你彻底解决 Cache 一致性问题。

问题根源:D-Cache 与 SDRAM 的“双轨制”

STM32F4 的 D-Cache 是 4 路组相联、总容量 64KB(部分型号)。当 CPU 写入帧缓冲时,数据可能只停留在 Cache 中,尚未写回 SDRAM。而 LCD 控制器(如 LTDC)或 DMA 直接读取 SDRAM 时,看到的仍是旧数据,导致显示异常。反之,若 DMA 更新了 SDRAM 数据,CPU 读取时可能命中过期的 Cache 行。

策略一:全量失效与清理(简单粗暴)

原理

在每次 CPU 写帧缓冲后、启动 LCD 刷新前,强制将整个 D-Cache 写回(Clean)并失效(Invalidate)。此方法简单可靠,但开销较大,适合帧率要求不高的场景。

配置步骤

  1. 使能 D-Cache:SCB_EnableDCache()
  2. 在关键操作点调用清理和失效函数。

代码示例

#include "stm32f4xx_hal.h"

void LCD_FlushFrameBuffer(uint32_t* fb, uint32_t size) {
    // 确保 CPU 写入全部写回 SDRAM
    SCB_CleanDCache();
    // 启动 LTDC 刷新(或 DMA2D 传输)
    LTDC_Reload();
}

void DMA_UpdateFrameBuffer(uint32_t* fb, uint32_t size) {
    // DMA 写入 SDRAM 后,使 D-Cache 失效,避免 CPU 读到旧数据
    SCB_InvalidateDCache();
    // 此时 CPU 访问 fb 将重新从 SDRAM 读取
}

注意事项

  • 全量清理/失效会暂停 CPU 访问,影响实时性。
  • 适用于帧缓冲较小或刷新率低于 30fps 的场景。

策略二:按区域维护(精准高效)

原理

只对修改过的帧缓冲区域执行 Clean 或 Invalidate,利用 SCB_CleanDCache_by_AddrSCB_InvalidateDCache_by_Addr 函数,按地址和大小操作。这减少了开销,适合频繁局部更新的 UI。

配置步骤

  1. 确保帧缓冲地址按 32 字节对齐(Cache line 大小)。
  2. 在每次局部绘制后,仅清理对应区域。
  3. 在 DMA 接收数据后,仅失效对应区域。

代码示例

#define CACHE_LINE_SIZE 32

void LCD_DrawRect(uint32_t* fb, int x, int y, int w, int h, uint32_t color) {
    // 绘制逻辑(略)
    // 计算区域地址和大小,注意对齐
    uint32_t addr = (uint32_t)(fb + y * 800 + x); // 假设 800 宽
    uint32_t size = w * h * 4; // 32-bit 像素
    // 对齐到 cache line
    uint32_t aligned_addr = addr & ~(CACHE_LINE_SIZE - 1);
    uint32_t aligned_size = (size + (addr - aligned_addr) + CACHE_LINE_SIZE - 1) & ~(CACHE_LINE_SIZE - 1);
    SCB_CleanDCache_by_Addr((uint32_t*)aligned_addr, aligned_size);
    // 触发 LTDC 刷新
}

void DMA_ReceiveData(uint32_t* buf, uint32_t size) {
    // DMA 传输完成后
    uint32_t addr = (uint32_t)buf;
    uint32_t aligned_addr = addr & ~(CACHE_LINE_SIZE - 1);
    uint32_t aligned_size = (size + (addr - aligned_addr) + CACHE_LINE_SIZE - 1) & ~(CACHE_LINE_SIZE - 1);
    SCB_InvalidateDCache_by_Addr((uint32_t*)aligned_addr, aligned_size);
}

注意事项

  • 地址和大小必须对齐到 32 字节,否则会破坏相邻数据。
  • 适合局部刷新频繁的 GUI 应用,性能提升明显。

策略三:MPU 配置免 Cache(彻底隔离)

原理

通过 MPU 将 SDRAM 区域设置为“非缓存”(Normal memory, Non-cacheable),这样 CPU 访问 SDRAM 时直接读写,不经过 D-Cache。彻底避免一致性问题,但牺牲了缓存加速效果。

配置步骤

  1. 初始化 MPU,配置 SDRAM 区域属性。
  2. 使能 MPU。
  3. 注意:此区域内的所有访问都无缓存,适合帧缓冲和 DMA 共享区域。

代码示例

void MPU_Config(void) {
    MPU_Region_InitTypeDef MPU_InitStruct;
    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_ACCESS_NOT_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);
    HAL_MPU_Enable(MPU_CONTROL_HRD_MODE);
}

int main(void) {
    HAL_Init();
    MPU_Config();
    // 后续初始化 SDRAM 和 LCD
}

注意事项

  • 禁用缓存后,CPU 访问 SDRAM 速度下降,但通常 LCD 刷新瓶颈不在 CPU。
  • 适用于帧缓冲较大且频繁 DMA 更新的场景,如视频播放。

总结与选型建议

| 策略 | 开销 | 适用场景 | |------|------|----------| | 全量清理/失效 | 高 | 低帧率、简单界面 | | 按区域维护 | 中 | 局部刷新、GUI 应用 | | MPU 免缓存 | 低(但无缓存加速) | 高带宽 DMA、视频 |

实际项目中,可结合策略二和策略三:将帧缓冲设为非缓存,但将 CPU 频繁读写的辅助缓冲区设为可缓存。务必在开发初期就规划好内存布局,避免后期修改带来的风险。

结语

Cache 一致性是嵌入式图形开发中的经典难题,掌握这三种策略,你就能根据项目需求灵活应对。记住,没有银弹,只有最适合场景的方案。希望本文能助你在 STM32F4 上打造流畅稳定的显示系统。