STM32H7 的 Cache 与 DMA 一致性陷阱:从现象到 MPU 配置的完整排查路径

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

在 STM32H7 上使用 DMA 时,你可能会遇到以下典型问题:

  • ADC 连续采样:DMA 将 ADC 数据搬运到数组,但 CPU 读到的数据始终是第一次的值,或部分数据未更新。
  • UART 空闲中断 + DMA 接收:DMA 接收完一帧数据,CPU 读取缓冲区却发现数据错乱、缺失或全是 0。
  • SPI 发送:CPU 填充发送缓冲区后启动 DMA,实际发送出去的数据却是旧内容。
  • 以太网通信:DMA 描述符和缓冲区数据不一致,导致丢包或协议栈异常。

这些现象的共同点是:CPU 与 DMA 看到的内存内容不一致。根源在于 STM32H7 的 L1 Cache(数据缓存 D-Cache)与 DMA 直接访问物理内存之间的冲突。

二、原理:Cache 与 DMA 为何“打架”?

STM32H7 内核(Cortex-M7)带有 L1 D-Cache,用于加速 CPU 对内存的访问。CPU 读写数据时,实际操作的是 Cache 中的副本,而非直接访问 SRAM。DMA 则绕过 Cache,直接读写物理 SRAM。

  • 场景 1:DMA 写入内存,CPU 读取
    DMA 将新数据写入 SRAM,但 CPU 之前已缓存了该地址的旧数据。CPU 再次读取时,命中 Cache,得到旧值,而不是 DMA 写入的新值。

  • 场景 2:CPU 写入内存,DMA 读取
    CPU 写入数据到 Cache,但未回写到 SRAM。此时启动 DMA,DMA 从 SRAM 读取到的是旧数据,导致发送/传输错误。

  • 场景 3:Cache 行部分更新
    Cache 行大小通常为 32 字节。若 CPU 只修改了部分数据,回写时可能覆盖 DMA 已写入的其他数据,造成数据损坏。

因此,必须通过软件手段维护 Cache 与 DMA 的一致性。

三、解决方案:MPU 配置 + Cache 维护操作

3.1 总体思路

  • 将 DMA 缓冲区所在的内存区域配置为 Non-Cacheable(不缓存),彻底避免一致性问题。
  • 或者,在 DMA 传输前后手动执行 Cache 清理(Clean)无效化(Invalidate) 操作。
  • 推荐使用 MPU 将 DMA 缓冲区映射为 Non-Cacheable,简单可靠。

3.2 MPU 配置步骤

  1. 使能 MPU:SCB_EnableMPU() 或直接配置 MPU 寄存器。
  2. 选择一个空闲的 MPU 区域(如 Region 0)。
  3. 设置基地址为 DMA 缓冲区起始地址(需 32 字节对齐)。
  4. 设置区域大小为最小覆盖缓冲区的大小(如 32B、64B、...、4GB)。
  5. 设置属性:TEX=0, C=0, B=0, S=1(Shareable, Non-Cacheable)。
  6. 使能该区域。

3.3 完整代码示例

以下代码将 0x30000000 开始的 64KB 区域配置为 Non-Cacheable,用于 DMA 缓冲区(假设该区域位于 AXI SRAM 或 D2 SRAM)。

#include "stm32h7xx.h"

void MPU_Config(void)
{
    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    // 禁用 MPU
    HAL_MPU_Disable();

    // 配置 Region 0:DMA 缓冲区区域,Non-Cacheable
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = 0x30000000;  // 根据实际 SRAM 地址修改
    MPU_InitStruct.Size = MPU_REGION_SIZE_64KB;
    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_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);
}

// 在 main 初始化中调用
int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MPU_Config();  // 必须在使能 Cache 之前配置
    SCB_EnableICache();
    SCB_EnableDCache();
    // ... 其他初始化
}

关键点:MPU 配置必须在使能 D-Cache 之前完成,否则可能触发异常。

3.4 手动 Cache 维护(备选方案)

若无法使用 MPU,可在 DMA 传输前后调用 CMSIS 函数:

  • DMA 写入内存前(CPU 准备接收):SCB_InvalidateDCache_by_Addr(addr, size);
  • DMA 读取内存前(CPU 准备发送):SCB_CleanDCache_by_Addr(addr, size);
  • DMA 写入完成后(CPU 读取前):再次 SCB_InvalidateDCache_by_Addr

注意:地址和大小必须 32 字节对齐,否则可能损坏相邻数据。

四、注意事项与避坑指南

  • MPU 区域对齐:基地址和大小必须符合 MPU 对齐要求(基地址按大小对齐,大小至少 32B)。
  • Cache 维护操作要成对:Clean 用于 CPU 写后、DMA 读前;Invalidate 用于 DMA 写后、CPU 读前。顺序错误会导致数据丢失。
  • 避免部分 Cache 行操作:若缓冲区不是 Cache 行大小(32B)的整数倍,Invalidate 可能丢弃相邻数据。建议缓冲区按 32B 对齐并填充至整数倍。
  • 多缓冲区场景:为每个 DMA 缓冲区单独配置 MPU 区域,或统一放在同一 Non-Cacheable 区域。
  • 性能权衡:Non-Cacheable 区域会降低 CPU 访问速度,但 DMA 缓冲区通常访问不频繁,影响可接受。
  • 调试技巧:若怀疑 Cache 问题,可临时禁用 D-Cache 验证;使用 SCB_InvalidateDCache() 全局无效化(慎用,影响性能)。

五、总结

STM32H7 的 Cache 与 DMA 一致性问题是嵌入式开发中的经典陷阱。通过 MPU 将 DMA 缓冲区配置为 Non-Cacheable 是最简单可靠的方案;若必须使用 Cache,则需严格遵循 Clean/Invalidate 时序。理解原理、合理配置,才能充分发挥 H7 的高性能,同时保证数据正确性。