引言

STM32F4 系列内置了 Cortex-M4 内核,支持 D-Cache(数据缓存)和 I-Cache。D-Cache 能显著提升 CPU 访问外部存储器(如 SDRAM)的性能,但同时也引入了缓存一致性问题。当 DMA 控制器直接访问内存时,它绕过 CPU 的缓存,直接读写物理内存。如果 CPU 和 DMA 对同一块内存区域进行操作,而缓存未及时同步,就会产生数据不一致。

本文针对三种典型场景,深入分析问题根源,并给出经过验证的规避方案。

场景一:CPU 写数据,DMA 读数据(发送场景)

问题原理

CPU 将待发送的数据写入内存缓冲区,然后启动 DMA 将缓冲区内容传输到外设(如 UART、SPI)。由于 D-Cache 的存在,CPU 写入的数据可能只停留在缓存中,尚未回写到物理内存。DMA 读取物理内存时,得到的是旧数据,导致发送错误。

规避方案

在启动 DMA 传输前,必须调用 SCB_CleanDCache()SCB_CleanDCache_by_Addr() 将缓存中对应地址区域的数据回写到物理内存。

代码示例

#include "stm32f4xx.h"
#include "core_cm4.h"

uint8_t tx_buffer[1024] __attribute__((aligned(32)));

void send_via_dma(void) {
    // 填充数据
    for (int i = 0; i < sizeof(tx_buffer); i++) {
        tx_buffer[i] = i;
    }

    // 关键:回写 D-Cache,确保数据到达物理内存
    SCB_CleanDCache_by_Addr((uint32_t*)tx_buffer, sizeof(tx_buffer));

    // 配置 DMA 并启动传输(此处省略具体外设配置)
    DMA_Start_Transmit(tx_buffer, sizeof(tx_buffer));
}

注意事项

  • 缓冲区地址必须按 32 字节对齐(Cache line 大小),否则 CleanDCache_by_Addr 可能无法正确回写整个区域。
  • 如果缓冲区大小不是 32 的倍数,需向上取整,避免遗漏边界。

场景二:DMA 写数据,CPU 读数据(接收场景)

问题原理

DMA 从外设接收数据并写入内存缓冲区,然后 CPU 读取该缓冲区进行处理。DMA 写入的是物理内存,但 CPU 读取时,如果缓存中已有该地址的旧数据(例如之前 CPU 访问过该区域),则 CPU 会命中缓存,读到旧数据,而新数据被忽略。

规避方案

在 CPU 读取 DMA 写入的数据之前,必须调用 SCB_InvalidateDCache()SCB_InvalidateDCache_by_Addr() 使缓存中对应地址区域失效,强制 CPU 从物理内存重新加载。

代码示例

uint8_t rx_buffer[1024] __attribute__((aligned(32)));
volatile uint32_t rx_len = 0;

void DMA_RX_Complete_Handler(void) {
    // 关键:使缓存失效,让 CPU 从物理内存读取最新数据
    SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buffer, sizeof(rx_buffer));

    // 现在可以安全地处理 rx_buffer 中的数据
    process_data(rx_buffer, rx_len);
}

注意事项

  • 失效操作必须在 DMA 完全完成之后、CPU 读取之前执行。
  • 如果 DMA 采用循环模式,每次传输完成中断中都要执行失效操作,但要注意避免在 DMA 仍在写入时失效,否则可能读到半新半旧的数据。

场景三:双缓冲交替访问(乒乓缓冲)

问题原理

在高速数据采集或通信中,常使用双缓冲(乒乓缓冲)机制:DMA 正在填充缓冲区 A 时,CPU 处理缓冲区 B;然后交换。这种模式下,CPU 和 DMA 交替访问两个缓冲区,若缓存管理不当,会导致数据错乱。例如,CPU 处理完 B 后,下次 DMA 可能写入 B,但 CPU 的缓存中仍保留 B 的旧数据,导致后续读取错误。

规避方案

在切换缓冲区时,对即将由 DMA 写入的缓冲区执行失效操作,对即将由 CPU 写入的缓冲区执行回写操作。具体流程如下:

  1. 当 DMA 完成填充缓冲区 A,CPU 准备处理 A 时,先失效 A 的缓存。
  2. 当 CPU 处理完 A,准备让 DMA 再次使用 A 时,先回写 A 的缓存(如果 CPU 修改过 A)。
  3. 对缓冲区 B 同理。

代码示例

#define BUFFER_SIZE 1024
uint8_t buf_A[BUFFER_SIZE] __attribute__((aligned(32)));
uint8_t buf_B[BUFFER_SIZE] __attribute__((aligned(32)));
volatile uint8_t current_buf = 0; // 0: A, 1: B

void DMA_Transfer_Complete_Handler(void) {
    uint8_t *processed_buf;
    uint8_t *next_buf;

    if (current_buf == 0) {
        processed_buf = buf_A;
        next_buf = buf_B;
    } else {
        processed_buf = buf_B;
        next_buf = buf_A;
    }

    // 1. 使已填充缓冲区的缓存失效,以便 CPU 读取最新数据
    SCB_InvalidateDCache_by_Addr((uint32_t*)processed_buf, BUFFER_SIZE);

    // 2. 处理数据(假设处理过程中可能修改缓冲区)
    process_data(processed_buf, BUFFER_SIZE);

    // 3. 回写已处理缓冲区,确保修改写回物理内存(若未修改可省略)
    SCB_CleanDCache_by_Addr((uint32_t*)processed_buf, BUFFER_SIZE);

    // 4. 切换缓冲区,并准备下一次 DMA 传输
    current_buf ^= 1;
    DMA_Start_Receive(next_buf, BUFFER_SIZE);
}

注意事项

  • 失效和回写操作必须成对出现,且顺序不能颠倒。
  • 如果 CPU 在步骤 2 中只读不写,则步骤 3 可以省略,但为了通用性建议保留。
  • 缓冲区切换时,要确保 DMA 不会在缓存操作完成前启动,否则可能仍会出错。

总结

D-Cache 与 DMA 的数据一致性是 STM32F4 开发中的关键问题。本文总结了三种典型场景及对应的规避方案:

  • CPU 写,DMA 读:启动 DMA 前执行 SCB_CleanDCache_by_Addr()
  • DMA 写,CPU 读:CPU 读取前执行 SCB_InvalidateDCache_by_Addr()
  • 双缓冲交替:切换时对两个缓冲区分别执行失效和回写操作。

此外,务必注意缓冲区地址对齐(32 字节)和大小处理,避免边界问题。在实际项目中,建议封装统一的缓存管理函数,减少出错概率。掌握这些技巧,你将能充分发挥 STM32F4 的高性能,同时避免数据陷阱。