STM32H7 的 D-Cache 与 DMA 一致性坑:从 Cache 维护 API 到 MPU 配置的完整避坑指南

STM32H7 系列凭借 480MHz 主频和 Cortex-M7 内核,成为高性能嵌入式应用的首选。但很多开发者开启 D-Cache 后,DMA 传输的数据会莫名其妙地错乱——发送的数据不对、接收的数据丢失。这背后是 Cache 与 DMA 的数据一致性问题。本文从原理到实践,带你彻底填平这个坑。

一、为什么会有 Cache 一致性问题?

Cortex-M7 的 D-Cache 位于 CPU 和主存之间。CPU 访问数据时,会先把数据从主存加载到 Cache,后续读写都直接操作 Cache,主存中的数据可能还是旧的。

  • DMA 写内存(如外设接收数据):DMA 直接把数据写入主存,但 CPU 可能仍从 Cache 读取旧数据。
  • DMA 读内存(如外设发送数据):CPU 把新数据写入 Cache,但尚未写回主存,DMA 读到的是旧数据。

这就是一致性问题。解决方式有两种:软件维护 Cache配置 MPU 将 DMA 缓冲区设为 Non-Cacheable

二、Cache 维护 API 的正确用法

CMSIS 提供了 SCB_CleanDCache_by_AddrSCB_InvalidateDCache_by_Addr 等函数。关键点:

  • Clean:将 Cache 中已修改的数据写回主存。用于 CPU 写、DMA 读 的场景(发送前调用)。
  • Invalidate:丢弃 Cache 中的旧数据,强制从主存重新加载。用于 DMA 写、CPU 读 的场景(接收后调用)。
  • 地址必须 32 字节对齐,长度建议向上取整到 32 字节的倍数,否则可能误伤相邻数据。
// 发送前:确保 CPU 写入的数据到达主存
SCB_CleanDCache_by_Addr((uint32_t*)tx_buf, sizeof(tx_buf));
HAL_UART_Transmit_DMA(&huart1, tx_buf, sizeof(tx_buf));

// 接收后:丢弃 Cache 旧数据,从主存重新加载
HAL_UART_Receive_DMA(&huart1, rx_buf, sizeof(rx_buf));
// 等待传输完成...
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, sizeof(rx_buf));

注意:Invalidate 会丢弃未写回的数据,若缓冲区同时被 CPU 写过,应先 CleanInvalidate

三、MPU 配置:一劳永逸的方案

频繁调用维护 API 既繁琐又影响性能。更好的做法是用 MPU 将 DMA 缓冲区所在的内存区域配置为 Non-Cacheable,让硬件自动保证一致性。

以 STM32H7 为例,将 0x30000000 开始的 32KB(D2 SRAM)配置为 Non-Cacheable:

void MPU_Config(void)
{
    HAL_MPU_Disable();

    MPU_Region_InitTypeDef MPU_InitStruct = {0};
    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;
    MPU_InitStruct.IsShareable      = MPU_ACCESS_SHAREABLE;
    MPU_InitStruct.Number           = MPU_REGION_NUMBER0;
    MPU_InitStruct.TypeExtField     = MPU_TEX_LEVEL1;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.DisableExec      = MPU_INSTRUCTION_ACCESS_DISABLE;

    HAL_MPU_ConfigRegion(&MPU_InitStruct);
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

配置后,DMA 缓冲区直接读写主存,无需任何 Cache 维护操作。

四、完整示例:UART DMA 收发

#include "stm32h7xx_hal.h"

#define BUF_SIZE 64
// 将缓冲区放到 Non-Cacheable 区域(0x30000000)
__attribute__((section(".RAM_D2"))) uint8_t rx_buf[BUF_SIZE];
__attribute__((section(".RAM_D2"))) uint8_t tx_buf[BUF_SIZE];

void UART_DMA_Init(void)
{
    MPU_Config();  // 先配置 MPU
    // ... UART 和 DMA 初始化代码
    HAL_UART_Receive_DMA(&huart1, rx_buf, BUF_SIZE);
}

void Send_Data(void)
{
    for (int i = 0; i < BUF_SIZE; i++) tx_buf[i] = i;
    // 若缓冲区为 Cacheable,需 Clean;Non-Cacheable 则无需
    HAL_UART_Transmit_DMA(&huart1, tx_buf, BUF_SIZE);
}

五、避坑注意事项

  • 地址对齐:Cache 维护 API 要求地址 32 字节对齐,长度建议为 32 的倍数。
  • MPU 区域大小:必须为 2 的幂,且基地址对齐到区域大小。
  • DMA 描述符:若使用链表模式,描述符本身也应放在 Non-Cacheable 区域。
  • 多缓冲区:每个 DMA 缓冲区都要单独处理,不能只维护一个。
  • 性能权衡:Non-Cacheable 区域访问速度较慢,仅将 DMA 缓冲区放入,其他数据仍用 Cache。
  • 调试技巧:若数据错乱,先检查是否忘记 Clean/Invalidate,或用 __DSB() 确保操作完成。

掌握这些要点,你就能在 STM32H7 上放心使用 D-Cache 和 DMA,充分发挥芯片性能。