STM32H7 的 D-Cache 与 DMA 一致性:用 MPU 配置 Non-Cacheable 区域的实操步骤

一、为什么 DMA 遇上 D-Cache 会“翻车”?

STM32H7 搭载 Cortex-M7 内核,主频高达 480MHz,为了弥补 CPU 与存储器之间的速度鸿沟,芯片内部集成了 D-Cache(数据缓存)。CPU 访问数据时,会先将数据从主存(如 SRAM、SDRAM)加载到 Cache 中,后续读写直接操作 Cache,从而大幅提升性能。

然而,DMA(直接存储器访问)控制器是独立于 CPU 的总线主设备,它直接访问主存,并不知晓 Cache 的存在。这就导致了经典的缓存一致性问题:

  • CPU 写数据到 DMA 发送缓冲区:数据可能只停留在 Cache 中(写回策略),并未真正写入主存。此时启动 DMA 发送,DMA 从主存读到的是旧数据,导致发送内容错误。
  • DMA 接收数据到缓冲区:DMA 将新数据写入主存,但 CPU 读取时,若该地址的数据已在 Cache 中(旧值),CPU 会直接返回 Cache 中的旧数据,导致处理错误。

简而言之:CPU 和 DMA 看到的内存视图不一致

二、解决思路:Cache 维护 vs. MPU 配置

针对上述问题,业界有两种主流方案:

  1. Cache 维护操作:在 DMA 传输前后,手动调用 SCB_CleanDCache_by_Addr()SCB_InvalidateDCache_by_Addr() 等函数,将 Cache 数据写回主存或无效化 Cache。缺点是容易遗漏、影响性能,且需处理地址对齐问题。
  2. MPU 配置 Non-Cacheable 区域:利用 Cortex-M7 的 MPU(内存保护单元),将 DMA 使用的内存区域属性设置为 Non-Cacheable。这样 CPU 对该区域的访问将直接穿透到主存,从根本上避免一致性问题。此方案一劳永逸,是 ST 官方推荐的做法。

本文将重点介绍第二种方案,并给出在 STM32H7 上使用 HAL 库配置 MPU 的完整步骤。

三、MPU 配置实操步骤

3.1 确定 DMA 缓冲区地址与大小

假设我们使用 uint8_t dma_buffer[1024] 作为 DMA 收发缓冲区,定义在 0x24000000(AXI SRAM)起始的地址。实际项目中,建议使用链接脚本或 __attribute__((section(".dma_buffer"))) 将缓冲区放到特定区域,便于 MPU 管理。

3.2 配置 MPU 区域

Cortex-M7 的 MPU 支持最多 16 个区域。我们选择一个未使用的区域(如 Region 0),配置其基地址、大小和属性。关键属性:

  • TEX = 0b001, C = 0, B = 0:定义为 Normal memory, Non-cacheable。
  • AP = 0b011:Full access(根据需求调整)。
  • XN = 0:允许执行(若缓冲区不存放代码,可设为 1 增强安全性)。

3.3 代码实现

以下代码基于 STM32H7 HAL 库,展示如何配置 MPU 并使能。

#include "stm32h7xx_hal.h"

// DMA 缓冲区定义,强制对齐到 32 字节(Cache line 大小)
aligned(32) uint8_t dma_buffer[1024];

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

    // 禁用 MPU
    HAL_MPU_Disable();

    // 配置 Region 0 为 Non-Cacheable
    MPU_InitStruct.Enable           = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress      = (uint32_t)dma_buffer; // 基地址需 32 字节对齐
    MPU_InitStruct.Size             = MPU_REGION_SIZE_1KB;  // 根据缓冲区大小调整
    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_LEVEL1; // TEX=001
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.DisableExec      = MPU_INSTRUCTION_ACCESS_DISABLE; // 禁止执行

    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    // 使能 MPU
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

关键点说明

  • BaseAddress 必须与 Size 对齐(例如 1KB 区域基地址需 1KB 对齐)。
  • Size 需覆盖整个缓冲区,且为 2 的幂次方。
  • TypeExtField 设置为 MPU_TEX_LEVEL1,配合 IsCacheable=0IsBufferable=0,实现 Non-Cacheable 属性。
  • 若使用 SCB_EnableDCache(),务必在 MPU 配置之后调用,否则可能触发异常。

3.4 在 main 中调用

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

    MPU_Config();          // 先配置 MPU
    SCB_EnableICache();    // 使能 I-Cache
    SCB_EnableDCache();    // 使能 D-Cache

    // 后续初始化 DMA 等
    ...
}

四、注意事项与避坑指南

  • 对齐问题:DMA 缓冲区地址和大小必须按 Cache line(32 字节)对齐,否则即使配置了 Non-Cacheable,相邻数据仍可能被缓存,导致意外。
  • MPU 区域重叠:确保新配置的区域不与已有区域重叠,否则行为未定义。可使用 HAL_MPU_GetRegionConfig() 检查。
  • 性能影响:Non-Cacheable 区域访问速度较慢,仅将 DMA 缓冲区设为 Non-Cacheable,其他内存保持 Cacheable 以平衡性能。
  • 多缓冲区管理:若使用多个 DMA 缓冲区,可合并到一个连续区域,或配置多个 MPU 区域。
  • 调试技巧:若 DMA 数据仍异常,检查 MPU 配置是否生效(读取 MPU->RBAR 等寄存器),并确认没有其他代码修改了 MPU。
  • Cache 维护替代方案:若无法使用 MPU(如区域数量不足),可在 DMA 传输前后调用 SCB_CleanDCache_by_Addr()SCB_InvalidateDCache_by_Addr(),但需注意地址对齐和性能开销。

五、总结

通过 MPU 将 DMA 缓冲区配置为 Non-Cacheable 区域,是解决 STM32H7 中 D-Cache 与 DMA 一致性问题的优雅方案。它避免了繁琐的 Cache 维护操作,提高了代码的可靠性和可维护性。掌握这一技能,能让你的高性能嵌入式设计更加稳健。赶紧在你的下一个 STM32H7 项目中实践吧!