一、为什么需要关注 D-Cache 与 SDRAM 的一致性?

STM32F4 系列(如 STM32F407/429)内置 Cortex-M4F 内核,带有可选的 D-Cache(数据缓存)和 I-Cache(指令缓存)。当外部 SDRAM 作为大容量数据缓冲区(如 LCD 显存、音频缓冲、网络数据包)时,CPU 通过 D-Cache 访问 SDRAM 可大幅减少访问延迟。但 D-Cache 与 SDRAM 之间并非实时同步,若直接读写,可能产生以下问题:

  • 脏行(Dirty Line)未回写:CPU 写入 SDRAM 的数据暂存在 Cache 中,若未及时写回,外部设备(如 DMA、LCD 控制器)读取 SDRAM 时得到旧数据。
  • 伪共享(False Sharing):多个外设或 CPU 核心访问同一 Cache Line 的不同部分,导致不必要的回写和失效。
  • DMA 与 CPU 竞争:DMA 直接访问 SDRAM,绕过 Cache,若 CPU 先写后 DMA 读,或 DMA 写后 CPU 读,都可能读到过期数据。

因此,理解并正确处理 D-Cache 一致性是 STM32F4 高性能应用的关键。

二、原理剖析:Cortex-M4 的 D-Cache 工作方式

Cortex-M4 的 D-Cache 采用写回(Write-back)策略,即 CPU 写操作只更新 Cache,不立即写回内存,直到该行被替换或显式清理。读操作则按写分配(Write-allocate),先加载到 Cache 再供 CPU 使用。

Cache 与内存交互的最小单位是 Cache Line,STM32F4 的 D-Cache Line 大小通常为 32 字节(可通过 SCB->DCCIMVAC 等寄存器操作)。当 CPU 访问 SDRAM 地址时,硬件会将整个 Line 加载到 Cache;当 CPU 写数据时,只修改 Cache 中的对应字节,并标记该行为脏。

关键寄存器与操作

  • SCB_EnableDCache():使能 D-Cache。
  • SCB_CleanDCache():将所有脏行写回内存。
  • SCB_InvalidateDCache():使所有 Cache 行无效,下次访问重新加载。
  • SCB_CleanInvalidateDCache():先回写再失效。
  • 也可按地址操作:SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize) 等。

一致性问题的本质:CPU 与 DMA 等外设对同一内存区域的访问路径不同(CPU 走 Cache,DMA 直接走总线),导致双方看到的数据视图不一致。

三、双缓冲机制:解决一致性的经典方案

双缓冲(Double Buffering)通过两个缓冲区交替使用,避免 CPU 与 DMA 同时操作同一块内存,从而简化一致性管理。典型场景:LCD 显示或音频播放。

工作流程

  1. 分配两个 SDRAM 缓冲区 Buffer A 和 Buffer B。
  2. CPU 向 Buffer A 写入新数据(如帧数据),写完后 Clean 相关 Cache 行,确保数据落回 SDRAM。
  3. 启动 DMA 从 Buffer A 传输到外设(如 LCD),同时 CPU 开始写 Buffer B。
  4. DMA 传输完成后,交换角色:CPU 写 Buffer A,DMA 读 Buffer B。

优点

  • CPU 和 DMA 不会同时访问同一缓冲区,避免竞争。
  • 只需在切换时做一次 Cache 清理,开销小。
  • 提高吞吐量,CPU 无需等待 DMA 完成。

四、实战:STM32F429 + SDRAM 双缓冲 LCD 显示

4.1 硬件与初始化

使用 STM32F429 Discovery 板,SDRAM 为 IS42S16400J(1M x 16bit),LCD 为 RGB 接口。初始化步骤:

  1. 配置 SDRAM 控制器(FMC),使能 SDRAM 时钟。
  2. 使能 D-Cache(注意:I-Cache 可独立使能)。
  3. 分配两个缓冲区(例如 320x240 像素,16 位色,每个缓冲区 150KB)。

代码示例(初始化)

#include "stm32f4xx.h"
#include "stm32f429i_discovery_sdram.h"

#define BUFFER_SIZE  (320*240*2)  // 16bpp
static uint8_t bufferA[BUFFER_SIZE] __attribute__((section(".sdram")));
static uint8_t bufferB[BUFFER_SIZE] __attribute__((section(".sdram")));

void SystemInit_Cache(void) {
    // 使能 D-Cache 和 I-Cache
    SCB_EnableDCache();
    SCB_EnableICache();
}

void SDRAM_Init(void) {
    // 使用 ST 官方库初始化 SDRAM
    SDRAM_Init();
    // 确保 SDRAM 可用
}

int main(void) {
    HAL_Init();
    SystemClock_Config();
    SDRAM_Init();
    SystemInit_Cache();
    // 其他外设初始化...
}

4.2 双缓冲写入与切换

在渲染循环中,CPU 写入当前缓冲区,然后 Clean 该缓冲区的 Cache 行,再启动 DMA 传输。

代码示例(写入与切换)

volatile uint8_t *activeBuf = bufferA;
volatile uint8_t *dmaBuf = bufferB;

void RenderFrame(void) {
    // 假设 activeBuf 是当前 CPU 写入的缓冲区
    // 填充图像数据到 activeBuf
    for (int i = 0; i < BUFFER_SIZE; i++) {
        activeBuf[i] = (uint8_t)(i & 0xFF);  // 示例数据
    }
    
    // 关键:将 activeBuf 对应的 Cache 行写回 SDRAM
    SCB_CleanDCache_by_Addr((uint32_t*)activeBuf, BUFFER_SIZE);
    
    // 启动 DMA 传输(假设 DMA 从 activeBuf 到 LCD)
    DMA_Start((uint32_t)activeBuf, (uint32_t)LCD_FB_ADDR, BUFFER_SIZE);
    
    // 切换缓冲区
    uint8_t *tmp = (uint8_t*)activeBuf;
    activeBuf = dmaBuf;
    dmaBuf = tmp;
}

void DMA_TransferComplete_Callback(void) {
    // DMA 完成,可准备下一帧
    // 注意:此时 dmaBuf 已被 DMA 读取,CPU 可以安全写入 activeBuf
}

注意事项

  • SCB_CleanDCache_by_Addr 的地址必须 32 字节对齐,大小最好为 32 的倍数,否则会多清理或遗漏。
  • 若 DMA 写 SDRAM(如摄像头采集),则需在 CPU 读取前调用 SCB_InvalidateDCache_by_Addr,使 Cache 行失效,强制从 SDRAM 重新加载。

4.3 完整代码整合

以下是一个简化的完整示例,演示双缓冲与 Cache 操作:

#include "stm32f4xx_hal.h"

// 假设已初始化 SDRAM 和 DMA

#define FRAME_SIZE  (320*240*2)
static uint8_t buf[2][FRAME_SIZE] __attribute__((section(".sdram")));
static volatile uint8_t curBuf = 0;

void FillFrame(uint8_t *buf, uint16_t color) {
    for (int i = 0; i < FRAME_SIZE; i+=2) {
        buf[i] = color & 0xFF;
        buf[i+1] = (color >> 8) & 0xFF;
    }
}

void DisplayFrame(void) {
    uint8_t *writeBuf = buf[curBuf];
    uint8_t *readBuf = buf[curBuf ^ 1];
    
    // 填充数据(模拟渲染)
    FillFrame(writeBuf, 0x1234);
    
    // 清理 writeBuf 的 Cache,确保数据到 SDRAM
    SCB_CleanDCache_by_Addr((uint32_t*)writeBuf, FRAME_SIZE);
    
    // 启动 DMA 从 readBuf 传输到 LCD(假设 readBuf 是上一帧已准备好的)
    HAL_DMA_Start(&hdma, (uint32_t)readBuf, (uint32_t)LCD_FB_ADDR, FRAME_SIZE);
    
    // 切换缓冲区
    curBuf ^= 1;
}

void DMA_IRQHandler(void) {
    // 处理 DMA 完成中断,可在此处调用 DisplayFrame 准备下一帧
}

五、常见陷阱与注意事项

  • Cache Line 对齐:缓冲区地址和大小最好对齐到 32 字节,否则 CleanInvalidate 可能影响相邻数据。
  • DMA 方向:DMA 读 SDRAM(外设到内存)时,CPU 在 DMA 完成后需 Invalidate 对应区域;DMA 写 SDRAM(内存到外设)时,CPU 在启动 DMA 前需 Clean。
  • 中断与优先级:在中断中调用 Cache 操作可能耗时,注意中断延迟。
  • 多核(如 H7):STM32F4 是单核,但若使用 DMA 和 CPU 并发,仍需一致化处理。
  • 调试技巧:使用 SCB_GetDCCSR 检查 Cache 状态,或临时禁用 D-Cache 对比现象。

六、总结

D-Cache 与 SDRAM 的数据一致性是 STM32F4 高性能应用绕不开的课题。通过理解 Cache 的写回机制,并采用双缓冲策略,可以高效且安全地管理数据流。本文提供的原理和代码可直接应用于 LCD、音频、网络等场景。记住核心原则:谁先写谁 Clean,谁后读谁 Invalidate。掌握这些,你的嵌入式系统将既快又稳。