STM32H7 的 Cache 与 DMA 一致性坑:MPU 配置与 Clean/Invalidate 的实战顺序

STM32H7 系列凭借 Cortex-M7 内核和 L1 Cache(I-Cache/D-Cache)实现了高主频下的优异性能,但 Cache 的引入也让 DMA 传输变得“危机四伏”。许多开发者都遇到过 DMA 发送的数据不对、接收的数据被覆盖等问题,根源往往在于 Cache 与 DMA 的一致性没有处理好。本文将带你理清原理,并给出 MPU 配置与 Clean/Invalidate 的实战顺序。

一、为什么 Cache 会导致 DMA 数据不一致?

Cortex-M7 的 D-Cache 是写回(Write-Back)写分配(Write-Allocate) 的。当 CPU 写数据时,数据先写入 Cache,并标记为“脏(Dirty)”,并不会立即写回 SRAM。如果此时启动 DMA 发送,DMA 直接从 SRAM 读取,读到的就是旧数据。

同理,DMA 接收数据时直接写入 SRAM,而 CPU 可能仍从 Cache 中读取旧数据,导致“看不到”新数据。

  • CPU 写 → DMA 读:需要将 Cache 中脏数据 Clean(写回) 到 SRAM。
  • DMA 写 → CPU 读:需要将 Cache 中对应区域 Invalidate(无效化),强制 CPU 从 SRAM 重新加载。

二、MPU 配置:为 DMA 缓冲区划定“安全区”

最稳妥的做法是利用 MPU 将 DMA 缓冲区配置为 Non-CacheableWrite-Through,从根源上避免一致性问题。但并非所有场景都适合,例如需要 CPU 频繁访问的大缓冲区,Non-Cacheable 会降低性能。

2.1 MPU 配置步骤(以 Non-Cacheable 为例)

  1. 使能 MPU:SCB->CTRL |= SCB_CTRL_MPU_Msk;
  2. 配置 Region 基地址和属性。
  3. 使能 Region。
  4. 使能 MPU 和 D-Cache。
#include "stm32h7xx.h"

void MPU_Config(void)
{
    HAL_MPU_Disable();

    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    // 假设 DMA 缓冲区在 0x30000000 (SRAM1),大小 32KB
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = 0x30000000;
    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; // 关键:Non-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);
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

注意:MPU 配置必须在使能 D-Cache 之前完成,且 Region 不能重叠。

三、Clean/Invalidate 的实战顺序

如果缓冲区是 Cacheable 的,就必须在 DMA 传输前后手动维护 Cache。顺序至关重要,弄反会导致数据丢失或性能下降。

3.1 DMA 发送(Memory → Peripheral)

  1. CPU 填充缓冲区数据。
  2. Clean 缓冲区对应的 D-Cache 行(写回 SRAM)。
  3. 启动 DMA 发送。
  4. 等待 DMA 完成。
#define DMA_BUF_SIZE 1024
uint8_t dma_tx_buf[DMA_BUF_SIZE] __attribute__((aligned(32)));

void DMA_Send_Data(void)
{
    // 1. 填充数据
    for (int i = 0; i < DMA_BUF_SIZE; i++) {
        dma_tx_buf[i] = i & 0xFF;
    }

    // 2. Clean D-Cache
    SCB_CleanDCache_by_Addr((uint32_t *)dma_tx_buf, DMA_BUF_SIZE);

    // 3. 启动 DMA
    HAL_DMA_Start(&hdma_memtomem, (uint32_t)dma_tx_buf, (uint32_t)&UART->TDR, DMA_BUF_SIZE);
}

3.2 DMA 接收(Peripheral → Memory)

  1. Invalidate 缓冲区对应的 D-Cache 行(丢弃旧数据)。
  2. 启动 DMA 接收。
  3. 等待 DMA 完成。
  4. CPU 读取数据(此时 Cache 未命中,从 SRAM 加载新数据)。
uint8_t dma_rx_buf[DMA_BUF_SIZE] __attribute__((aligned(32)));

void DMA_Receive_Data(void)
{
    // 1. Invalidate D-Cache
    SCB_InvalidateDCache_by_Addr((uint32_t *)dma_rx_buf, DMA_BUF_SIZE);

    // 2. 启动 DMA 接收
    HAL_DMA_Start(&hdma_memtomem, (uint32_t)&UART->RDR, (uint32_t)dma_rx_buf, DMA_BUF_SIZE);

    // 3. 等待完成
    HAL_DMA_PollForTransfer(&hdma_memtomem, HAL_DMA_FULL_TRANSFER, 1000);

    // 4. 读取数据
    process_data(dma_rx_buf, DMA_BUF_SIZE);
}

3.3 双向传输(如 SPI 全双工)

顺序:先 Invalidate RX 缓冲区,再 Clean TX 缓冲区,然后启动 DMA。

SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, len);
SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, len);
HAL_SPI_TransmitReceive_DMA(&hspi, tx_buf, rx_buf, len);

四、注意事项与常见坑

  • 地址对齐SCB_CleanDCache_by_Addr 要求地址 32 字节对齐,长度建议为 32 的倍数。使用 __attribute__((aligned(32))) 修饰缓冲区。
  • 不要跨区域操作:如果缓冲区跨越了多个 MPU Region,维护操作可能不完整。
  • Invalidate 前必须确保 DMA 已停止:否则可能丢弃 DMA 刚写入但未及时更新的数据。
  • 避免频繁 Clean/Invalidate:会严重影响性能,尽量用 Non-Cacheable 区域。
  • DMA 描述符:如果使用链表模式,描述符本身也需注意 Cache 一致性。
  • 中断中调用SCB_CleanDCache_by_Addr 等函数执行时间较长,避免在高速中断中频繁调用。

五、总结

STM32H7 的 Cache 与 DMA 一致性问题是高性能应用的“必修课”。核心原则:

  • 能不用 Cacheable 就不用,优先 MPU 配置为 Non-Cacheable。
  • 必须用时,严格遵循:发送前 Clean,接收前 Invalidate。
  • 顺序不能错,否则数据丢失或读到旧值。

掌握这些,你就能在 STM32H7 上既享受 Cache 的性能,又保证 DMA 的可靠。