STM32H7 的 D-Cache 与 DMA 一致性排查:从 MPU 配置到 Clean/Invalidate 的实战避坑

STM32H7 系列凭借 480MHz 主频和 Art Accelerator 成为高性能嵌入式首选,但开启 D-Cache 后,DMA 传输数据错乱、外设读取旧值等问题频发。根源在于 Cortex-M7 的 D-Cache 与 DMA 访问内存时缺乏硬件一致性。本文从原理到实战,帮你彻底理清。

一、为什么 DMA 和 D-Cache 会打架?

Cortex-M7 的 D-Cache 是写回(Write-Back) 策略:CPU 写数据时只更新 Cache,不立即写回 SRAM;读数据时若 Cache 未命中,则从 SRAM 加载并缓存。

DMA 控制器直接访问 SRAM,不经过 Cache。于是出现两种典型错误:

  • CPU 写、DMA 读:CPU 写的数据还在 Cache 中(脏行),DMA 从 SRAM 读到旧数据。
  • DMA 写、CPU 读:DMA 将新数据写入 SRAM,但 CPU 读的是 Cache 中的旧数据(若该地址曾被缓存)。

解决思路:在 DMA 传输前后,手动维护 Cache 一致性——Clean(写回)Invalidate(无效化)

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

最优雅的方案是利用 MPU 将 DMA 缓冲区配置为 Non-Cacheable,一劳永逸。但并非所有场景都适用(如需要 CPU 频繁访问的大缓冲区)。

2.1 MPU 配置步骤

  1. 使能 MPU:SCB_EnableMPU() 前先 SCB_DisableMPU()
  2. 配置 Region:基地址、大小、属性。
  3. 使能 Region 和 MPU。
#include "stm32h7xx.h"

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

    HAL_MPU_Disable();

    // 将 0x30000000 开始的 64KB 配置为 Non-Cacheable, Non-Shared
    MPU_InitStruct.Enable           = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress      = 0x30000000;
    MPU_InitStruct.Size             = MPU_REGION_SIZE_64KB;
    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_ENABLE;

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

注意:DMA 缓冲区必须按 Cache 行大小(32 字节)对齐,否则 Clean/Invalidate 会波及相邻数据。

三、Clean 与 Invalidate 的正确使用

若缓冲区仍为 Cacheable,则必须在 DMA 前后手动操作。

3.1 操作时机

  • CPU 写 → DMA 读(发送):DMA 启动前执行 SCB_CleanDCache_by_Addr(),把脏数据写回 SRAM。
  • DMA 写 → CPU 读(接收):DMA 完成后执行 SCB_InvalidateDCache_by_Addr(),丢弃 Cache 旧数据。

3.2 完整代码示例(UART DMA 接收)

#define BUF_SIZE 128
__attribute__((aligned(32))) uint8_t rx_buf[BUF_SIZE];

void UART_DMA_Start(void)
{
    // 接收前无效化 Cache,确保 DMA 写入的数据不被 Cache 覆盖
    SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, BUF_SIZE);
    HAL_UART_Receive_DMA(&huart1, rx_buf, BUF_SIZE);
}

// DMA 完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    // 接收完成后再次无效化,让 CPU 读到 DMA 写入的新数据
    SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, BUF_SIZE);
    // 此时 rx_buf 中才是有效数据
    process_data(rx_buf, BUF_SIZE);
}

发送方向同理:

void UART_DMA_Send(uint8_t *data, uint16_t len)
{
    // 发送前 Clean,确保 DMA 从 SRAM 读到最新数据
    SCB_CleanDCache_by_Addr((uint32_t*)data, len);
    HAL_UART_Transmit_DMA(&huart1, data, len);
}

四、避坑指南

  • 地址对齐SCB_*DCache_by_Addr 的地址必须 32 字节对齐,长度建议为 32 的倍数,否则可能误伤相邻变量。
  • 不要对同一缓冲区同时 Clean 和 Invalidate:接收场景只需 Invalidate,发送只需 Clean。
  • 中断中谨慎操作:Cache 维护函数执行时间较长,避免在高速中断中频繁调用。
  • DMA 描述符也要注意:若使用链表式 DMA,描述符本身若在 Cacheable 区域,同样需要 Clean。
  • 多核/多主控场景:H7 双核间共享内存需配置为 Non-Cacheable 或使用硬件信号量。
  • 调试时关闭 Cache:若问题诡异,可先关闭 D-Cache 验证是否为一致性问题。

五、总结

STM32H7 的 D-Cache 是把双刃剑。推荐优先使用 MPU 将 DMA 缓冲区设为 Non-Cacheable,简单可靠;若必须 Cacheable,则严格遵循 发送前 Clean、接收后 Invalidate 的原则,并注意地址对齐。掌握这些,DMA 数据错乱将不再是玄学。