STM32F4 使用 SDRAM 作为显存时的 Cache 一致性维护策略

1. 问题背景

在嵌入式图形系统中,SDRAM 常被用作 LCD 的显存(FrameBuffer),以支持高分辨率显示。STM32F4 系列(如 STM32F429/439)内置了 D-Cache(数据缓存)和 I-Cache(指令缓存),当 CPU 写入显存时,数据可能只停留在 Cache 中,而 DMA(如 LTDC 控制器)读取 SDRAM 时却直接访问物理内存,导致两者数据不一致,表现为屏幕显示异常(如花屏、撕裂)。

2. 原理剖析

2.1 Cache 的工作机制

STM32F4 的 D-Cache 采用写回(Write-back)策略:CPU 写数据时,先写入 Cache,并标记为脏(Dirty),只有在缓存行被替换或显式 Clean 操作时,才将数据写回 SDRAM。而 LTDC(LCD 控制器)通过 DMA 直接读取 SDRAM,不经过 Cache。因此,若 CPU 更新显存后未及时写回,LTDC 读取的仍是旧数据。

2.2 一致性问题的触发场景

  • CPU 绘制图形到显存 → LTDC 读取显存刷新屏幕
  • DMA 从外部存储器拷贝图像到显存 → CPU 读取显存进行图像处理

这两种场景都会因 Cache 与内存数据不同步而产生错误。

3. 维护策略

策略一:关闭 D-Cache(简单粗暴)

直接禁用 D-Cache,所有内存访问都直接走 SDRAM,彻底避免一致性问题。但会显著降低 CPU 访问内存的性能,尤其是图形绘制时。

配置方法:

SCB_DisableDCache();

适用场景: 对性能要求不高,或调试阶段快速验证。

策略二:手动 Clean + Invalidate(推荐)

在关键操作前后,手动维护 Cache。

  • Clean:将脏缓存行写回 SDRAM,确保 DMA 读取到最新数据。
  • Invalidate:使缓存行失效,下次 CPU 读取时从 SDRAM 重新加载,避免读到旧数据。

配置步骤:

  1. 开启 D-Cache:SCB_EnableDCache()
  2. 在 CPU 写完显存后,调用 SCB_CleanDCache_by_Addr() 写回数据。
  3. 在 CPU 读取 DMA 写入的数据前,调用 SCB_InvalidateDCache_by_Addr() 使缓存失效。

代码示例:

// 显存地址和大小(需按 32 字节对齐)
#define FRAME_BUFFER_ADDR  0xC0000000
#define FRAME_BUFFER_SIZE  (800*480*2)  // RGB565 格式

// CPU 绘制完成后,写回 Cache
void FrameBuffer_Flush(void) {
    SCB_CleanDCache_by_Addr((uint32_t*)FRAME_BUFFER_ADDR, FRAME_BUFFER_SIZE);
}

// 在 CPU 读取 DMA 写入的数据前,使缓存失效
void FrameBuffer_Invalidate(void) {
    SCB_InvalidateDCache_by_Addr((uint32_t*)FRAME_BUFFER_ADDR, FRAME_BUFFER_SIZE);
}

注意: 地址和大小必须 32 字节对齐,否则操作无效。

策略三:双缓冲 + 切换(高级)

使用两个显存区域,一个用于 CPU 绘制,另一个用于 LTDC 显示。当 CPU 完成绘制后,切换 LTDC 的显存地址,并 Clean 新显存区域。这样避免了在绘制过程中 LTDC 读取到不完整数据,同时减少了 Cache 操作频率。

配置步骤:

  1. 定义两个缓冲区:frame_buf[2],地址对齐到 32 字节。
  2. LTDC 当前显示 frame_buf[active]
  3. CPU 绘制到 frame_buf[1-active],绘制完成后 Clean 该区域。
  4. 切换 LTDC 显存地址到新缓冲区,并设置 active 翻转。

代码示例:

static uint8_t frame_buf[2][FRAME_BUFFER_SIZE] __attribute__((aligned(32)));
static volatile uint8_t active_buf = 0;

void LTDC_SwitchBuffer(uint32_t addr) {
    LTDC->Layer1->WHPCR &= ~(LTDC_LxWHPCR_WHSTPOS | LTDC_LxWHPCR_WHSPPOS);
    LTDC->Layer1->WHPCR |= ((addr >> 16) & 0xFFF) | ((addr & 0xFFFF) << 16);
    // 实际需根据 LTDC 寄存器配置,此处简化
}

void Draw_Scene(void) {
    uint8_t *draw_buf = frame_buf[1-active_buf];
    // 绘制内容到 draw_buf
    SCB_CleanDCache_by_Addr((uint32_t*)draw_buf, FRAME_BUFFER_SIZE);
    LTDC_SwitchBuffer((uint32_t)draw_buf);
    active_buf ^= 1;
}

4. 完整示例:结合 LTDC 和 SDRAM

以下代码演示了在 STM32F429 上初始化 SDRAM、开启 Cache、配置 LTDC,并实现显存刷新的完整流程。

#include "stm32f4xx.h"

#define SDRAM_BASE  0xC0000000
#define LCD_WIDTH   800
#define LCD_HEIGHT  480
#define PIXEL_SIZE  2  // RGB565
#define FB_SIZE     (LCD_WIDTH * LCD_HEIGHT * PIXEL_SIZE)

// SDRAM 初始化(略,需根据具体硬件配置)
void SDRAM_Init(void) {
    // 配置 FMC 控制器,使能 SDRAM 银行
}

// LTDC 初始化(略)
void LTDC_Init(void) {
    // 配置时序、层参数,设置显存地址为 SDRAM_BASE
}

int main(void) {
    // 系统时钟、GPIO、FMC、LTDC 初始化
    SDRAM_Init();
    LTDC_Init();

    // 开启 D-Cache(注意:必须在 SDRAM 初始化之后)
    SCB_EnableDCache();

    uint16_t *fb = (uint16_t*)SDRAM_BASE;

    while (1) {
        // CPU 绘制:填充红色
        for (int i = 0; i < FB_SIZE/2; i++) {
            fb[i] = 0xF800;
        }
        // 写回 Cache,确保 LTDC 看到新数据
        SCB_CleanDCache_by_Addr((uint32_t*)fb, FB_SIZE);
        HAL_Delay(100);

        // 填充蓝色
        for (int i = 0; i < FB_SIZE/2; i++) {
            fb[i] = 0x001F;
        }
        SCB_CleanDCache_by_Addr((uint32_t*)fb, FB_SIZE);
        HAL_Delay(100);
    }
}

5. 注意事项

  • 对齐要求SCB_CleanDCache_by_AddrSCB_InvalidateDCache_by_Addr 的地址和长度必须 32 字节对齐,否则操作无效。建议显存缓冲区使用 __attribute__((aligned(32)))
  • 性能权衡:频繁 Clean 整个显存会消耗 CPU 时间。如果只修改部分区域,可只 Clean 修改的行,但需注意行大小(通常 32 字节)。
  • DMA 方向:如果 DMA 从外设写入显存,CPU 读取前必须 Invalidate,否则可能读到 Cache 中的旧数据。
  • 中断安全:在中断中执行 Cache 操作时,注意优先级和原子性,避免与主循环冲突。
  • 调试技巧:出现花屏时,可先暂时关闭 D-Cache 验证是否一致性导致,再优化策略。

6. 总结

STM32F4 的 Cache 一致性是使用 SDRAM 显存时必须面对的问题。通过关闭 Cache、手动 Clean/Invalidate 或双缓冲切换,可以有效解决。推荐使用策略二(手动维护)作为默认方案,兼顾性能与可靠性。对于高性能图形应用,结合双缓冲和局部 Clean 能获得最佳效果。理解 Cache 的工作原理,并根据实际场景选择合适策略,是嵌入式开发者必备的技能。