STM32H7 的 DCache 与 DMA 一致性:MPU 配置与 clean/invalidate 的实战避坑

STM32H7 系列搭载 Cortex-M7 内核,主频高达 480MHz,并配备了 L1 数据缓存(DCache)。开启 DCache 后,CPU 访问频繁的数据会缓存在 Cache 中,大幅提升性能。然而,DMA 控制器直接访问物理内存,不经过 Cache,这就导致了经典的 Cache 一致性 问题:CPU 写入的数据可能还在 Cache 中未写回内存,DMA 读取到旧数据;或者 DMA 写入的新数据被 CPU 的 Cache 旧副本覆盖。

本文将从原理出发,手把手教你配置 MPU 并正确使用 clean/invalidate 操作,彻底解决这一痛点。

一、为什么会出现一致性问题?

Cortex-M7 的 DCache 采用写回(Write-Back) 策略。当 CPU 写数据时,若 Cache 命中,只更新 Cache 而不立即写回内存;当 CPU 读数据时,若 Cache 命中,直接返回 Cache 内容而不读内存。

DMA 传输则直接读写物理内存。因此:

  • CPU 写 → DMA 读:CPU 写入的数据可能还在 Cache 中,DMA 读到旧内存数据。
  • DMA 写 → CPU 读:DMA 写入新数据到内存,但 CPU 的 Cache 中可能还保留着旧数据,CPU 读到旧值。

解决思路有两种:

  1. 将 DMA 缓冲区所在内存区域配置为 Non-Cacheable(通过 MPU)。
  2. 在 DMA 传输前后,手动执行 Clean(写回)和 Invalidate(无效化)操作。

二、MPU 配置:将 DMA 缓冲区设为 Non-Cacheable

最稳妥的方法是利用 MPU 将 DMA 缓冲区所在的内存区域(如 SRAM1、SRAM2 或 SDRAM 的某段)配置为 Non-Cacheable。这样 CPU 和 DMA 都直接访问物理内存,无需额外维护操作。

2.1 MPU 配置步骤

  1. 使能 MPU:SCB->SHCSR |= SCB_SHCSR_MEMFAULTENA_Msk;(可选,用于调试)
  2. 禁用 MPU:MPU->CTRL = 0;
  3. 配置 Region:设置基地址、属性、大小。
  4. 使能 Region:MPU->CTRL |= MPU_CTRL_ENABLE_Msk;
  5. 使能 Cache:SCB_EnableICache(); SCB_EnableDCache();

2.2 完整 MPU 配置代码

#include "stm32h7xx.h"

// 将地址 0x30000000 开始的 64KB 区域配置为 Non-Cacheable
void MPU_Config(void)
{
    HAL_MPU_Disable();

    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    // 配置 Region 0
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.Number = MPU_REGION_NUMBER0;
    MPU_InitStruct.BaseAddress = 0x30000000;
    MPU_InitStruct.Size = MPU_REGION_SIZE_64KB;
    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);

    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

注意:Region 的基地址必须按大小对齐(如 64KB 区域基地址需 64KB 对齐)。

三、Clean 与 Invalidate 操作时机

如果无法将缓冲区设为 Non-Cacheable(例如使用外部 SDRAM 且性能要求高),则必须在 DMA 传输前后手动维护 Cache。

3.1 关键函数

  • SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize):将 Cache 中已修改的数据写回内存。
  • SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize):丢弃 Cache 中的内容,强制下次读取从内存加载。
  • SCB_CleanInvalidateDCache_by_Addr():先 Clean 再 Invalidate。

3.2 操作原则

  • CPU 写 → DMA 读:在启动 DMA 前执行 Clean
  • DMA 写 → CPU 读:在 DMA 完成后执行 Invalidate
  • 双向传输:先 Clean,DMA 完成后再 Invalidate。

3.3 代码示例

#define DMA_BUF_SIZE 1024
__attribute__((aligned(32))) uint8_t dma_tx_buf[DMA_BUF_SIZE];
__attribute__((aligned(32))) uint8_t dma_rx_buf[DMA_BUF_SIZE];

// 发送前:Clean
void DMA_SendData(void)
{
    // 填充数据到 dma_tx_buf
    for (int i = 0; i < DMA_BUF_SIZE; i++) {
        dma_tx_buf[i] = i & 0xFF;
    }

    // 关键:将 Cache 写回内存
    SCB_CleanDCache_by_Addr((uint32_t *)dma_tx_buf, DMA_BUF_SIZE);

    // 启动 DMA 传输
    HAL_DMA_Start(&hdma_memtomem, (uint32_t)dma_tx_buf, (uint32_t)dest, DMA_BUF_SIZE);
}

// 接收完成后:Invalidate
void DMA_ReceiveCompleteCallback(void)
{
    // 关键:无效化 Cache,确保 CPU 读取到 DMA 写入的新数据
    SCB_InvalidateDCache_by_Addr((uint32_t *)dma_rx_buf, DMA_BUF_SIZE);

    // 现在可以安全读取 dma_rx_buf
    process_data(dma_rx_buf, DMA_BUF_SIZE);
}

四、实战避坑指南

  • 地址对齐SCB_CleanDCache_by_Addr 的地址和大小建议 32 字节对齐,否则可能影响相邻数据。
  • 缓冲区大小:Invalidate 的大小必须覆盖 DMA 实际写入的范围,不能多也不能少。
  • 中断中调用:Clean/Invalidate 操作耗时,避免在高速中断中频繁调用。
  • 多缓冲区:若使用多个 DMA 缓冲区,确保每个缓冲区独立对齐并单独维护。
  • MPU 与手动维护二选一:不要既配置 Non-Cacheable 又手动 Clean/Invalidate,浪费性能。
  • 调试时关闭 Cache:初期调试可先关闭 DCache,确认逻辑正确后再开启并处理一致性问题。

五、总结

STM32H7 的 DCache 是性能利器,但必须谨慎处理与 DMA 的一致性。推荐优先使用 MPU 将 DMA 缓冲区设为 Non-Cacheable,简单可靠;若必须使用 Cacheable 内存,则严格遵循“写前 Clean,读后 Invalidate”的原则,并注意地址对齐和操作范围。掌握这些技巧,你就能在 H7 上既享受 Cache 的高速,又保证 DMA 数据的正确性。