STM32F4 D-Cache 与 DMA 数据一致性:三种典型规避方案深度解析

一、问题根源:D-Cache 与 DMA 的"信息孤岛"

STM32F4 系列(如 STM32F407、STM32F429)内置了 D-Cache(数据缓存),用于加速 CPU 对内存的访问。然而,DMA 控制器直接访问物理内存(SRAM 或外部存储器),不经过 Cache。这导致两个关键矛盾:

  • CPU 写、DMA 读:CPU 将数据写入 Cache(标记为 dirty),但尚未回写到物理内存。DMA 直接读取物理内存,得到的是旧数据。
  • DMA 写、CPU 读:DMA 将新数据写入物理内存,但 Cache 中可能仍保留旧副本(标记为 valid)。CPU 读取时命中 Cache,得到陈旧数据。

这种不一致性在高速数据采集、通信外设(如 UART、SPI、以太网)中尤为致命。

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

原理

通过显式操作 Cache,在 DMA 传输前后强制数据同步。

  • DMA 写内存前:执行 SCB_CleanDCache() 将 Cache 中脏数据回写到物理内存。
  • DMA 读内存后:执行 SCB_InvalidateDCache() 使 Cache 中对应行失效,强制 CPU 下次从物理内存重新加载。

配置步骤

  1. 启用 D-Cache(在 SystemInit() 后调用 SCB_EnableDCache())。
  2. 在 DMA 传输前,根据方向调用清理或失效函数。
  3. 注意:操作粒度建议按 Cache 行大小(32 字节)对齐,避免误伤其他数据。

代码示例

// 假设使用 DMA 接收 UART 数据到 buffer[64]
#define BUFFER_SIZE 64
uint8_t buffer[BUFFER_SIZE] __attribute__((aligned(32)));

void DMA_Receive_WithCache(void) {
    // 启动 DMA 接收(DMA 写入 buffer)
    HAL_UART_Receive_DMA(&huart1, buffer, BUFFER_SIZE);
    
    // 等待传输完成(中断或轮询)
    while (HAL_UART_GetState(&huart1) != HAL_UART_STATE_READY);
    
    // 使 Cache 失效,确保 CPU 读取最新数据
    SCB_InvalidateDCache_by_Addr((uint32_t*)buffer, BUFFER_SIZE);
    
    // 现在可以安全处理 buffer
    ProcessData(buffer);
}

void DMA_Transmit_WithCache(void) {
    // 准备数据
    PrepareData(buffer);
    
    // 清理 Cache,将数据回写到物理内存
    SCB_CleanDCache_by_Addr((uint32_t*)buffer, BUFFER_SIZE);
    
    // 启动 DMA 发送(DMA 从 buffer 读取)
    HAL_UART_Transmit_DMA(&huart1, buffer, BUFFER_SIZE);
}

注意事项

  • 使用 SCB_CleanDCache_by_AddrSCB_InvalidateDCache_by_Addr 时,地址和长度必须 32 字节对齐,否则可能无效或引发异常。
  • 频繁清理/失效会降低性能,适合低频或小数据量场景。

三、方案二:配置 MPU 区域为不可缓存(硬件隔离)

原理

通过 MPU(内存保护单元)将 DMA 使用的内存区域设置为 DeviceStrongly-ordered 属性,禁止 Cache 缓存该区域。这样 CPU 和 DMA 都直接访问物理内存,彻底消除一致性问题。

配置步骤

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

代码示例

void MPU_Config_NonCacheable(void) {
    MPU_Region_InitTypeDef MPU_InitStruct;
    
    // 禁用 MPU 进行配置
    HAL_MPU_Disable();
    
    // 配置区域:基地址 0x20000000(SRAM1),大小 32KB
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = 0x20000000;
    MPU_InitStruct.Size = MPU_REGION_SIZE_32KB;
    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);
}

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

注意事项

  • 不可缓存区域会降低 CPU 访问速度,仅对 DMA 缓冲区使用。
  • 确保区域大小覆盖所有 DMA 缓冲区,避免越界。
  • 若使用外部 SDRAM,同样可配置为不可缓存。

四、方案三:双缓冲机制(乒乓缓冲)

原理

使用两个缓冲区交替供 CPU 和 DMA 使用。当 DMA 正在写缓冲区 A 时,CPU 处理缓冲区 B;完成后交换。由于同一时刻只有一个设备访问某个缓冲区,且切换时通过 Cache 操作同步,从而避免冲突。

配置步骤

  1. 定义两个缓冲区(对齐 32 字节)。
  2. 使用 DMA 双缓冲模式(如 STM32F4 的 DMA 支持双缓冲)。
  3. 在 DMA 传输完成中断中,切换缓冲区并执行必要的 Cache 操作。

代码示例

#define BUFFER_SIZE 128
uint8_t buffer_A[BUFFER_SIZE] __attribute__((aligned(32)));
uint8_t buffer_B[BUFFER_SIZE] __attribute__((aligned(32)));
volatile uint8_t current_buffer = 0;

void DMA_Init_DoubleBuffer(void) {
    // 配置 DMA 双缓冲模式(以 SPI 接收为例)
    hdma_spi_rx.Init.Mode = DMA_CIRCULAR;
    hdma_spi_rx.Init.MemoryDataAlignment = DMA_MDATAALIGN_BYTE;
    // 设置双缓冲:内存地址分别为 buffer_A 和 buffer_B
    HAL_DMAEx_MultiBufferStart_IT(&hdma_spi_rx, (uint32_t)&hspi1->DR, (uint32_t)buffer_A, (uint32_t)buffer_B, BUFFER_SIZE);
}

void DMA_IRQHandler(void) {
    // 传输完成中断
    if (__HAL_DMA_GET_FLAG(&hdma_spi_rx, DMA_FLAG_TC)) {
        __HAL_DMA_CLEAR_FLAG(&hdma_spi_rx, DMA_FLAG_TC);
        
        // 当前 DMA 写的是哪个缓冲区?
        current_buffer = (__HAL_DMA_GET_MEMORY_ADDR(&hdma_spi_rx) == (uint32_t)buffer_A) ? 0 : 1;
        
        // 使失效当前缓冲区(CPU 即将读取)
        if (current_buffer == 0) {
            SCB_InvalidateDCache_by_Addr((uint32_t*)buffer_A, BUFFER_SIZE);
            ProcessData(buffer_A);
        } else {
            SCB_InvalidateDCache_by_Addr((uint32_t*)buffer_B, BUFFER_SIZE);
            ProcessData(buffer_B);
        }
    }
}

注意事项

  • 双缓冲适用于持续数据流,减少 Cache 操作频率。
  • 确保 DMA 配置为循环模式或正确管理传输完成标志。
  • 缓冲区切换逻辑需在中断中快速完成,避免数据覆盖。

五、总结与选型建议

| 方案 | 优点 | 缺点 | 适用场景 | |------|------|------|----------| | Cache 清理/失效 | 简单直接,无需硬件配置 | 性能损耗,需注意对齐 | 低频、小数据量 | | MPU 不可缓存 | 硬件隔离,无性能损耗 | 占用 MPU 区域,降低访问速度 | 高频、大数据量,且缓冲区固定 | | 双缓冲 | 高效,适合持续流 | 内存占用翻倍,逻辑复杂 | 实时数据采集、通信流 |

在实际工程中,可组合使用:例如用 MPU 将 DMA 缓冲区设为不可缓存,同时配合双缓冲减少 CPU 等待。务必在项目初期就考虑一致性设计,避免后期调试的深坑。

希望本文能帮你彻底解决 STM32F4 的 D-Cache 与 DMA 协同问题。若有疑问,欢迎在评论区交流!