一、现象:DMA 数据错乱,关 Cache 就好?

在 STM32H7 上开发以太网、USB 或 SPI 高速传输时,你是否遇到过:

  • DMA 发送的数据,接收端看到的是旧数据或随机值。
  • DMA 接收的数据,CPU 读出来部分正确、部分错乱。
  • 一旦关闭 D-Cache,一切正常;打开就出错。

这不是玄学,而是 Cache 与 DMA 的一致性(Coherency)问题。Cortex-M7 的 D-Cache 和 DMA 控制器各自独立访问内存,若没有正确配置 MPU,CPU 可能读到 Cache 中的旧数据,而 DMA 却把新数据写到了物理内存。

二、原理:为什么 Cache 会“欺骗”你?

2.1 Cache 的工作方式

  • CPU 访问内存时,先查 D-Cache。若命中,直接返回 Cache 中的数据,不访问物理内存。
  • 写操作默认是 写回(Write-Back):CPU 只写 Cache,标记为脏,稍后才写回物理内存。
  • DMA 直接访问物理内存,不经过 Cache。

2.2 典型错误场景

场景 A:DMA 发送

  1. CPU 填充缓冲区数据(此时数据在 Cache 中,物理内存还是旧值)。
  2. 启动 DMA 发送,DMA 从物理内存读数据,发送的是旧值。

场景 B:DMA 接收

  1. DMA 将新数据写入物理内存。
  2. CPU 读缓冲区,若 Cache 中已有该地址的旧数据,则读到旧值。

2.3 解决思路

  • 软件维护:手动调用 SCB_CleanDCache_by_Addr()SCB_InvalidateDCache_by_Addr()
  • 硬件配置:通过 MPU 将 DMA 缓冲区所在内存区域配置为 Non-CacheableWrite-Through,一劳永逸。

三、MPU 配置步骤(以 STM32H7 为例)

3.1 确定内存区域

假设 DMA 缓冲区放在 0x30000000(D2 SRAM),大小 32KB。

3.2 配置 MPU 区域

  • 选择空闲的 MPU Region(如 Region 0)。
  • 设置基地址、大小、属性。
  • 属性关键位:
    • TEX=0, C=0, B=0:Non-Cacheable。
    • TEX=0, C=1, B=0:Write-Through, No Write Allocate。
    • TEX=0, C=1, B=1:Write-Back, Write Allocate(默认,易出错)。
  • 使能该 Region。

3.3 完整代码示例

#include "stm32h7xx.h"

#define DMA_BUFFER_ADDR  0x30000000UL
#define DMA_BUFFER_SIZE  0x8000UL  // 32KB

void MPU_Config(void)
{
    HAL_MPU_Disable();

    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    // 配置 D2 SRAM 为 Non-Cacheable
    MPU_InitStruct.Enable           = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress      = DMA_BUFFER_ADDR;
    MPU_InitStruct.Size             = MPU_REGION_SIZE_32KB;
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.IsBufferable     = MPU_ACCESS_NOT_BUFFERABLE;
    MPU_InitStruct.IsCacheable      = MPU_ACCESS_NOT_CACHEABLE;
    MPU_InitStruct.IsShareable      = MPU_ACCESS_NOT_SHAREABLE;
    MPU_InitStruct.Number           = MPU_REGION_NUMBER0;
    MPU_InitStruct.TypeExtField     = MPU_TEX_LEVEL0;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.DisableExec      = MPU_INSTRUCTION_ACCESS_DISABLE;

    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    // 使能 MPU
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

注意:HAL_MPU_ConfigRegion 内部会处理 TEX/C/B 位,直接使用 IsCacheableIsBufferable 即可。

3.4 启动 DMA 前的额外操作

即使配置了 Non-Cacheable,若之前该区域已被 Cache 缓存,仍需先失效(Invalidate)一次:

SCB_InvalidateDCache_by_Addr((uint32_t*)DMA_BUFFER_ADDR, DMA_BUFFER_SIZE);

四、注意事项与避坑指南

  • MPU 区域对齐:基地址必须按区域大小对齐(32KB 区域基地址低 15 位为 0)。
  • 区域重叠:高编号 Region 优先级更高,避免冲突。
  • DMA 描述符:若使用链表式 DMA,描述符本身也需放在 Non-Cacheable 区域。
  • 性能权衡:Non-Cacheable 会降低 CPU 访问速度,仅对 DMA 缓冲区使用。
  • 多核/多主:STM32H7 的 MDMA、BDMA 同样受 Cache 影响,需统一处理。
  • 调试技巧:用 SCB_InvalidateDCache() 全失效可快速验证问题。

五、总结

STM32H7 的 Cache 是性能利器,但 DMA 与 Cache 的共存必须通过 MPU 或手动维护来保证一致性。推荐将 DMA 缓冲区配置为 Non-Cacheable,并在初始化时失效一次 Cache。掌握这些,你的 DMA 传输将不再“随机”出错。