引言

在 STM32F4 系列(如 STM32F429、F407)中,D-Cache(数据缓存)是提升 CPU 访问外部 SDRAM 或低速外设的关键硬件。然而,D-Cache 的引入也带来了一个经典难题:DMA 与 CPU 之间的数据一致性。当 CPU 和 DMA 同时访问同一内存区域时,若未正确处理缓存,轻则数据错误,重则系统崩溃。本文面向有嵌入式开发基础的工程师,深入剖析三种典型场景,并提供基于 CMSIS 的解决方案。

背景知识:D-Cache 与 DMA 的冲突根源

D-Cache 是 CPU 与主存之间的高速缓存,默认采用 写回(Write-back) 策略:CPU 写数据时,数据先写入缓存,标记为脏(Dirty),之后才被回写到主存。DMA 则直接访问主存,不经过缓存。因此,当 CPU 修改数据后,DMA 可能从主存读到旧数据;反之,DMA 写入新数据后,CPU 可能从缓存读到旧数据。

STM32F4 的 Cortex-M4 内核提供了两个关键操作:

  • Clean:将脏缓存行回写到主存。
  • Invalidate:使缓存行失效,下次访问时从主存重新加载。

CMSIS 提供了 SCB_CleanDCache()SCB_InvalidateDCache() 等函数,以及更精细的按地址操作函数 SCB_CleanDCache_by_Addr()SCB_InvalidateDCache_by_Addr()

场景一:CPU 写数据,DMA 读数据

问题描述

CPU 在内存中准备好一组数据(如 ADC 采样值),然后启动 DMA 将其传输到外设(如 DAC 或 UART)。由于 D-Cache 的写回策略,数据可能仍停留在缓存中,DMA 从主存读取时得到的是旧数据。

解决方案:Clean 操作

在启动 DMA 传输前,必须对缓冲区执行 Clean 操作,确保脏数据回写到主存。

代码示例

// 缓冲区定义(需对齐到 32 字节,即缓存行大小)
__attribute__((aligned(32))) uint32_t tx_buffer[256];

void prepare_data_and_start_dma(void)
{
    // CPU 写入数据
    for (int i = 0; i < 256; i++) {
        tx_buffer[i] = i * 2;
    }

    // 关键:Clean D-Cache,将脏数据回写到主存
    SCB_CleanDCache_by_Addr((uint32_t*)tx_buffer, sizeof(tx_buffer));

    // 启动 DMA 传输(假设 DMA 已配置好)
    DMA_Start_Transfer(tx_buffer, 256);
}

注意事项

  • 缓冲区地址和大小必须对齐到 32 字节(缓存行大小),否则 Clean 操作可能不完整。
  • 若缓冲区较大,可考虑使用 SCB_CleanDCache() 全清,但性能较低,建议按地址操作。

场景二:DMA 写数据,CPU 读数据

问题描述

DMA 从外设(如 ADC、UART)接收数据到内存缓冲区,然后 CPU 读取这些数据进行处理。由于 D-Cache 可能包含旧数据,CPU 读到的可能是缓存中的过期内容。

解决方案:Invalidate 操作

在 CPU 读取数据前,必须对缓冲区执行 Invalidate 操作,使缓存行失效,强制从主存重新加载。

代码示例

__attribute__((aligned(32))) uint32_t rx_buffer[256];

void DMA_Transfer_Complete_Callback(void)
{
    // 关键:Invalidate D-Cache,使缓存行失效,从主存重新加载
    SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buffer, sizeof(rx_buffer));

    // 现在可以安全地读取 rx_buffer 中的数据
    for (int i = 0; i < 256; i++) {
        process_sample(rx_buffer[i]);
    }
}

注意事项

  • Invalidate 操作会丢弃缓存中的脏数据,因此仅当 DMA 完全覆盖缓冲区时才安全。若 DMA 只写入部分区域,需谨慎处理。
  • 若 DMA 在后台持续传输,建议使用双缓冲机制(见场景三)。

场景三:共享缓冲区,CPU 和 DMA 交替访问

问题描述

在实时系统中,CPU 和 DMA 可能频繁交替访问同一缓冲区(如音频流、图像处理)。若每次切换都执行全量 Clean/Invalidate,性能开销巨大,且容易出错。

解决方案:双缓冲 + 缓存维护策略

采用 双缓冲(Ping-Pong Buffer)机制,将缓冲区分为两个独立区域。CPU 处理一个区域时,DMA 填充另一个区域,通过切换角色避免冲突。同时,在每个区域切换时执行必要的缓存操作。

代码示例

#define BUFFER_SIZE 1024
__attribute__((aligned(32))) uint32_t buffer[2][BUFFER_SIZE];
volatile uint8_t current_buf = 0;

void DMA_Transfer_Complete_Callback(void)
{
    uint8_t done_buf = current_buf;
    // 切换缓冲区
    current_buf ^= 1;

    // 对已完成 DMA 写入的缓冲区执行 Invalidate
    SCB_InvalidateDCache_by_Addr((uint32_t*)buffer[done_buf], sizeof(buffer[done_buf]));

    // 处理数据(此时 CPU 读取的是最新数据)
    process_data(buffer[done_buf], BUFFER_SIZE);

    // 处理完毕后,若 CPU 修改了缓冲区,需 Clean 后再交给 DMA
    SCB_CleanDCache_by_Addr((uint32_t*)buffer[done_buf], sizeof(buffer[done_buf]));
}

void DMA_Init(void)
{
    // 配置 DMA 使用 buffer[current_buf] 作为目标地址
    DMA_SetTarget(buffer[current_buf]);
    DMA_Start();
}

注意事项

  • 双缓冲可避免 CPU 和 DMA 同时访问同一内存区域,但缓存操作仍需按需执行。
  • 若缓冲区较大,可考虑使用 MPU(内存保护单元)将区域配置为 非缓存(Non-cacheable),彻底避免一致性问题,但会牺牲 CPU 访问性能。

进阶技巧:使用 MPU 配置内存属性

对于实时性要求高、且频繁被 DMA 访问的缓冲区,可配置 MPU 将该区域设为 非缓存写通(Write-through)。这样 CPU 写入时直接更新主存,DMA 读取时始终得到最新数据,无需手动 Clean/Invalidate。

void MPU_Config_NonCacheable(void)
{
    MPU_Region_InitTypeDef MPU_InitStruct;
    // 使能 MPU
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);

    // 配置区域:例如 0x20000000,大小 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;
    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    // 使能 MPU
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

总结与最佳实践

  • 场景一:CPU 写 → DMA 读,必须执行 Clean
  • 场景二:DMA 写 → CPU 读,必须执行 Invalidate
  • 场景三:交替访问,使用 双缓冲 + 按需 Clean/Invalidate,或配置 MPU 为非缓存。

最佳实践

  • 缓冲区对齐到 32 字节,并使用 __attribute__((aligned(32)))
  • 优先使用 SCB_CleanDCache_by_AddrSCB_InvalidateDCache_by_Addr 进行精细操作,避免全缓存操作带来的性能损失。
  • 在 DMA 中断回调中执行缓存操作,确保时序正确。
  • 若系统对实时性要求极高,可考虑使用 MPU 将 DMA 相关区域配置为非缓存,但需权衡 CPU 访问性能。

通过以上方案,你可以彻底解决 STM32F4 系列 D-Cache 与 DMA 的数据一致性问题,让系统稳定运行。