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

为什么需要关注 D-Cache 与 DMA 的一致性?

STM32F4 系列(如 STM32F429)内置了 D-Cache(数据缓存),用于加速 CPU 对内存的访问。然而,DMA 控制器直接访问物理内存,不经过缓存。当 CPU 和 DMA 同时操作同一内存区域时,若缓存未及时刷新或失效,CPU 可能读到旧数据,或 DMA 将旧数据写入内存,导致数据不一致。这在高速数据采集、通信外设(如 UART、SPI、以太网)中尤为常见。

场景一:DMA 写入内存,CPU 读取(外设→内存)

问题描述

DMA 从外设(如 ADC、UART)接收数据到内存缓冲区,CPU 随后处理该缓冲区。若缓冲区被 D-Cache 缓存,CPU 可能读取到缓存中的旧数据,而非 DMA 刚写入的新数据。

原理分析

DMA 写入内存后,内存内容已更新,但 D-Cache 中对应行仍保留旧值。CPU 读取时优先命中缓存,导致数据陈旧。

解法:使缓存行失效(Invalidate)

在 CPU 读取 DMA 数据前,调用 SCB_InvalidateDCache_by_Addr 使缓冲区对应的缓存行失效,强制 CPU 从内存重新加载。

配置步骤

  1. 启用 D-Cache:在 main() 中调用 SCB_EnableDCache()
  2. 确保缓冲区地址按 32 字节对齐(D-Cache 行大小)。
  3. DMA 传输完成后,在读取数据前使缓存失效。

代码示例

// 缓冲区定义(32字节对齐)
__attribute__((aligned(32))) uint8_t rx_buffer[256];

// DMA 传输完成回调(中断中)
void DMA_RxComplete(DMA_HandleTypeDef *hdma) {
    // 使缓存行失效,确保 CPU 读取到 DMA 写入的新数据
    SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buffer, sizeof(rx_buffer));
    // 现在可以安全处理 rx_buffer
    process_data(rx_buffer);
}

注意事项

  • 失效操作有开销,仅在必要时调用,避免频繁失效影响性能。
  • 缓冲区大小应为 32 的倍数,否则需处理尾部非对齐部分。

场景二:CPU 写入内存,DMA 读取(内存→外设)

问题描述

CPU 准备数据到缓冲区,然后启动 DMA 将数据传输到外设(如 DAC、SPI 发送)。若缓冲区被缓存,DMA 可能读取到内存中的旧数据,而非 CPU 刚写入的新数据。

原理分析

CPU 写入缓冲区时,数据可能仅更新在 D-Cache 中,尚未写回内存。DMA 直接访问内存,因此读到旧数据。

解法:清理缓存行(Clean)

在启动 DMA 传输前,调用 SCB_CleanDCache_by_Addr 将缓存数据写回内存,确保 DMA 能读取最新数据。

配置步骤

  1. 启用 D-Cache。
  2. CPU 填充缓冲区后,清理对应缓存行。
  3. 启动 DMA 传输。

代码示例

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

// 准备数据
fill_data(tx_buffer);

// 清理缓存,将数据写回内存
SCB_CleanDCache_by_Addr((uint32_t*)tx_buffer, sizeof(tx_buffer));

// 启动 DMA 发送
HAL_UART_Transmit_DMA(&huart1, tx_buffer, sizeof(tx_buffer));

注意事项

  • 若缓冲区较小,也可直接使用 SCB_CleanDCache() 全清理,但效率较低。
  • 清理操作必须在 DMA 启动前完成,否则可能仍读到旧数据。

场景三:共享缓冲区(CPU 和 DMA 同时读写)

问题描述

例如,一个环形缓冲区用于双缓冲采集,CPU 处理一个缓冲块,同时 DMA 填充另一个。若两者共享同一缓存行,可能发生数据交错错误。

原理分析

D-Cache 以 32 字节行为单位管理。若两个缓冲区地址相邻且共享缓存行,CPU 清理或失效操作可能影响对方数据。

解法:使用双缓冲并隔离缓存行

将两个缓冲区分别对齐到不同的缓存行,并在切换时执行清理+失效操作。

配置步骤

  1. 定义两个缓冲区,每个大小对齐到 32 字节,且起始地址间隔至少 32 字节。
  2. 在缓冲区切换时,先清理当前 CPU 写入的缓冲区,再失效即将读取的缓冲区。

代码示例

__attribute__((aligned(32))) uint8_t buf_a[256];
__attribute__((aligned(32))) uint8_t buf_b[256];

volatile uint8_t active_buf = 0; // 0: buf_a, 1: buf_b

void DMA_RxComplete(DMA_HandleTypeDef *hdma) {
    // 失效当前 DMA 填充的缓冲区
    if (active_buf == 0) {
        SCB_InvalidateDCache_by_Addr((uint32_t*)buf_a, sizeof(buf_a));
        process(buf_a);
        // 清理另一个缓冲区,准备下次 DMA 写入
        SCB_CleanDCache_by_Addr((uint32_t*)buf_b, sizeof(buf_b));
        active_buf = 1;
    } else {
        SCB_InvalidateDCache_by_Addr((uint32_t*)buf_b, sizeof(buf_b));
        process(buf_b);
        SCB_CleanDCache_by_Addr((uint32_t*)buf_a, sizeof(buf_a));
        active_buf = 0;
    }
}

注意事项

  • 确保缓冲区地址不共享缓存行,否则失效操作可能误伤另一缓冲区。
  • 使用 volatile 修饰状态变量,防止编译器优化。

总结与最佳实践

  • 启用 D-Cache 前:评估外设 DMA 的使用场景,必要时可禁用 D-Cache(SCB_DisableDCache())以简化设计,但会牺牲性能。
  • 对齐与大小:缓冲区地址和大小尽量对齐到 32 字节,减少缓存行操作开销。
  • 操作顺序:DMA 写入后必须失效,DMA 读取前必须清理。
  • 性能权衡:频繁清理/失效会影响性能,可考虑使用 MPU 将 DMA 缓冲区配置为非缓存(强序内存),但需谨慎配置。

通过以上三种场景的解法,开发者可以安全地在 STM32F4 上使用 D-Cache 与 DMA,兼顾性能与数据一致性。