引言

在 STM32F4 系列(如 STM32F429/469)驱动 TFT-LCD 或图形加速器(LTDC、DMA2D)时,SDRAM 常被用作帧缓冲(Framebuffer)。然而,当 CPU 通过 D-Cache 写入帧缓冲,而 DMA2D 或 LTDC 直接读取 SDRAM 时,D-Cache 中的脏数据尚未回写,外设读到的却是旧数据,这就是经典的 Cache 一致性问题。若不处理,画面会出现撕裂、花屏或数据错乱。

本文面向有一定嵌入式基础的开发者,介绍三种高效的维护策略,并给出可落地的代码示例。

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

STM32F4 的 D-Cache(Cortex-M4 内核)是写回(Write-back)模式,CPU 写数据时先写入 Cache,标记为脏(Dirty),只有在缓存行被替换或显式 Clean 时才回写 SDRAM。而 DMA2D、LTDC 等外设直接访问 SDRAM,不经过 Cache。因此,CPU 写入帧缓冲后,外设可能读到旧数据;反之,外设写入(如摄像头采集)后,CPU 可能读到 Cache 中的旧副本。

策略一:直接关闭 D-Cache(最简单,但性能损失大)

原理

关闭 D-Cache 后,CPU 每次读写都直接访问 SDRAM,彻底避免一致性问题。但代价是 CPU 访问 SDRAM 的延迟显著增加(SDRAM 通常 10-20ns,而 Cache 命中仅 1-2ns),图形渲染性能可能下降 30%-50%。

配置步骤

  1. 在系统初始化时调用 SCB_DisableDCache()
  2. 确保所有外设(LTDC、DMA2D)正常工作。

代码示例

#include "stm32f4xx.h"

void SystemInit_CacheDisabled(void) {
    SCB_DisableDCache();  // 关闭 D-Cache
    // 后续所有 SDRAM 访问均直接走 AHB 总线
}

// 使用示例:初始化 LTDC 和 SDRAM 后,直接操作帧缓冲
void DrawPixel(uint32_t x, uint32_t y, uint16_t color) {
    uint16_t *fb = (uint16_t*)SDRAM_FB_ADDR;
    fb[y * LCD_WIDTH + x] = color;  // 无 Cache,直接写 SDRAM
}

注意事项

  • 适合对性能要求不高、或帧缓冲访问频率低的场景(如静态图片显示)。
  • 若同时使用 DMA2D 做图形加速,DMA2D 性能不受影响,但 CPU 操作会变慢。

策略二:手动 Clean + Invalidate(灵活,但需精细控制)

原理

保持 D-Cache 开启,在关键操作前后手动维护一致性。具体来说:

  • 当 CPU 写入帧缓冲后,调用 SCB_CleanDCache_by_Addr() 将脏数据回写 SDRAM,确保外设可读。
  • 当外设写入内存(如摄像头 DMA 采集)后,调用 SCB_InvalidateDCache_by_Addr() 使 Cache 行失效,强制 CPU 下次从 SDRAM 重新读取。

配置步骤

  1. 开启 D-Cache(默认开启,也可显式调用 SCB_EnableDCache())。
  2. 在每次 CPU 写帧缓冲后,调用 Clean 函数。
  3. 在每次外设写内存后,调用 Invalidate 函数。

代码示例

#include "stm32f4xx.h"

#define FB_ADDR  0xC0000000  // SDRAM 帧缓冲地址
#define FB_SIZE  (800*480*2) // 假设 800x480 RGB565

// CPU 绘制完成后,回写脏数据
void Framebuffer_Flush(void) {
    SCB_CleanDCache_by_Addr((uint32_t*)FB_ADDR, FB_SIZE);
}

// 外设(如摄像头 DMA)写入后,使 Cache 失效
void Framebuffer_Invalidate(void) {
    SCB_InvalidateDCache_by_Addr((uint32_t*)FB_ADDR, FB_SIZE);
}

// 使用示例:绘制一帧后刷新
void DrawFrame(void) {
    // ... 绘制操作,直接写 FB_ADDR ...
    Framebuffer_Flush();  // 确保 LTDC 读到最新数据
}

注意事项

  • 地址必须 32 字节对齐(Cache line 大小),大小需为 32 的倍数,否则可能遗漏部分缓存行。
  • 频繁 Clean 会带来性能开销,建议在帧绘制完成后一次性 Clean,而不是每像素操作。
  • 若使用 DMA2D 进行块拷贝,需在启动 DMA2D 前 Clean 源地址,在完成后 Invalidate 目标地址。

策略三:MPU 配置 Non-cacheable 区域(推荐,性能与一致性兼得)

原理

通过 MPU(Memory Protection Unit)将 SDRAM 帧缓冲区域设置为 Non-cacheable(或 Write-through),这样 CPU 访问该区域时绕过 D-Cache,直接读写 SDRAM,而其他内存区域(如 SRAM)仍保持 Cache 加速。这是最优雅的方案,无需手动维护,且性能损失极小(仅帧缓冲区域无缓存)。

配置步骤

  1. 初始化 MPU,配置一个 Region 覆盖 SDRAM 地址范围。
  2. 设置 Region 属性为 Non-cacheable(TEX=0, C=0, B=1 或 TEX=1, C=0, B=0,具体见参考手册)。
  3. 使能 MPU 和 D-Cache。

代码示例

#include "stm32f4xx.h"

void MPU_Config_NonCacheable(void) {
    // 禁用 MPU 进行配置
    MPU->CTRL = 0;
    
    // 配置 Region 0:SDRAM 区域
    MPU->RNR = 0;  // Region 0
    MPU->RBAR = 0xC0000000;  // 基地址
    // 大小:256MB(0x10000000),使能,子区域使能
    MPU->RASR = (0x1E << 1) |  // 大小 256MB (2^28)
                (0x0 << 0) |    // 使能
                (0x0 << 16) |   // TEX=0
                (0x0 << 18) |   // C=0
                (0x1 << 19) |   // B=1 (Non-cacheable)
                (0x0 << 24);    // 访问权限:全权限
    
    // 使能 MPU,使用默认内存映射作为后备
    MPU->CTRL = (0x1 << 0) | (0x1 << 2);  // ENABLE=1, PRIVDEFENA=1
    
    // 使能 D-Cache(若未开启)
    SCB_EnableDCache();
}

// 使用示例:初始化时调用一次
int main(void) {
    // ... 系统时钟、SDRAM 初始化 ...
    MPU_Config_NonCacheable();
    // 之后直接操作帧缓冲,无需任何手动维护
    uint16_t *fb = (uint16_t*)0xC0000000;
    fb[0] = 0xFFFF;  // 直接写,LTDC 立即可见
    while(1);
}

注意事项

  • 确保 SDRAM 地址和大小与 MPU Region 匹配,且 Region 大小必须是 2 的幂次。
  • 若 SDRAM 同时用于其他数据(如堆),需谨慎划分区域,避免影响性能。
  • 此策略下,CPU 访问帧缓冲无 Cache 加速,但相比关闭整个 D-Cache,其他代码和数据仍受益于 Cache,整体性能最优。

总结与选择建议

| 策略 | 性能 | 复杂度 | 适用场景 | |------|------|--------|----------| | 关闭 D-Cache | 低 | 极低 | 简单显示,性能不敏感 | | 手动 Clean/Invalidate | 中 | 中 | 需要精细控制,帧率要求高 | | MPU Non-cacheable | 高 | 中高 | 图形应用,推荐生产使用 |

对于大多数 STM32F4 图形项目,推荐使用策略三(MPU 配置 Non-cacheable),它平衡了性能与开发效率。若项目对帧缓冲访问频率极高且需要 CPU 加速,可考虑策略二,但务必注意缓存行对齐和操作时机。

希望本文能帮助你彻底解决 D-Cache 与 SDRAM 的一致性问题,让图形显示更稳定流畅。欢迎在评论区交流你的实践经验!