一、D-Cache 与 SDRAM 的相爱相杀

STM32F4 系列(如 STM32F429/439)内置了 4KB 的 D-Cache 和 I-Cache,用于加速对慢速存储器(如外部 SDRAM)的访问。Cache 作为 CPU 与 SDRAM 之间的高速缓存,遵循局部性原理,将常用数据复制到 Cache 中,从而减少总线等待时间。

然而,Cache 的引入带来了**数据一致性(Coherency)**问题:

  • CPU 写 SDRAM:数据可能只写入 Cache,尚未回写(Write-Back)到 SDRAM,此时若 DMA 外设直接读 SDRAM,会读到旧数据。
  • DMA 写 SDRAM:DMA 将数据从外设搬运到 SDRAM,但 Cache 中可能还保留着旧副本,CPU 读 Cache 时得到的是过期数据。

STM32F4 的 D-Cache 采用**写回(Write-Back)**策略,且不具备硬件一致性协议(如 MESI),因此软件必须显式维护一致性。

二、一致性问题的本质与 volatile 的局限

很多开发者习惯用 volatile 修饰共享变量,期望解决一致性问题。但 volatile 只告诉编译器“不要优化对该变量的访问”,它不生成任何内存屏障指令,也不影响 Cache 行为。例如:

volatile uint32_t *buf = (uint32_t *)0xC0000000; // SDRAM 地址
*buf = 0x12345678; // 可能只写 Cache

此时若 DMA 随后读取该地址,可能得到旧值。volatile 无法保证数据从 Cache 回写到 SDRAM。

真正解决一致性需要两步:

  1. Cache 维护操作:使用 CMSIS 提供的函数 SCB_CleanDCache()SCB_InvalidateDCache() 或按地址操作的 SCB_CleanDCache_by_Addr() 等。
  2. 内存屏障:确保 Cache 操作完成后再进行 DMA 或外设访问,防止指令重排。

三、实战配置:以 STM32F429 + SDRAM + DMA 为例

3.1 硬件环境

  • MCU:STM32F429ZI(Cortex-M4,带 D-Cache)
  • 外部 SDRAM:IS42S16400J(16MB,映射到 0xC0000000)
  • 外设:DMA2 从 ADC 搬运数据到 SDRAM

3.2 初始化步骤

1. 使能 D-Cache

main() 中,初始化 SDRAM 后使能 Cache:

void SystemInit_Cache(void) {
    SCB_EnableDCache();
    SCB_EnableICache();
}

2. 配置 SDRAM 为 Cacheable

STM32F4 的 MPU(内存保护单元)默认将外部 RAM 区域配置为 Write-Back 且 Cacheable。若需更改,需配置 MPU 寄存器。一般默认即可,但建议显式设置:

void MPU_Config(void) {
    MPU_Region_InitTypeDef MPU_InitStruct;
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = 0xC0000000;
    MPU_InitStruct.Size = MPU_REGION_SIZE_16MB;
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
    MPU_InitStruct.IsCacheable = MPU_ACCESS_CACHEABLE;
    MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
    MPU_InitStruct.Number = MPU_REGION_NUMBER0;
    MPU_Init(&MPU_InitStruct);
    MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

3. DMA 传输前的 Cache 清理

当 CPU 写数据到 SDRAM,然后启动 DMA 将数据发送到外设(如 DAC)时,需先 Clean Cache:

void DMA_SendFromSDRAM(uint32_t *src, uint32_t len) {
    // 确保 CPU 写的数据已回写到 SDRAM
    SCB_CleanDCache_by_Addr((uint32_t *)src, (int32_t)len);
    // 内存屏障,确保 Clean 完成
    __DSB();
    
    // 启动 DMA 传输(假设 DMA 已配置)
    DMA_Start(src, (uint32_t *)DAC_BASE, len);
}

4. DMA 接收后的 Cache 失效

当 DMA 从外设接收数据到 SDRAM,然后 CPU 读取时,需先 Invalidate Cache:

void DMA_ReceiveToSDRAM(uint32_t *dst, uint32_t len) {
    // 启动 DMA 传输
    DMA_Start((uint32_t *)ADC_BASE, dst, len);
    // 等待 DMA 完成(如中断标志)
    while (DMA_GetFlagStatus(DMA_FLAG_TC) == RESET);
    
    // 使 Cache 失效,丢弃旧副本
    SCB_InvalidateDCache_by_Addr((uint32_t *)dst, (int32_t)len);
    __DSB();
    
    // 现在 CPU 读取的是 SDRAM 中的新数据
}

3.3 完整代码示例(简化)

#include "stm32f4xx.h"

#define SDRAM_BASE 0xC0000000
#define BUFFER_SIZE 1024

uint32_t buf[BUFFER_SIZE] __attribute__((at(SDRAM_BASE))); // 放在 SDRAM

void SystemInit(void) {
    // 初始化时钟、SDRAM、MPU(略)
    MPU_Config();
    SCB_EnableDCache();
    SCB_EnableICache();
}

int main(void) {
    // 填充数据
    for (int i = 0; i < BUFFER_SIZE; i++) {
        buf[i] = i;
    }
    
    // 发送到外设前 Clean Cache
    SCB_CleanDCache_by_Addr((uint32_t *)buf, sizeof(buf));
    __DSB();
    
    // 启动 DMA 发送(略)
    
    // 接收数据后 Invalidate Cache
    // 假设 DMA 已填充 buf
    SCB_InvalidateDCache_by_Addr((uint32_t *)buf, sizeof(buf));
    __DSB();
    
    // 读取 buf 即为最新数据
    uint32_t val = buf[0];
    
    while (1);
}

四、内存屏障指令详解

Cortex-M4 支持两条屏障指令:

  • DMB(Data Memory Barrier):确保在 DMB 之前的所有内存访问(读/写)完成后,才执行后续的内存访问。适用于数据同步。
  • DSB(Data Synchronization Barrier):更强的屏障,确保在 DSB 之前的所有内存访问完成后,才执行后续的任何指令(包括取指)。适用于需要等待 Cache 操作完成或外设操作完成时。

在 CMSIS 中,可使用 __DMB()__DSB()。通常,在 Cache 维护操作后使用 __DSB() 以确保操作完成。

五、注意事项与调试技巧

  • 地址对齐SCB_CleanDCache_by_Addr()SCB_InvalidateDCache_by_Addr() 要求地址按 32 字节对齐(Cache line 大小)。若未对齐,需手动处理边界。
  • 性能权衡:频繁 Clean/Invalidate 会降低性能,建议采用 DMA 双缓冲或环形缓冲区,减少 Cache 操作次数。
  • 调试方法:若怀疑一致性问题,可暂时禁用 D-Cache(SCB_DisableDCache())观察现象是否消失。若消失,则确认是 Cache 问题。
  • 使用 MPU 配置非 Cacheable 区域:对于 DMA 频繁访问的缓冲区,可将其配置为 Write-Through 或 Non-Cacheable,避免手动维护。但会牺牲性能。
  • 注意编译器优化:即使使用 volatile,编译器也可能将多个访问合并,因此仍需显式屏障。

六、总结

STM32F4 的 D-Cache 是性能利器,但必须正确维护数据一致性。volatile 不能替代 Cache 操作和内存屏障。通过 SCB_CleanDCache / SCB_InvalidateDCache 配合 __DSB(),可以确保 CPU 与 DMA 之间的数据同步。在实际项目中,建议将 DMA 缓冲区设计为独立区域,并统一管理 Cache 操作,避免散落各处的维护代码。掌握这些技巧,能让你在嵌入式开发中少走弯路,快速定位疑难 Bug。