引言

在 STM32F4 系列(如 STM32F429/439)上,D-Cache 能显著提升 CPU 访问外部 SDRAM 的性能,但同时也引入了数据一致性问题。当 CPU 写入数据到 SDRAM(经 Cache),而 DMA 外设直接读写 SDRAM 时,双方看到的数据可能不一致,导致图像撕裂、通信帧错误等诡异故障。本文面向有经验的嵌入式开发者,提供三种可落地的解决策略。

一、问题根源:Cache 与 SDRAM 的“双视图”

D-Cache 是 CPU 与 SDRAM 之间的高速缓存,以 32 字节(或 64 字节)为一行(Cache Line)。当 CPU 写 SDRAM 时,数据先写入 Cache,标记为脏(Dirty),仅在替换或显式清理时才写回 SDRAM。而 DMA 外设(如 LTDC、SDIO、DMA2D)直接访问 SDRAM 物理地址,看不到 Cache 中的脏数据。同理,DMA 写入 SDRAM 后,Cache 中可能残留旧数据,CPU 读到的仍是过期值。

关键点

  • Cache 与 SDRAM 的一致性需要软件主动维护。
  • 不同场景(CPU 写/DMA 读,DMA 写/CPU 读)需要不同的处理。

二、策略一:Cache 清理与无效化(软件控制)

这是最直接的方法,适用于数据量小、频率低的场景。核心操作:

  • Clean:将脏 Cache 行写回 SDRAM,确保 DMA 能读到最新数据。
  • Invalidate:使 Cache 行失效,强制 CPU 下次从 SDRAM 重新读取。

配置步骤

  1. 使能 D-Cache(在 SystemInit 后调用)。
  2. 在 CPU 写数据后、启动 DMA 前,执行 Clean。
  3. 在 DMA 完成后、CPU 读数据前,执行 Invalidate。

代码示例(使用 CMSIS 函数)

#include "stm32f4xx_hal.h"

// 使能 D-Cache
SCB_EnableDCache();

// 缓冲区(需按 32 字节对齐)
__ALIGN_BEGIN static uint8_t buf[1024] __ALIGN_END;

// CPU 写入数据
void cpu_write_data(void) {
    for (int i = 0; i < sizeof(buf); i++) buf[i] = i;
}

// 启动 DMA 前:清理 Cache,确保数据写回 SDRAM
void before_dma_start(void) {
    SCB_CleanDCache_by_Addr((uint32_t*)buf, (int32_t)sizeof(buf));
    // 或者全清理:SCB_CleanDCache();
}

// DMA 完成后:无效化 Cache,使 CPU 读取新数据
void after_dma_done(void) {
    SCB_InvalidateDCache_by_Addr((uint32_t*)buf, (int32_t)sizeof(buf));
}

注意事项

  • 地址和长度必须按 32 字节对齐,否则行为未定义。
  • 频繁 Clean/Invalidate 会降低性能,不适合大数据流。

三、策略二:MPU 配置非缓存区域(硬件隔离)

通过 MPU(Memory Protection Unit)将 SDRAM 的特定区域配置为“非缓存(Non-cacheable)”,使 CPU 访问该区域时直接读写 SDRAM,绕过 Cache。适用于对一致性要求高、但性能要求不苛刻的缓冲区(如 DMA 描述符、共享标志)。

配置步骤

  1. 确定 SDRAM 基地址(如 0xC0000000)。
  2. 划分一个子区域(如 0xC0000000 开始 64KB)为非缓存。
  3. 配置 MPU 区域属性:TEX=0, C=0, B=1(或 TEX=1, C=0, B=0,取决于实现)。

代码示例(使用 HAL 的 MPU 配置)

#include "stm32f4xx_hal.h"

void MPU_Config_NonCacheable(void) {
    MPU_Region_InitTypeDef MPU_InitStruct;
    
    __HAL_RCC_MPU_CLK_ENABLE();
    
    HAL_MPU_Disable();
    
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = 0xC0000000;  // SDRAM 基地址
    MPU_InitStruct.Size = MPU_REGION_SIZE_64KB;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
    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;  // 关键:非缓存
    MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE;
    
    HAL_MPU_ConfigRegion(&MPU_InitStruct);
    HAL_MPU_Enable(MPU_CONTROL_HRNDM_ENABLE);
}

// 在 main 函数中调用
int main(void) {
    HAL_Init();
    MPU_Config_NonCacheable();
    // ... 其他初始化
}

注意事项

  • 非缓存区域访问速度慢,但保证一致性。
  • 需确保 SDRAM 初始化完成后再配置 MPU。
  • 区域大小必须是 2 的幂,且对齐。

四、策略三:DMA 与 Cache 协同管理(双缓冲 + 手动同步)

对于高频大数据流(如摄像头图像),推荐使用双缓冲机制,结合 Cache 操作,避免每次传输都全量清理。核心思想:CPU 和 DMA 交替使用两个缓冲区,每个缓冲区在切换时只清理/无效化当前使用的部分。

实现步骤

  1. 分配两个缓冲区(各 32 字节对齐)。
  2. CPU 写缓冲区 A,DMA 从缓冲区 B 读取(或反之)。
  3. 每次切换时,对刚使用的缓冲区执行 Clean 或 Invalidate。

代码示例(伪代码框架)

#define BUFFER_SIZE 4096
__ALIGN_BEGIN static uint8_t buf_A[BUFFER_SIZE] __ALIGN_END;
__ALIGN_BEGIN static uint8_t buf_B[BUFFER_SIZE] __ALIGN_END;

volatile uint8_t current_buf = 0; // 0=A, 1=B

void process_cpu_data(void) {
    uint8_t* active = (current_buf == 0) ? buf_A : buf_B;
    // CPU 写入 active 缓冲区
    // ...
    // 清理该缓冲区,确保 DMA 可见
    SCB_CleanDCache_by_Addr((uint32_t*)active, BUFFER_SIZE);
    // 启动 DMA 从 active 读取(或写入)
    start_dma(active, BUFFER_SIZE);
    // 切换缓冲区
    current_buf ^= 1;
}

void dma_complete_callback(void) {
    uint8_t* inactive = (current_buf == 0) ? buf_B : buf_A;
    // 无效化非活动缓冲区,准备 CPU 读取
    SCB_InvalidateDCache_by_Addr((uint32_t*)inactive, BUFFER_SIZE);
    // 通知应用处理 inactive 数据
}

注意事项

  • 双缓冲避免 CPU 和 DMA 同时访问同一缓冲区,减少冲突。
  • 清理和无效化操作要精确控制,避免过度开销。
  • 确保 DMA 中断优先级高于 CPU 的普通任务,及时切换。

五、总结与选型建议

| 策略 | 适用场景 | 性能影响 | 复杂度 | |------|----------|----------|--------| | 清理/无效化 | 小数据量、低频 | 中 | 低 | | MPU 非缓存 | 共享标志、描述符 | 低 | 中 | | 双缓冲+同步 | 大数据流、高频 | 高 | 高 |

实战建议

  • 优先使用 MPU 配置非缓存区域来保护关键控制结构。
  • 对于图像或音频数据流,采用双缓冲并配合精确的 Cache 操作。
  • 在调试时,可临时禁用 D-Cache 来验证问题是否由一致性引起。

数据一致性是 STM32F4 高性能应用的必修课,掌握这三种策略,能让你在复杂系统中游刃有余。希望本文能助你避开这些“隐形炸弹”。