引言

在 STM32F4 系列(如 STM32F407、F429 等)上,D-Cache(数据缓存)能显著提升 CPU 访问内部 SRAM 的速度,但当外部 SDRAM 介入时,D-Cache 与 DMA 外设之间的数据一致性便成为隐患。典型场景:LCD 显存位于 SDRAM,CPU 写入后 DMA 读取,若未正确处理缓存,屏幕会出现随机花屏;或以太网 DMA 接收数据时,CPU 读到的是陈旧缓存。本文从原理出发,提供三种实用排查手段,助你快速定位问题。

D-Cache 与 SDRAM 一致性问题的根源

STM32F4 的 D-Cache 是 4 路组关联、总大小 4KB(部分型号 8KB),以 32 字节为一行。当 CPU 访问 SDRAM 时,数据会被缓存到 D-Cache 中。若 DMA 外设直接访问 SDRAM(绕过 CPU),则可能产生两种冲突:

  • 写一致性:CPU 修改缓存行但未写回 SDRAM,DMA 读取到旧数据。
  • 读一致性:DMA 写入 SDRAM 新数据,但 CPU 缓存中仍是旧副本。

因此,在 CPU 与 DMA 交替访问同一 SDRAM 区域时,必须显式维护缓存一致性。

三种排查手段

手段一:使用 CMSIS 函数进行缓存清理与失效

原理

CMSIS 提供 SCB_CleanDCache()SCB_InvalidateDCache() 及按地址操作的函数。清理(Clean)将脏缓存行写回 SDRAM;失效(Invalidate)丢弃缓存行,强制下次从 SDRAM 读取。

配置步骤

  1. 确保在 system_stm32f4xx.c 中启用了 D-Cache(SCB_EnableDCache())。
  2. 在 DMA 写入 SDRAM 前,调用 SCB_CleanDCache_by_Addr() 清理相关区域。
  3. 在 DMA 写入完成后,调用 SCB_InvalidateDCache_by_Addr() 使缓存失效。

代码示例

// 假设 SDRAM 缓冲区地址为 0xC0000000,长度 1024 字节
#define BUF_ADDR 0xC0000000
#define BUF_SIZE 1024

// CPU 写数据到 SDRAM,然后 DMA 读取
void CPU_Write_Then_DMA_Read(void) {
    uint32_t *buf = (uint32_t *)BUF_ADDR;
    for (int i = 0; i < BUF_SIZE/4; i++) {
        buf[i] = i; // 写入数据
    }
    // 清理缓存,确保数据写回 SDRAM
    SCB_CleanDCache_by_Addr((uint32_t *)BUF_ADDR, BUF_SIZE);
    
    // 启动 DMA 读取(此处省略 DMA 配置)
    DMA_Start_Read(BUF_ADDR, BUF_SIZE);
}

// DMA 写入 SDRAM 后,CPU 读取
void DMA_Write_Then_CPU_Read(void) {
    // 启动 DMA 写入(此处省略)
    DMA_Start_Write(BUF_ADDR, BUF_SIZE);
    // 等待 DMA 完成
    while(DMA_IsBusy());
    // 使缓存失效,强制从 SDRAM 读取最新数据
    SCB_InvalidateDCache_by_Addr((uint32_t *)BUF_ADDR, BUF_SIZE);
    
    uint32_t *buf = (uint32_t *)BUF_ADDR;
    for (int i = 0; i < BUF_SIZE/4; i++) {
        printf("%d\n", buf[i]); // 读取数据
    }
}

注意事项

  • 地址必须 32 字节对齐,长度最好为 32 的倍数,否则可能未完全覆盖。
  • 频繁清理/失效会降低性能,应仅在必要区域使用。

手段二:配置 MPU 将 SDRAM 区域设为非缓存

原理

通过 MPU(内存保护单元)将 SDRAM 区域配置为 Normal, Non-Cacheable,则 CPU 访问该区域时直接读写 SDRAM,不经过 D-Cache,从而彻底避免一致性问题。适合对性能要求不高或访问频率低的区域。

配置步骤

  1. 初始化 MPU,设置区域基地址、大小和属性。
  2. 属性中 TEX=0, C=0, B=0 表示 Non-Cacheable。
  3. 使能 MPU。

代码示例

void MPU_Config_SDRAM_NonCacheable(void) {
    MPU_Region_InitTypeDef MPU_InitStruct;
    
    // 禁用 MPU 进行配置
    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);
    
    // 使能 MPU
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

注意事项

  • MPU 区域大小必须是 2 的幂,且最小 32 字节。
  • 非缓存区域访问速度较慢,若 SDRAM 带宽要求高,需权衡。
  • 确保 MPU 配置在系统早期完成,避免运行时切换。

手段三:使用 DMA 缓冲区的对齐与双缓冲策略

原理

将 DMA 缓冲区设计为 32 字节对齐,并采用双缓冲(Double Buffer)机制。CPU 处理一个缓冲区时,DMA 操作另一个,并在切换时进行缓存维护。这能减少缓存操作频率,并避免同一时刻的冲突。

配置步骤

  1. 定义两个缓冲区,使用 __attribute__((aligned(32))) 对齐。
  2. 在 DMA 完成中断中切换缓冲区,并调用缓存维护函数。
  3. 确保 DMA 配置为循环或双缓冲模式(如 STM32F4 的 DMA2D 或通用 DMA)。

代码示例

#define BUF_SIZE 1024
__attribute__((aligned(32))) uint8_t buf1[BUF_SIZE];
__attribute__((aligned(32))) uint8_t buf2[BUF_SIZE];

volatile uint8_t active_buf = 0; // 0 表示 buf1,1 表示 buf2

void DMA_TransferComplete_Callback(void) {
    // 切换缓冲区
    if (active_buf == 0) {
        active_buf = 1;
        // 使旧缓冲区失效,准备 CPU 读取
        SCB_InvalidateDCache_by_Addr((uint32_t *)buf1, BUF_SIZE);
        // 配置 DMA 使用 buf2(此处省略)
        DMA_SetBuffer(buf2, BUF_SIZE);
    } else {
        active_buf = 0;
        SCB_InvalidateDCache_by_Addr((uint32_t *)buf2, BUF_SIZE);
        DMA_SetBuffer(buf1, BUF_SIZE);
    }
}

void CPU_Process_Data(void) {
    uint8_t *data = (active_buf == 0) ? buf1 : buf2;
    // 处理数据(注意:处理前确保对应缓冲区已失效)
    Process(data, BUF_SIZE);
}

注意事项

  • 缓冲区必须 32 字节对齐,否则缓存操作可能出错。
  • 双缓冲可避免 CPU 和 DMA 同时访问同一缓冲区,但需在切换时正确维护缓存。
  • 若使用 DMA2D(图形加速),需注意其与 D-Cache 的交互,通常需要清理缓存。

总结

D-Cache 与 SDRAM 的一致性问题是 STM32F4 开发中的常见陷阱。本文介绍了三种排查手段:

  • 缓存清理/失效:最直接,但需注意地址对齐和性能开销。
  • MPU 非缓存区域:彻底规避,但牺牲速度。
  • 双缓冲策略:工程上常用,兼顾性能与正确性。

实际开发中,建议先使用手段二快速验证问题是否由缓存引起,再根据性能需求选择手段一或三。掌握这些技巧,能让你在嵌入式开发中少走弯路。