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

STM32H7 主频高达 480MHz,为了匹配内核速度,Cortex-M7 引入了 D-Cache(数据缓存)。CPU 读写数据时,实际访问的是 Cache,而不是直接访问 SRAM 或外设。这带来了性能飞跃,但也埋下了隐患。

DMA 是“绕过 CPU”的数据搬运工,它直接访问物理内存。当 CPU 和 DMA 同时操作同一块内存时,就会出现:

  • CPU 写数据到 Cache,但未写回内存:DMA 从内存读到的还是旧数据。
  • DMA 将新数据写入内存,但 CPU 读的是 Cache 中的旧数据:CPU 看不到更新。

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

二、解决思路:三种武器

  1. 禁用 D-Cache:简单粗暴,但性能损失巨大,不推荐。
  2. 手动维护 Cache:使用 SCB_CleanDCache_by_Addr()SCB_InvalidateDCache_by_Addr()。灵活但容易遗漏,且地址必须 32 字节对齐。
  3. MPU 配置 Non-Cacheable 区域:将 DMA 缓冲区所在的内存区域配置为“不缓存”,一劳永逸。这是最推荐的做法

三、MPU 配置 Non-Cacheable 区域:正确姿势

MPU(Memory Protection Unit)可以将某段内存属性设置为 Normal Non-Cacheable,这样 CPU 访问该区域时不会经过 D-Cache,DMA 和 CPU 看到的数据始终一致。

3.1 关键配置步骤

  • 确定 DMA 缓冲区地址和大小(建议放在 D2 SRAM 或 AXI SRAM 中)。
  • 配置 MPU 区域,属性设为 MPU_ACCESS_NOT_CACHEABLEMPU_ACCESS_NOT_BUFFERABLEMPU_ACCESS_SHAREABLE
  • 使能 MPU 和 D-Cache。
  • 注意地址和大小必须按 MPU 粒度对齐(通常 32 字节)。

3.2 完整代码示例

#include "stm32h7xx.h"

// 假设 DMA 缓冲区放在 D2 SRAM 中,地址 0x30000000,大小 4KB
#define DMA_BUFFER_ADDR  0x30000000
#define DMA_BUFFER_SIZE  0x1000

void MPU_Config(void)
{
    HAL_MPU_Disable();

    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    // 配置 D2 SRAM 区域为 Non-Cacheable, Shareable
    MPU_InitStruct.Enable           = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress      = DMA_BUFFER_ADDR;
    MPU_InitStruct.Size             = MPU_REGION_SIZE_4KB; // 必须覆盖整个缓冲区
    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_ENABLE;

    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    // 使能 MPU
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MPU_Config();          // 先配置 MPU
    SCB_EnableICache();    // 使能 I-Cache
    SCB_EnableDCache();    // 使能 D-Cache

    // 初始化 DMA 缓冲区
    uint8_t *dma_buf = (uint8_t *)DMA_BUFFER_ADDR;
    for (int i = 0; i < DMA_BUFFER_SIZE; i++) {
        dma_buf[i] = 0;
    }

    // 启动 DMA 传输...
    while (1) {}
}

四、踩坑记录:那些年我们掉过的坑

坑 1:MPU 区域大小与基地址不对齐

MPU 要求基地址按区域大小对齐。例如 4KB 区域,基地址必须是 4KB 的整数倍。否则配置无效,甚至触发 HardFault。

解决:使用 __attribute__((aligned(32))) 或链接脚本指定对齐。

坑 2:忘记使能 MPU 或 D-Cache

顺序很重要:先配置 MPU,再使能 D-Cache。如果先使能 D-Cache,之前访问过的数据可能已被缓存,后续 MPU 配置不会自动清理。

解决:在 main() 最开始调用 MPU_Config(),再调用 SCB_EnableDCache()

坑 3:DMA 描述符或缓冲区跨越了多个 MPU 区域

如果 DMA 缓冲区跨越了两个 MPU 区域,且其中一个区域是 Cacheable,则仍会出现一致性问题。

解决:确保整个缓冲区位于同一个 Non-Cacheable 区域内,或使用多个 MPU 区域覆盖。

坑 4:以太网 DMA 描述符未配置 Non-Cacheable

STM32H7 的以太网 DMA 描述符必须放在 Non-Cacheable 区域,否则会出现“发送正常但接收丢包”的诡异现象。

解决:将描述符和缓冲区都放在同一 Non-Cacheable 区域,或使用 SCB_CleanInvalidateDCache() 手动维护。

坑 5:使用 SCB_InvalidateDCache 导致数据丢失

SCB_InvalidateDCache_by_Addr() 会丢弃 Cache 中未写回的数据。如果 CPU 刚写过数据但未 Clean,直接 Invalidate 会丢失数据。

解决:先 Clean 再 Invalidate,或直接使用 Non-Cacheable 区域避免手动维护。

五、总结与建议

  • 优先使用 MPU 配置 Non-Cacheable 区域,这是最稳妥的方案。
  • 如果必须使用 Cacheable 内存,务必在 DMA 传输前后正确调用 Clean/Invalidate 函数。
  • 地址对齐、区域覆盖、配置顺序是三大关键点。
  • 调试时可用 SCB_GetDCacheState() 确认 D-Cache 状态,用逻辑分析仪或调试器观察内存数据。

搞定 D-Cache 与 DMA 的一致性,STM32H7 才能真正发挥其高性能优势。希望本文能帮你少走弯路,顺利通关!