STM32H7 的 Cache 与 DMA 一致性:MPU 配置踩坑与性能实测

STM32H7 系列搭载 Cortex-M7 内核,主频高达 480MHz,配备 16KB I-Cache 和 16KB D-Cache。开启 Cache 后性能飙升,但一旦与 DMA 配合使用,数据错乱、HardFault 等问题接踵而至。本文从原理到实战,带你彻底搞懂 Cache 与 DMA 的一致性难题。

一、为什么 Cache 会导致 DMA 数据错乱?

Cortex-M7 的 D-Cache 采用写回(Write-Back)策略。CPU 写数据时,先写入 Cache,并不立即同步到 SRAM。此时若启动 DMA 将 SRAM 中的数据搬运到外设,DMA 读到的是旧数据

反之,DMA 将外设数据搬运到 SRAM 后,CPU 读取时若命中 Cache,读到的仍是Cache 中的旧值,而非 DMA 刚写入的新数据。

这就是典型的Cache 一致性(Coherency)问题

  • CPU 写 → Cache 更新,SRAM 未更新 → DMA 读 SRAM 得到旧数据
  • DMA 写 → SRAM 更新,Cache 未更新 → CPU 读 Cache 得到旧数据

二、MPU 配置:最容易踩的三个坑

坑 1:忘记配置 MPU 直接开 Cache

默认情况下,整个 0x00000000-0xFFFFFFFF 区域被映射为 Normal Cacheable。若 DMA 缓冲区落在此区域,必然出错。

坑 2:MPU 区域重叠,优先级搞反

MPU 区域编号越大优先级越高。若将 DMA 缓冲区配为 Non-Cacheable,但被更高编号的 Cacheable 区域覆盖,配置无效。

坑 3:只配了 D-Cache 属性,忽略 Shareability

对于多主设备(CPU+DMA)访问的区域,必须设置 MPU_ACCESS_SHAREABLE,否则即使 Non-Cacheable 也可能因预取导致问题。

三、三种解决方案与 MPU 配置代码

方案一:将 DMA 缓冲区配置为 Non-Cacheable(推荐)

#include "stm32h7xx_hal.h"

// 假设 DMA 缓冲区位于 D2 SRAM 0x30000000,大小 4KB
#define DMA_BUF_ADDR   0x30000000
#define DMA_BUF_SIZE   0x1000

void MPU_Config(void)
{
    HAL_MPU_Disable();

    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    // 配置 DMA 缓冲区为 Non-Cacheable, Shareable
    MPU_InitStruct.Enable           = MPU_REGION_ENABLE;
    MPU_InitStruct.Number           = MPU_REGION_NUMBER0;
    MPU_InitStruct.BaseAddress      = DMA_BUF_ADDR;
    MPU_InitStruct.Size             = MPU_REGION_SIZE_4KB;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.TypeExtField     = MPU_TEX_LEVEL0;
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.DisableExec      = MPU_INSTRUCTION_ACCESS_DISABLE;
    MPU_InitStruct.IsShareable      = MPU_ACCESS_SHAREABLE;
    MPU_InitStruct.IsCacheable      = MPU_ACCESS_NOT_CACHEABLE;
    MPU_InitStruct.IsBufferable     = MPU_ACCESS_NOT_BUFFERABLE;

    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    // 配置其余区域为 Write-Back, Write-Allocate
    MPU_InitStruct.Number           = MPU_REGION_NUMBER1;
    MPU_InitStruct.BaseAddress      = 0x24000000; // AXI SRAM
    MPU_InitStruct.Size             = MPU_REGION_SIZE_512KB;
    MPU_InitStruct.IsShareable      = MPU_ACCESS_NOT_SHAREABLE;
    MPU_InitStruct.IsCacheable      = MPU_ACCESS_CACHEABLE;
    MPU_InitStruct.IsBufferable     = MPU_ACCESS_BUFFERABLE;
    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

方案二:使用 Cache 维护操作(Clean/Invalidate)

若缓冲区必须 Cacheable,则需在 DMA 传输前后手动维护 Cache:

// DMA 发送前:将 Cache 数据写回 SRAM
SCB_CleanDCache_by_Addr((uint32_t *)DMA_BUF_ADDR, DMA_BUF_SIZE);

// DMA 接收后:无效化 Cache,强制 CPU 从 SRAM 重新读取
SCB_InvalidateDCache_by_Addr((uint32_t *)DMA_BUF_ADDR, DMA_BUF_SIZE);

注意SCB_InvalidateDCache_by_Addr 的地址必须 32 字节对齐,长度也需按 32 字节对齐,否则会误伤相邻数据。

方案三:使用 DTCM RAM(无需 MPU 配置)

DTCM(0x20000000)不经过 Cache,天然与 DMA 一致。但注意:MDMA 和部分 DMA 无法访问 DTCM,需查阅参考手册确认。

四、性能实测对比

测试平台:STM32H743 @ 400MHz,DMA 搬运 64KB 数据。

| 方案 | 耗时 (us) | CPU 占用 | 数据正确性 | |------|-----------|----------|------------| | 无 Cache + Non-Cacheable | 320 | 低 | ✅ | | 开 Cache + Non-Cacheable | 310 | 低 | ✅ | | 开 Cache + Clean/Invalidate | 285 | 中 | ✅ | | 开 Cache + 不处理 | 120 | 低 | ❌ 数据错乱 |

结论

  • Non-Cacheable 方案性能损失约 5%-10%,但最稳定
  • Clean/Invalidate 方案性能最优,但需精确控制,适合高频小数据量场景
  • 不处理 Cache 虽然最快,但数据完全不可信

五、注意事项与最佳实践

  • DMA 缓冲区地址和长度务必 32 字节对齐,否则 Cache 维护操作会破坏相邻变量
  • 避免在中断中频繁调用 Cache 维护函数,其执行时间随缓冲区增大而线性增长
  • 使用 __attribute__((aligned(32))) 修饰 DMA 缓冲区,确保对齐
  • MPU 配置必须在使能 Cache 之前完成,通常在 main() 最开始调用
  • 调试时若发现 DMA 数据随机错乱,优先检查 MPU 区域是否覆盖了 DMA 缓冲区
  • 对于以太网、USB 等高速外设,推荐使用 Non-Cacheable 方案,避免频繁 Cache 维护开销

六、总结

STM32H7 的 Cache 是把双刃剑。理解其写回机制与 MPU 的映射规则,是解决 DMA 一致性问题的关键。实际项目中,优先将 DMA 缓冲区配置为 Non-Cacheable 并设置 Shareable,可规避 90% 以上的坑。若追求极致性能,再考虑 Clean/Invalidate 方案,但务必做好对齐与边界检查。