一、为什么 STM32H7 的 Cache 会坑到 DMA?

STM32H7 的 Cortex-M7 内核带有 L1 D-Cache 和 I-Cache,默认开启。CPU 访问内存时,数据可能只停留在 Cache 中,并未写回 SRAM。而 DMA 控制器直接访问物理内存,它看不到 Cache 里的“脏数据”。

典型翻车场景:

  • CPU 写入缓冲区后启动 DMA 发送,DMA 读到的却是旧数据。
  • DMA 接收数据到内存,CPU 读到的却是 Cache 中的旧内容。
  • 使用 SCB_InvalidateDCache 后,未回写的脏数据被直接丢弃,导致数据损坏。

根本原因:Cache 与 DMA 的可见性不一致。解决手段有两种:MPU 配置为 Write-Through/Non-Cacheable,或手动执行 Clean/Invalidate。

二、MPU 配置:从源头规避一致性问题

对于 DMA 缓冲区,推荐用 MPU 将其配置为 Non-CacheableWrite-Through,这样 CPU 和 DMA 看到的内存始终一致。

2.1 配置步骤

  1. 使能 MPU:SCB_EnableMPU() 前先 SCB_EnableICache()SCB_EnableDCache()
  2. 定义 MPU 区域,设置基地址、大小、属性。
  3. 调用 HAL_MPU_Config() 并使能。

2.2 完整代码示例

#include "stm32h7xx_hal.h"

// 假设 DMA 缓冲区位于 D2 SRAM,地址 0x30000000,大小 32KB
#define DMA_BUF_ADDR   0x30000000
#define DMA_BUF_SIZE   0x8000

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

    HAL_MPU_Disable();

    // 配置 DMA 缓冲区区域为 Non-Cacheable, Non-Shared
    MPU_InitStruct.Enable           = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress      = DMA_BUF_ADDR;
    MPU_InitStruct.Size             = MPU_REGION_SIZE_32KB;
    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_LEVEL0;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.DisableExec      = MPU_INSTRUCTION_ACCESS_DISABLE;

    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

注意:MPU 区域大小必须是 2 的幂,且基地址对齐到该大小。

三、手动 Clean/Invalidate:灵活但易错

若无法使用 MPU(如缓冲区分散),必须手动维护 Cache。

3.1 核心 API

  • SCB_CleanDCache_by_Addr(addr, size):将 Cache 脏数据写回内存。
  • SCB_InvalidateDCache_by_Addr(addr, size):丢弃 Cache 内容,强制从内存重读。
  • SCB_CleanInvalidateDCache_by_Addr(addr, size):先写回再无效。

3.2 正确使用时机

  • CPU 写 → DMA 读:先 Clean,再启动 DMA。
  • DMA 写 → CPU 读:DMA 完成后 Invalidate,再读数据。
  • DMA 写 → CPU 改 → DMA 读:先 Invalidate,改完再 Clean

3.3 代码示例:UART DMA 接收

#define RX_BUF_SIZE 128
alignas(32) uint8_t rx_buf[RX_BUF_SIZE]; // 32 字节对齐

void Start_DMA_Receive(void)
{
    // 接收前无效化,避免 Cache 旧数据干扰
    SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, RX_BUF_SIZE);
    HAL_UART_Receive_DMA(&huart1, rx_buf, RX_BUF_SIZE);
}

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    // 接收完成后再次无效化,确保 CPU 读到 DMA 写入的新数据
    SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, RX_BUF_SIZE);
    // 处理数据...
}

四、避坑清单与注意事项

  • 地址对齐:Clean/Invalidate 的地址和大小必须 32 字节对齐,否则行为未定义。
  • 不要过度无效化:Invalidate 会丢弃未回写的脏数据,若 CPU 刚写过缓冲区,必须先 Clean。
  • DMA 传输中禁止操作 Cache:否则可能破坏正在传输的数据。
  • MPU 配置后仍需注意:若区域配置为 Write-Through,CPU 写操作会立即写内存,但读仍可能缓存,必要时配合 Invalidate。
  • 多缓冲区场景:每个缓冲区单独维护,避免交叉污染。
  • 使用 __DSB()__ISB():在 Cache 操作后插入屏障指令,确保操作完成。

五、总结

STM32H7 的 Cache 与 DMA 一致性没有银弹。首选 MPU 将 DMA 缓冲区设为 Non-Cacheable,简单可靠;若必须使用 Cache,则严格遵循“写后 Clean,读前 Invalidate”的原则,并注意对齐与屏障。掌握这些,你就能避开 90% 的 H7 数据错乱坑。