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

STM32H7 主频高达 480MHz,为了匹配 CPU 速度,芯片内置了 L1 D-Cache(数据缓存)。CPU 访问内存时,若命中 Cache 则直接读写缓存,不会立即同步到实际 RAM。而 DMA 是独立于 CPU 的外设,它直接访问物理内存(SRAM/SDRAM),完全绕过 Cache。

这就导致两个典型问题:

  • DMA 读取时:CPU 刚写入的数据还在 Cache 里(Write-Back 模式),物理内存中仍是旧值,DMA 发送出去的是旧数据。
  • DMA 写入时:DMA 把新数据写入物理内存,但 CPU 之前读过的旧数据还在 Cache 中,CPU 再次读取时命中 Cache,拿到的是旧数据。

根本矛盾:同一块内存存在两个副本(Cache 与物理内存),且没有硬件自动同步机制。

二、解决思路:让 DMA 缓冲区“绕过”Cache

常见方案有三种:

  1. 使用 SCB_CleanDCache_by_Addr() / SCB_InvalidateDCache_by_Addr() 手动维护一致性。
  2. 将 DMA 缓冲区放在默认非缓存区域(如 DTCM,但 H7 的 DTCM 不能被大多数 DMA 访问)。
  3. 通过 MPU 将指定内存区域配置为 Non-Cacheable,一劳永逸。

方案 3 最稳定,适合频繁 DMA 传输的场景。下面详细讲解 MPU 配置的正确姿势。

三、MPU 配置 Non-Cacheable 区域步骤

3.1 确定内存区域

假设我们使用 AXI SRAM(0x24000000)中的一段 32KB 空间作为 DMA 缓冲区。注意:不要与栈、堆或已使用的区域重叠。

3.2 配置 MPU 属性

MPU 以 Region 为单位管理,每个 Region 需设置基地址、大小、访问权限和内存属性。关键属性:

  • TEX = 1, C = 0, B = 0:Normal memory, Outer and Inner Non-cacheable。
  • AP = 0b011:Full access(根据需求调整)。
  • XN = 0:允许执行(通常禁止执行更安全,可设 1)。

3.3 完整代码示例

#include "stm32h7xx.h"

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

void MPU_Config_NonCacheable(void)
{
    HAL_MPU_Disable();  // 配置前必须关闭 MPU

    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    // 选择 Region 0(0~15 任选,不冲突即可)
    MPU_InitStruct.Enable           = MPU_REGION_ENABLE;
    MPU_InitStruct.Number           = MPU_REGION_NUMBER0;
    MPU_InitStruct.BaseAddress      = DMA_BUFFER_ADDR;
    MPU_InitStruct.Size             = MPU_REGION_SIZE_32KB;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.TypeExtField     = MPU_TEX_LEVEL1;   // TEX = 1
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.DisableExec      = MPU_INSTRUCTION_ACCESS_DISABLE;
    MPU_InitStruct.IsShareable      = MPU_ACCESS_NOT_SHAREABLE;
    MPU_InitStruct.IsCacheable      = MPU_ACCESS_NOT_CACHEABLE; // C = 0
    MPU_InitStruct.IsBufferable     = MPU_ACCESS_NOT_BUFFERABLE; // B = 0

    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

注意:HAL_MPU_ConfigRegion 内部会处理 TEX/C/B 的组合,只需正确赋值 IsCacheable 和 IsBufferable 即可。

3.4 在 main 中调用

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MPU_Config_NonCacheable();  // 必须在使能 Cache 之前调用
    SCB_EnableICache();
    SCB_EnableDCache();
    // 后续初始化 DMA 并使用 DMA_BUFFER_ADDR 作为缓冲区
    ...
}

四、注意事项与常见坑

  • 顺序至关重要:MPU 配置必须在 SCB_EnableDCache() 之前完成,否则 Cache 已生效,再改 MPU 可能引发不可预知行为。
  • Region 重叠:多个 Region 重叠时,编号高的优先级高。确保 Non-Cacheable 区域没有被其他 Region 覆盖。
  • 大小与对齐:Region 大小必须是 2 的幂,且基地址按大小对齐。例如 32KB 区域基地址必须 32KB 对齐。
  • DTCM 不能用于 DMA:H7 的 DTCM 地址为 0x20000000,大部分 DMA 无法访问,不要试图将其配为 Non-Cacheable 给 DMA 用。
  • 多缓冲区场景:若多个 DMA 缓冲区分散,可合并到一个连续区域,或使用多个 MPU Region。
  • 性能权衡:Non-Cacheable 区域 CPU 访问会变慢,只把 DMA 缓冲区放进去,其他代码/数据仍保持 Cacheable。
  • 调试技巧:用 SCB_InvalidateDCache_by_Addr() 配合测试,若数据一致则说明 MPU 配置成功。

五、验证方法

写一个简单测试:CPU 向 Non-Cacheable 缓冲区写入递增数据,启动 DMA 发送到另一块内存,比较结果。若完全一致,则配置正确。也可通过查看 MPU 寄存器 MPU->RBAR 和 MPU->RASR 确认属性。

六、总结

STM32H7 的 D-Cache 与 DMA 冲突是嵌入式开发中的经典陷阱。通过 MPU 将 DMA 缓冲区配置为 Non-Cacheable,可以从根源上避免数据一致性问题,且无需在每次传输前后手动清理/无效化 Cache。牢记配置顺序、区域对齐和权限设置,你就能安心享受 H7 的高性能与 DMA 的高效率。