引言

在 STM32F4 系列(如 STM32F407、STM32F429)中,D-Cache 的引入大幅提升了 CPU 访问外部 SDRAM 或 Flash 的速度。然而,DMA 控制器直接访问物理内存,不经过 Cache,导致 CPU 与 DMA 看到的数据可能不一致。例如,CPU 写入数据到内存(仅更新 Cache),DMA 读取时可能拿到旧数据;反之,DMA 写入内存后,CPU 读取时可能命中过期的 Cache 行。这种问题在高速数据传输(如 ADC 采样、以太网收发)中尤为致命。

本文将针对 STM32F4 系列,提供三种经过验证的规避方案,并深入分析其原理与适用场景。

问题根源

STM32F4 的 D-Cache 采用写回(Write-back)策略,即 CPU 写操作仅更新 Cache,直到 Cache 行被替换时才写回内存。DMA 访问内存时绕过 Cache,因此:

  • CPU → DMA:CPU 写数据后,若 Cache 未写回,DMA 读到的是旧值。
  • DMA → CPU:DMA 写入内存后,若 CPU 读取时命中 Cache 中的旧数据,则读到错误值。

解决思路无非三种:主动同步 Cache、绕过 Cache、或避免共享内存。

方案一:Cache 清理与失效(软件控制)

原理

通过操作 Cortex-M4 内核提供的 SCB 寄存器,强制将 Cache 内容写回内存(Clean)或使 Cache 行失效(Invalidate)。在 DMA 操作前后调用相应函数,保证数据一致性。

配置步骤

  1. 启用 D-Cache(若未启用):
    SCB_EnableDCache();
    
  2. 在 DMA 发送前,调用 SCB_CleanDCache() 或按地址范围清理。
  3. 在 DMA 接收后,调用 SCB_InvalidateDCache() 或按地址范围失效。

代码示例

#include "stm32f4xx.h"

// 假设使用 DMA 传输缓冲区 uint32_t buffer[1024]
#define BUFFER_SIZE 1024
uint32_t buffer[BUFFER_SIZE] __attribute__((aligned(32))); // 32字节对齐,匹配Cache行

void DMA_Send(uint32_t *data, uint32_t size) {
    // 清理Cache,确保数据写回内存
    SCB_CleanDCache_by_Addr((uint32_t*)data, (int32_t)size * sizeof(uint32_t));
    
    // 配置并启动DMA传输(此处省略具体DMA配置)
    DMA_Start(data, size);
}

void DMA_Receive(uint32_t *data, uint32_t size) {
    // 启动DMA接收
    DMA_Start(data, size);
    // 等待DMA完成(轮询或中断)
    while(DMA_IsBusy());
    
    // 使Cache失效,强制从内存重新加载
    SCB_InvalidateDCache_by_Addr((uint32_t*)data, (int32_t)size * sizeof(uint32_t));
}

注意事项

  • 地址必须 32 字节对齐,大小最好为 32 的倍数,否则可能影响相邻数据。
  • 频繁调用 Clean/Invalidate 会降低性能,适合低频或小数据量场景。
  • 使用 SCB_CleanInvalidateDCache() 可同时完成清理和失效。

方案二:MPU 配置非缓存区域(硬件隔离)

原理

利用 Cortex-M4 的 MPU(内存保护单元),将 DMA 使用的内存区域配置为“非缓存(Non-cacheable)”属性。这样 CPU 访问该区域时直接读写内存,绕过 D-Cache,从根本上避免不一致。

配置步骤

  1. 定义 MPU 区域,设置基地址、大小、属性。
  2. 在系统初始化时,调用 MPU 配置函数。
  3. 确保 DMA 缓冲区位于该区域内。

代码示例

#include "stm32f4xx.h"

void MPU_Config(void) {
    // 禁用 MPU
    MPU->CTRL = 0;
    
    // 配置区域0:非缓存,可读写
    MPU->RNR = 0; // 区域编号
    MPU->RBAR = (uint32_t)buffer; // 基地址
    MPU->RASR = (0x03 << 1) |      // 全权限
                (0x01 << 16) |     // 指令访问禁止
                (0x00 << 18) |     // 非共享
                (0x01 << 19) |     // 非缓存(Normal, Non-cacheable)
                (0x00 << 21) |     // 写回,但非缓存已覆盖
                (0x01 << 24) |     // 使能区域
                (0x0F << 1);       // 区域大小:2^(size+1) 字节,这里需根据实际调整
    
    // 使能 MPU
    MPU->CTRL = 1;
    __DSB();
    __ISB();
}

// 初始化时调用
void SystemInit(void) {
    MPU_Config();
    // ... 其他初始化
}

注意事项

  • 区域大小必须为 2 的幂,且基地址对齐。
  • 非缓存区域会降低 CPU 访问速度,但 DMA 操作频繁时收益更大。
  • 需确保 MPU 配置在启用 D-Cache 之前或之后均可,但建议在系统早期完成。

方案三:双缓冲机制(软件设计)

原理

使用两个缓冲区交替进行 DMA 和 CPU 操作。CPU 写入缓冲区 A 时,DMA 从缓冲区 B 读取;完成后交换角色。由于同一时刻只有一个缓冲区被 CPU 或 DMA 使用,且每次切换时进行 Cache 同步,可避免冲突。

配置步骤

  1. 分配两个缓冲区,大小相同。
  2. 使用一个索引变量切换当前活动缓冲区。
  3. 在切换前,对即将使用的缓冲区执行 Clean/Invalidate。

代码示例

#define BUF_SIZE 1024
uint32_t buf[2][BUF_SIZE] __attribute__((aligned(32)));
volatile uint8_t active_buf = 0;

void ProcessData(void) {
    // 假设DMA接收数据到当前缓冲区
    uint32_t *current = buf[active_buf];
    
    // 启动DMA接收(省略配置)
    DMA_Start(current, BUF_SIZE);
    while(DMA_IsBusy());
    
    // 使当前缓冲区Cache失效,确保读取最新数据
    SCB_InvalidateDCache_by_Addr(current, BUF_SIZE * sizeof(uint32_t));
    
    // 处理数据...
    
    // 切换缓冲区
    active_buf ^= 1;
    // 清理新缓冲区(即将用于DMA),确保无残留数据
    SCB_CleanDCache_by_Addr(buf[active_buf], BUF_SIZE * sizeof(uint32_t));
}

注意事项

  • 缓冲区切换需原子操作,避免中断干扰。
  • 适用于连续数据流场景,如音频、网络收发。
  • 内存开销翻倍,但性能最优,无需频繁同步。

总结

三种方案各有优劣:

  • Cache 清理/失效:简单灵活,适合小数据量或低频操作,但需注意对齐和性能损耗。
  • MPU 非缓存区域:硬件隔离,彻底避免问题,但牺牲部分 CPU 访问速度,适合 DMA 频繁且数据量大的场景。
  • 双缓冲:软件设计巧妙,性能最佳,但内存占用翻倍,适合实时性要求高的流式传输。

开发者应根据实际需求(数据量、频率、内存资源)选择合适方案。在 STM32F4 项目中,务必在初始化阶段规划好内存布局和 Cache 策略,避免后期调试的噩梦。希望本文能帮助你解决 D-Cache 与 DMA 的“爱恨情仇”,让系统稳定高效运行。