一、问题现象:为什么 DMA 数据总是“慢半拍”?

在 STM32H7 上,很多开发者会遇到这样的现象:

  • DMA 从外设搬运数据到内存后,CPU 读到的还是旧值;
  • CPU 修改缓冲区后启动 DMA 发送,对方收到的却是修改前的数据;
  • 只有关闭 D-Cache 才正常,但性能大幅下降。

根本原因:Cortex-M7 的 D-Cache 与 DMA 控制器看到的内存视图不一致。DMA 直接访问物理内存(SRAM),而 CPU 可能读写的是 Cache 中的副本。若没有正确维护一致性,数据就会“错位”。

二、原理:D-Cache、写缓冲与 DMA 的冲突

Cortex-M7 的 D-Cache 是写回(Write-Back)+ 写分配(Write-Allocate) 策略:

  • CPU 写数据时,若 Cache 命中,只更新 Cache,不立即写回 SRAM;
  • CPU 读数据时,若 Cache 未命中,先从 SRAM 加载到 Cache 行,再返回。

DMA 不经过 Cache,直接读写 SRAM。因此:

  • CPU 写 → DMA 读:数据可能还在 Cache 中未写回,DMA 读到旧数据;
  • DMA 写 → CPU 读:SRAM 已更新,但 Cache 中仍是旧副本,CPU 读到旧数据。

此外,Cortex-M7 还有写缓冲(Write Buffer),即使执行了内存屏障,写操作也可能延迟到达 SRAM。

三、Cache 行对齐:容易被忽略的“隐形杀手”

D-Cache 以 Cache Line(通常 32 字节) 为单位操作。若缓冲区未按 32 字节对齐,Clean/Invalidate 可能影响相邻数据:

  • Clean:将 Cache 行写回 SRAM。若缓冲区跨行,可能把无关数据也写回;
  • Invalidate:丢弃 Cache 行。若缓冲区跨行,可能丢弃相邻变量尚未写回的数据,导致丢失。

避坑原则:

  • DMA 缓冲区必须 32 字节对齐,且大小建议为 32 字节整数倍;
  • 使用 __attribute__((aligned(32))) 或 ALIGN_32BYTES 宏;
  • 若无法对齐,需手动处理边界行,或使用 MPU 将该区域配置为 Non-Cacheable。
// 推荐:32 字节对齐的 DMA 缓冲区
#define DMA_BUF_SIZE  256
__attribute__((aligned(32))) uint8_t dma_rx_buf[DMA_BUF_SIZE];
__attribute__((aligned(32))) uint8_t dma_tx_buf[DMA_BUF_SIZE];

四、正确使用 Clean 与 Invalidate

CMSIS 提供了维护 Cache 的函数:

  • SCB_CleanDCache_by_Addr(addr, size):将 Cache 写回 SRAM;
  • SCB_InvalidateDCache_by_Addr(addr, size):丢弃 Cache,强制下次从 SRAM 读;
  • SCB_CleanInvalidateDCache_by_Addr(addr, size):先写回再丢弃。

使用场景:

  • CPU 写 → DMA 读:启动 DMA 前执行 SCB_CleanDCache_by_Addr;
  • DMA 写 → CPU 读:DMA 完成中断中执行 SCB_InvalidateDCache_by_Addr;
  • 双向缓冲:使用 SCB_CleanInvalidateDCache_by_Addr。
// 发送前:确保 CPU 写入的数据到达 SRAM
SCB_CleanDCache_by_Addr((uint32_t*)dma_tx_buf, DMA_BUF_SIZE);
HAL_DMA_Start(&hdma_memtomem, (uint32_t)dma_tx_buf, (uint32_t)dest, DMA_BUF_SIZE);

// 接收完成中断中:丢弃旧 Cache,读取最新数据
void DMA_RxComplete_Callback(void)
{
    SCB_InvalidateDCache_by_Addr((uint32_t*)dma_rx_buf, DMA_BUF_SIZE);
    // 此时 CPU 读取 dma_rx_buf 才是 DMA 写入的最新值
}

注意:

  • 地址和大小都需 32 字节对齐,否则函数内部会按行处理,可能影响相邻数据;
  • 调用前确保没有其他 CPU 访问该缓冲区,避免竞态;
  • 中断中调用需注意执行时间,但通常可接受。

五、MPU 配置:一劳永逸的 Non-Cacheable 方案

若某块内存区域只用于 DMA,且 CPU 访问不频繁,可将其配置为 Non-Cacheable,彻底避免一致性问题。

使用 MPU 配置步骤:

  1. 使能 MPU:SCB->SHCSR |= SCB_SHCSR_MEMFAULTENA_Msk;
  2. 禁用 MPU:MPU->CTRL = 0;
  3. 配置 Region:
    • 选择区域号(0~15);
    • 设置基地址、大小、属性;
    • 属性设为 Normal, Non-Cacheable, Shareable;
  4. 使能 MPU:MPU->CTRL = MPU_CTRL_ENABLE_Msk | MPU_CTRL_PRIVDEFENA_Msk;
  5. 执行 __DSB(); __ISB();
void MPU_Config_DMA_NonCacheable(void)
{
    MPU->CTRL = 0; // 禁用 MPU
    MPU->RNR = 0;  // 使用 Region 0

    // 基地址:DMA 缓冲区起始地址,需 32 字节对齐
    MPU->RBAR = (uint32_t)dma_rx_buf & MPU_RBAR_ADDR_Msk;

    // 大小:256 字节,2^(SIZE+1),SIZE=7 表示 256 字节
    // 属性:TEX=1, C=0, B=0, S=1 -> Normal, Non-Cacheable, Shareable
    MPU->RASR = (0x7 << MPU_RASR_SIZE_Pos) |
                (1 << MPU_RASR_TEX_Pos)  |
                (0 << MPU_RASR_C_Pos)    |
                (0 << MPU_RASR_B_Pos)    |
                (1 << MPU_RASR_S_Pos)    |
                (1 << MPU_RASR_AP_Pos)   | // 全访问
                (1 << MPU_RASR_ENABLE_Pos);

    MPU->CTRL = MPU_CTRL_ENABLE_Msk | MPU_CTRL_PRIVDEFENA_Msk;
    __DSB();
    __ISB();
}

注意:

  • MPU 区域不能与已有区域重叠,否则可能触发 MemManage 异常;
  • 配置为 Non-Cacheable 后,CPU 访问速度会下降,但换来了简单可靠;
  • 若使用 FreeRTOS 等 RTOS,需确保 MPU 配置在任务切换时不被破坏。

六、完整排查流程与代码模板

排查步骤:

  1. 确认 DMA 缓冲区是否 32 字节对齐;
  2. 确认是否在正确时机执行 Clean/Invalidate;
  3. 检查是否遗漏 __DSB() 等内存屏障;
  4. 若仍不稳定,尝试将缓冲区配置为 Non-Cacheable;
  5. 使用调试器查看 SRAM 与 Cache 内容,对比差异。

代码模板:

#include "stm32h7xx.h"

#define BUF_SIZE 256
__attribute__((aligned(32))) uint8_t rx_buf[BUF_SIZE];
__attribute__((aligned(32))) uint8_t tx_buf[BUF_SIZE];

void DMA_Init(void)
{
    // 初始化 DMA,源/目的地址为 rx_buf/tx_buf
    // ...
}

void Send_Data(void)
{
    // 填充 tx_buf
    for (int i = 0; i < BUF_SIZE; i++) tx_buf[i] = i;

    // 清理 Cache,确保数据写回 SRAM
    SCB_CleanDCache_by_Addr((uint32_t*)tx_buf, BUF_SIZE);
    __DSB(); // 确保写操作完成

    // 启动 DMA 发送
    HAL_DMA_Start(&hdma_memtomem, (uint32_t)tx_buf, (uint32_t)dest, BUF_SIZE);
}

void DMA_RxComplete_Callback(void)
{
    // 无效化 Cache,丢弃旧数据
    SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, BUF_SIZE);
    __DSB();

    // 处理 rx_buf 中的数据
    Process_Data(rx_buf, BUF_SIZE);
}

七、总结与避坑清单

  • 对齐:DMA 缓冲区必须 32 字节对齐;
  • 时机:CPU 写后 Clean,DMA 写后 Invalidate;
  • 屏障:Clean/Invalidate 后加 __DSB();
  • MPU:对 DMA 专用内存可配置为 Non-Cacheable;
  • 调试:关闭 D-Cache 可快速定位是否为一致性问题;
  • 文档:参考 STM32H7 参考手册中 Cache 与 MPU 章节,以及 AN4838 应用笔记。

掌握这些要点,你就能在 STM32H7 上安全地使用 D-Cache 与 DMA,既享受高性能,又避免数据错乱。