STM32F4 D-Cache 与 DMA 数据一致性:三种典型场景及解法

一、背景与原理

STM32F4 系列(如 STM32F429)内置了 D-Cache(数据缓存),用于加速 CPU 对内存的访问。然而,DMA 控制器直接访问物理内存(SRAM),不经过 D-Cache。这导致 CPU 和 DMA 看到的数据视图可能不一致:

  • CPU 写操作:数据先写入 Cache,可能尚未回写到物理内存。
  • DMA 读操作:DMA 直接从物理内存读取,可能读到旧数据。
  • DMA 写操作:DMA 将数据写入物理内存,但 Cache 中可能残留旧副本,CPU 读时可能命中 Cache 得到脏数据。

因此,必须通过软件维护 Cache 的一致性,主要操作有:

  • Clean:将 Cache 中的脏数据回写到物理内存。
  • Invalidate:将 Cache 中的行标记为无效,强制下次从物理内存重新加载。

在 STM32F4 上,CMSIS 提供了 SCB_CleanDCache()SCB_InvalidateDCache()SCB_CleanInvalidateDCache() 等函数。注意,这些操作以 Cache 行为单位(通常 32 字节),因此建议 DMA 缓冲区按 32 字节对齐,且长度尽量为 32 的倍数。

二、三种典型场景与解法

场景 1:CPU 写数据,DMA 读数据(如发送数据到外设)

问题:CPU 填充发送缓冲区后,DMA 启动传输,但缓冲区内容可能还在 Cache 中,DMA 读到的是旧数据。

解法:在启动 DMA 之前,对缓冲区执行 Clean 操作,确保数据回写。

配置步骤

  1. 定义缓冲区,使用 __ALIGNED(32) 对齐。
  2. 使能 D-Cache(若未使能)。
  3. 在 DMA 启动前调用 SCB_CleanDCache_by_Addr 或全 Clean。

代码示例

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

__ALIGNED(32) uint8_t tx_buffer[256];

void DMA_SendData(void) {
    // 填充数据
    for (int i = 0; i < sizeof(tx_buffer); i++) {
        tx_buffer[i] = i;
    }
    // Clean D-Cache,确保数据回写 SRAM
    SCB_CleanDCache_by_Addr((uint32_t*)tx_buffer, sizeof(tx_buffer));
    // 配置 DMA(略)
    DMA_StartTransmit(tx_buffer, sizeof(tx_buffer));
}

注意:如果缓冲区较小,也可以直接调用 SCB_CleanDCache() 全 Clean,但效率较低。

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

问题:DMA 将外设数据写入缓冲区,但 CPU 读取时可能命中 Cache 中的旧副本,导致读到脏数据。

解法:在 CPU 读取之前,对缓冲区执行 Invalidate 操作,使 Cache 行失效。

配置步骤

  1. 确保缓冲区对齐。
  2. DMA 传输完成后(通过中断或标志),执行 Invalidate。
  3. 然后 CPU 从缓冲区读取。

代码示例

__ALIGNED(32) uint8_t rx_buffer[128];
volatile uint8_t dma_done = 0;

void DMA_IRQHandler(void) {
    if (DMA_GetFlagStatus(...)) {
        dma_done = 1;
    }
}

void ProcessReceivedData(void) {
    if (dma_done) {
        // Invalidate D-Cache,使 CPU 从 SRAM 重新加载
        SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buffer, sizeof(rx_buffer));
        // 现在可以安全读取 rx_buffer
        for (int i = 0; i < sizeof(rx_buffer); i++) {
            process(rx_buffer[i]);
        }
        dma_done = 0;
    }
}

注意:Invalidate 操作会丢弃 Cache 中未回写的脏数据,因此仅当缓冲区完全由 DMA 写入时才使用。若 CPU 也修改过该缓冲区,需先 Clean 再 Invalidate。

场景 3:双缓冲交替访问(CPU 和 DMA 轮流使用两个缓冲区)

问题:在双缓冲设计中,CPU 处理缓冲区 A 时,DMA 填充缓冲区 B;下次交换。若不做 Cache 维护,切换时可能因 Cache 残留导致数据错乱。

解法:在每次切换缓冲区时,对即将使用的缓冲区执行 Invalidate(若 DMA 写入)或 Clean(若 CPU 写入)。

配置步骤

  1. 定义两个对齐缓冲区。
  2. 使用索引切换。
  3. 在切换时根据角色执行相应操作。

代码示例

__ALIGNED(32) uint8_t buffer[2][256];
uint8_t active_buf = 0;

void SwitchBuffer(void) {
    // 假设 DMA 正在填充 active_buf,CPU 处理另一个
    uint8_t *dma_buf = buffer[active_buf];
    uint8_t *cpu_buf = buffer[active_buf ^ 1];

    // 在启动 DMA 前,确保 DMA 缓冲区无脏数据(CPU 可能写过)
    SCB_CleanDCache_by_Addr((uint32_t*)dma_buf, sizeof(buffer[0]));
    // 启动 DMA 传输到 dma_buf(略)

    // 在 CPU 读取前,使缓存失效
    SCB_InvalidateDCache_by_Addr((uint32_t*)cpu_buf, sizeof(buffer[0]));
    // 处理 cpu_buf 数据
    ProcessData(cpu_buf);

    active_buf ^= 1;
}

注意:此场景需确保 DMA 传输长度不超过缓冲区大小,且缓冲区大小是 32 的倍数,否则需额外处理尾部。

三、注意事项与最佳实践

  • 对齐与长度:DMA 缓冲区必须 32 字节对齐,长度建议为 32 的倍数。可使用 __ALIGNED(32)__attribute__((aligned(32)))
  • 操作粒度SCB_CleanDCache_by_AddrSCB_InvalidateDCache_by_Addr 会操作包含指定地址的整个 Cache 行,因此相邻数据可能被误操作,需确保缓冲区独立。
  • 性能权衡:频繁的 Clean/Invalidate 会降低性能,可考虑使用 MPU 将 DMA 缓冲区配置为非缓存(如 SRAM 区域),但需谨慎配置。
  • 中断安全:在中断中执行 Clean/Invalidate 时,注意优先级和原子性,避免与主循环冲突。
  • 调试技巧:使用逻辑分析仪或断点观察数据,若出现随机错误,优先检查 Cache 操作。

四、总结

D-Cache 与 DMA 的一致性问题是 STM32F4 高性能应用的常见陷阱。通过理解 Clean 和 Invalidate 的原理,针对三种典型场景(CPU 写 DMA 读、DMA 写 CPU 读、双缓冲)采取相应操作,即可有效避免数据错乱。记住:写后 Clean,读前 Invalidate,并确保缓冲区对齐。掌握这些技巧,你的嵌入式系统将更加健壮。