一、为什么 DMA 和 D-Cache 会打架?

STM32H7 搭载 Cortex-M7 内核,主频可达 480MHz,为了弥补 CPU 与内存之间的速度鸿沟,芯片内部集成了 D-Cache(数据缓存)。CPU 访问数据时,会先将内存中的数据加载到 Cache 中,后续读写直接操作 Cache,从而大幅提升性能。

但 DMA(直接存储器访问)是独立于 CPU 的硬件单元,它直接读写物理内存,不经过 Cache。这就导致了经典的一致性问题:

  • CPU 写,DMA 读:CPU 将数据写入 Cache,但尚未写回内存(Write-Back 模式),此时启动 DMA 发送,DMA 读到的是内存中的旧数据。
  • DMA 写,CPU 读:DMA 将新数据写入内存,但 CPU 读取时命中 Cache 中的旧数据,导致读到错误值。

简单说:Cache 让 CPU 和 DMA 看到了两个不同的“内存视图”。

二、解决思路:MPU 配置 + Cache 维护

针对上述问题,STM32H7 提供两种主要手段:

  1. MPU(内存保护单元)配置:将 DMA 缓冲区所在的内存区域配置为 Non-CacheableWrite-Through 模式,从根源上避免 Cache 与 DMA 的不一致。
  2. 手动 Clean/Invalidate:在 DMA 传输前后,调用 SCB_CleanDCache_by_Addr()SCB_InvalidateDCache_by_Addr() 维护 Cache 一致性。

实际项目中,两者常结合使用:对频繁 DMA 的大缓冲区用 MPU 设为 Non-Cacheable;对临时小块数据用 Clean/Invalidate 更灵活。

三、MPU 配置实战(以 DMA 接收缓冲区为例)

假设我们在 0x30000000(D1 SRAM)定义了一个 1024 字节的 DMA 接收缓冲区,希望该区域不经过 D-Cache。

3.1 配置步骤

  • 使能 MPU:SCB->SHCSR |= SCB_SHCSR_MEMFAULTENA_Msk;(可选,用于调试)
  • 禁用 MPU:MPU->CTRL = 0;
  • 配置 Region 0:基地址 0x30000000,大小 1024 字节,属性为 Normal Non-Cacheable
  • 使能 MPU 和 D-Cache

3.2 完整代码

#include "stm32h7xx.h"

#define DMA_BUFFER_ADDR  0x30000000
#define DMA_BUFFER_SIZE  1024

void MPU_Config(void)
{
    // 关闭 MPU
    MPU->CTRL = 0;

    // 配置 Region 0
    MPU->RNR = 0;                           // 选择 Region 0
    MPU->RBAR = DMA_BUFFER_ADDR & MPU_RBAR_ADDR_Msk;
    MPU->RASR = (0x09 << MPU_RASR_SIZE_Pos) |  // 大小 2^(9+1)=1024 字节
                (0x00 << MPU_RASR_AP_Pos)   |  // 特权/用户可读写
                (0x01 << MPU_RASR_TEX_Pos)  |  // TEX=001, C=0, B=0 -> Normal Non-Cacheable
                MPU_RASR_ENABLE_Msk;

    // 使能 MPU(背景区域默认属性)
    MPU->CTRL = MPU_CTRL_ENABLE_Msk | MPU_CTRL_PRIVDEFENA_Msk;

    // 使能 D-Cache 和 I-Cache
    SCB_EnableICache();
    SCB_EnableDCache();
}

注意:MPU 配置必须在使能 Cache 之前完成,且 Region 地址和大小必须对齐。

四、Clean/Invalidate 实战

如果不想将整个缓冲区设为 Non-Cacheable,可以在 DMA 传输前后手动维护 Cache。

4.1 关键函数

  • SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize):将 Cache 中已修改的数据写回内存。
  • SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize):丢弃 Cache 中的旧数据,强制从内存重新加载。

4.2 典型场景代码

场景 1:CPU 准备数据,DMA 发送

uint8_t tx_buf[256] __attribute__((aligned(32)));

void DMA_Send(void)
{
    // 填充数据
    for (int i = 0; i < 256; i++) tx_buf[i] = i;

    // 清理 D-Cache,确保数据写回内存
    SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, 256);

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

场景 2:DMA 接收数据,CPU 读取

uint8_t rx_buf[256] __attribute__((aligned(32)));

void DMA_Receive_Complete_Callback(void)
{
    // 无效化 D-Cache,丢弃旧数据
    SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, 256);

    // 现在 CPU 读取的是 DMA 写入的新数据
    process_data(rx_buf, 256);
}

五、注意事项与避坑指南

  • 地址对齐:Clean/Invalidate 操作的地址和大小必须是 32 字节对齐,否则可能误伤相邻数据。建议缓冲区使用 __attribute__((aligned(32)))
  • Invalidate 的风险:如果缓冲区中部分数据在 Cache 中被修改但未写回,直接 Invalidate 会丢失这些修改。因此,Invalidate 前应确保没有待写回的数据,或先 Clean 再 Invalidate。
  • DMA 传输中的 Cache 操作:不要在 DMA 传输过程中操作 Cache,否则可能引发不可预知错误。
  • 多缓冲区管理:如果使用多个 DMA 缓冲区,建议统一放在 Non-Cacheable 区域,简化维护。
  • 性能权衡:Non-Cacheable 区域会降低 CPU 访问速度,对性能敏感的数据可考虑 Write-Through 模式或手动维护。
  • 调试技巧:若发现数据错乱,可先关闭 D-Cache 验证是否为一致性问题。

六、总结

STM32H7 的 D-Cache 与 DMA 一致性是嵌入式开发中的经典难题。核心原则是:让 CPU 和 DMA 看到相同的内存视图。通过 MPU 将 DMA 缓冲区设为 Non-Cacheable 是最稳妥的方案;若需兼顾性能,则务必在 DMA 传输前后正确调用 Clean/Invalidate,并注意对齐与操作顺序。掌握这些技巧,你就能在 H7 平台上既享受 Cache 带来的性能飞跃,又保证数据传输的绝对可靠。