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

STM32H7 的 Cortex-M7 内核带有一级数据缓存(D-Cache),默认写通/写回策略下,CPU 访问的数据可能只停留在 Cache 中,并未写回 SRAM。而 DMA 是直接访问物理内存的,它看不到 Cache 里的新数据。

典型现象:

  • CPU 写入缓冲区后启动 DMA 发送,DMA 发出的却是旧数据。
  • DMA 接收数据到缓冲区,CPU 读到的却是 Cache 中的旧值。
  • 数据时对时错,调试时加打印又正常(因为打印触发了内存访问)。

根本原因:CPU 与 DMA 对同一块内存的视图不一致

二、解决思路:三种方案对比

  • 方案1:操作前后手动 Clean/Invalidate Cache 使用 SCB_CleanDCache_by_Addr()SCB_InvalidateDCache_by_Addr()。灵活但容易漏调用,且 Invalidate 有风险(可能丢弃未写回数据)。
  • 方案2:将 DMA 缓冲区放到 Non-Cacheable 区域 通过 MPU 配置,让该区域绕过 D-Cache。一劳永逸,性能略降但最安全。
  • 方案3:使用 DTCM RAM DTCM 默认不经过 Cache,但容量有限(通常 128KB),且 DMA 能否访问取决于总线矩阵。

推荐方案2:用 MPU 把 DMA 缓冲区所在 SRAM 区域设为 Non-Cacheable,简单可靠。

三、MPU 配置实战步骤

3.1 确定内存区域

假设使用 D2 SRAM(0x30000000 起),划出 32KB 给 DMA 缓冲区:

  • 基地址:0x30000000
  • 大小:32KB(2^15),需满足 MPU 区域大小对齐。

3.2 MPU 配置要点

  • 使能 MPU:SCB->CTRL |= SCB_CTRL_MPU_Msk;
  • 选择区域号(0~15),配置基地址、属性、大小。
  • 属性设为 Normal, Non-Cacheable:
    • TEX=0b001, C=0, B=0
    • 或直接使用 ARM_MPU_ATTR_NORMAL_NON_CACHEABLE
  • 使能该区域和 MPU。

3.3 完整代码示例

#include "stm32h7xx.h"

#define DMA_BUFFER_ADDR   0x30000000UL
#define DMA_BUFFER_SIZE   (32UL * 1024UL)

void MPU_Config_NonCacheable(void)
{
    /* 关闭 MPU,配置前需禁用 */
    ARM_MPU_Disable();

    /* 配置 Region 0:DMA 缓冲区 Non-Cacheable */
    ARM_MPU_SetRegion(
        0,                                  /* Region number */
        ARM_MPU_RBAR(DMA_BUFFER_ADDR,       /* Base address */
                     ARM_MPU_SH_NON,        /* Shareable: Non-shareable */
                     0,                     /* AP: 读写权限,0 表示 RW */
                     1),                    /* XN: 禁止执行 */
        ARM_MPU_RLAR(DMA_BUFFER_ADDR + DMA_BUFFER_SIZE - 1,
                     ARM_MPU_ATTR_NORMAL_NON_CACHEABLE, /* 属性 */
                     0)                     /* AttrIndex */
    );

    /* 使能 MPU */
    ARM_MPU_Enable(MPU_CTRL_PRIVDEFENA_Msk);
}

注意:ARM_MPU_RLAR 的第二个参数是属性,ARM_MPU_ATTR_NORMAL_NON_CACHEABLE 在 CMSIS 中定义为 (0UL << 28) | (0UL << 24) | (0UL << 20) | (0UL << 16) | (0UL << 12) | (0UL << 8) | (0UL << 4) | (0UL << 0) 的简化宏,实际需确认 CMSIS 版本。若没有该宏,可手动构造:(0x1UL << 28) | (0UL << 24) | (0UL << 20) | (0UL << 16) | (0UL << 12) | (0UL << 8) | (0UL << 4) | (0UL << 0)

更稳妥的写法是直接使用寄存器:

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

    /* Region 0: 0x30000000, 32KB, Normal Non-Cacheable */
    MPU->RNR  = 0;
    MPU->RBAR = (DMA_BUFFER_ADDR & MPU_RBAR_ADDR_Msk) | MPU_RBAR_VALID_Msk | (0 << MPU_RBAR_REGION_Pos);
    MPU->RASR = (0x1UL << MPU_RASR_TEX_Pos) |   /* TEX = 001 */
                (0UL << MPU_RASR_C_Pos)   |     /* C = 0 */
                (0UL << MPU_RASR_B_Pos)   |     /* B = 0 */
                (0UL << MPU_RASR_S_Pos)   |     /* S = 0 */
                (3UL << MPU_RASR_AP_Pos)  |     /* AP = 011, 全访问 */
                (0UL << MPU_RASR_XN_Pos)  |     /* 允许执行 */
                (0UL << MPU_RASR_SRD_Pos) |     /* 子区域禁用 */
                (14UL << MPU_RASR_SIZE_Pos) |   /* 2^(14+1) = 32KB */
                (1UL << MPU_RASR_ENABLE_Pos);   /* 使能该区域 */

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

3.4 在 main 中调用

int main(void)
{
    HAL_Init();
    SystemClock_Config();

    MPU_Config_NonCacheable();  /* 必须在使能 D-Cache 之前或之后均可,但建议尽早 */

    /* 使能 D-Cache */
    SCB_EnableDCache();

    /* 后续 DMA 操作直接使用 DMA_BUFFER_ADDR 即可,无需 Clean/Invalidate */
    ...
}

四、注意事项与避坑

  • MPU 区域大小必须为 2 的幂,且基地址按大小对齐。32KB 对应 SIZE=14。
  • 子区域(SRD) 可进一步细分,但一般不用。
  • 使能 MPU 后,未覆盖的区域默认走背景区域,若设置了 PRIVDEFENA,特权模式可访问默认内存映射。
  • D-Cache 使能后,栈、堆等仍走 Cache,只有 MPU 指定的 Non-Cacheable 区域绕过。
  • 若使用 DTCM,注意 DMA 能否访问:D1 域 DMA 不能访问 DTCM,需用 MDMA 或放到 AXI SRAM。
  • Invalidate 操作有风险:若缓冲区有未写回数据,Invalidate 会丢弃,务必先 Clean。
  • 多缓冲区场景:每个 DMA 缓冲区都需单独配置 MPU 区域,或合并到一个大区域。
  • 调试时:可用 SCB_InvalidateDCache_by_Addr 临时验证,但正式代码建议用 MPU 方案。

五、总结

STM32H7 的 D-Cache 与 DMA 一致性问题是嵌入式开发中的经典坑。通过 MPU 将 DMA 缓冲区配置为 Non-Cacheable,可以从根源上避免数据不一致,代码简洁且稳定。记住:能用 MPU 解决的,就不要靠手动 Clean/Invalidate。希望本文的实战步骤能帮你少走弯路。