为什么 H7 的 Cache 会成为 DMA 的“猪队友”?

STM32H7 的 Cortex-M7 内核包含 L1 数据 Cache(D-Cache)指令 Cache(I-Cache),通常 D-Cache 大小为 16KB 或 32KB。Cache 的存在让 CPU 访问频繁使用的数据时无需每次都读 AXI SRAM,从而大幅提升性能。

但 DMA 是“独立于 CPU 的总线主设备”,它不经过 Cache,直接读写物理内存。这就导致两个经典问题:

  • DMA 写内存,CPU 读旧值:DMA 把新数据写入 SRAM,但 CPU 之前已把该地址的旧数据缓存到 D-Cache,CPU 读到的仍是旧值。
  • CPU 写内存,DMA 读旧值:CPU 写数据到 D-Cache(写回策略下未立即写入 SRAM),DMA 启动后从 SRAM 读到的是旧数据。

解决思路只有两条:要么让 DMA 缓冲区不经过 Cache(MPU 配置为 Write-Through/Non-Cacheable),要么在 DMA 传输前后手动维护 Cache 一致性(Clean/Invalidate)

方法一:MPU 配置 DMA 缓冲区为 Non-Cacheable

最省心的做法是把 DMA 使用的内存区域通过 MPU 配置为 Non-CacheableWrite-Through,这样 CPU 和 DMA 看到的内存始终一致。

配置步骤

  1. 在链接脚本中划分一块专用 RAM 区域(如 0x30000000 开始的 32KB),或直接使用 AXI SRAM 的某段地址。
  2. main() 初始化阶段调用 MPU 配置函数。
  3. 使能 MPU 和 D-Cache。

完整代码示例

#include "stm32h7xx.h"

/* 假设 DMA 缓冲区放在 0x30000000,大小 32KB */
#define DMA_BUFFER_ADDR  0x30000000UL
#define DMA_BUFFER_SIZE  (32UL * 1024UL)

void MPU_Config(void)
{
    HAL_MPU_Disable();

    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    /* 配置 DMA 缓冲区区域为 Non-Cacheable, Non-Shared */
    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_LEVEL0;
    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;   /* 关键 */
    MPU_InitStruct.IsBufferable     = MPU_ACCESS_NOT_BUFFERABLE;  /* 关键 */

    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_BUFFER_ADDR 区域读写均不经过 Cache */
    while (1) { }
}

注意:MPU 配置必须在使能 D-Cache 之前完成,否则可能因 Cache 已缓存旧数据而出错。

方法二:手动 Clean / Invalidate 维护一致性

如果 DMA 缓冲区必须放在可 Cache 区域(如普通 AXI SRAM),则需要在 DMA 传输前后手动操作 Cache。

核心 API

  • SCB_CleanDCache_by_Addr(addr, size):将 D-Cache 中已修改的数据写回 SRAM。用于 CPU 写、DMA 读之前
  • SCB_InvalidateDCache_by_Addr(addr, size):丢弃 D-Cache 中对应地址的缓存行,强制下次从 SRAM 读取。用于 DMA 写、CPU 读之前
  • SCB_CleanInvalidateDCache_by_Addr(addr, size):先写回再无效化,用于双向传输。

典型场景代码

#define BUF_SIZE  1024
__attribute__((aligned(32))) uint8_t dma_buf[BUF_SIZE];

/* 场景1:CPU 填充数据,DMA 发送 */
void DMA_Send(void)
{
    for (int i = 0; i < BUF_SIZE; i++) {
        dma_buf[i] = i & 0xFF;
    }
    /* 写回 D-Cache,确保 SRAM 中数据最新 */
    SCB_CleanDCache_by_Addr((uint32_t *)dma_buf, BUF_SIZE);

    HAL_DMA_Start(&hdma, (uint32_t)dma_buf, (uint32_t)&UART->TDR, BUF_SIZE);
}

/* 场景2:DMA 接收数据,CPU 读取 */
void DMA_Receive(void)
{
    HAL_DMA_Start(&hdma, (uint32_t)&UART->RDR, (uint32_t)dma_buf, BUF_SIZE);
    /* 等待 DMA 完成(中断或轮询) */
    while (HAL_DMA_PollForTransfer(&hdma, HAL_DMA_FULL_TRANSFER, 1000) != HAL_OK);

    /* 无效化 D-Cache,丢弃旧缓存,强制从 SRAM 读新数据 */
    SCB_InvalidateDCache_by_Addr((uint32_t *)dma_buf, BUF_SIZE);

    for (int i = 0; i < BUF_SIZE; i++) {
        process(dma_buf[i]);
    }
}

避坑要点与注意事项

  • 地址与大小必须 32 字节对齐:Cortex-M7 的 Cache 行大小为 32 字节,SCB_*DCache_by_Addr 的地址和长度都必须是 32 的倍数,否则会误伤相邻数据。建议缓冲区用 __attribute__((aligned(32))) 修饰。
  • Invalidate 前先 Clean:如果缓冲区中既有 CPU 写入的数据,又有 DMA 要写入的区域,直接 Invalidate 会丢失 CPU 未写回的数据。应使用 SCB_CleanInvalidateDCache_by_Addr
  • 不要对栈上变量做 DMA:栈变量地址不固定且可能与其他数据共享 Cache 行,极易出错。DMA 缓冲区应使用全局数组或静态分配。
  • 中断中避免 Cache 操作:Cache 维护操作耗时且可能被中断打断,建议在 DMA 完成中断中仅置标志,在主循环中执行 Invalidate。
  • MPU 配置与链接脚本一致:如果使用自定义段,确保链接脚本中的地址与 MPU 配置的基地址一致,否则 MPU 不生效。
  • 调试时关闭 Cache:在排查诡异数据错误时,可先关闭 D-Cache 验证是否为一致性问题,再逐步开启。

总结

STM32H7 的 Cache 与 DMA 一致性问题是嵌入式开发中的经典“深坑”。优先推荐 MPU 将 DMA 缓冲区配置为 Non-Cacheable,简单可靠;若必须使用 Cache 区域,则严格遵循“CPU 写后 Clean、DMA 写后 Invalidate”的原则,并注意 32 字节对齐和 Clean/Invalidate 顺序。掌握这两招,你就能在 H7 上放心地同时享受高主频与 DMA 高效传输。