STM32H7 的 D-Cache 与 DMA 一致性:地址对齐、MPU 配置与 Clean/Invalidate 的实战避坑

STM32H7 系列凭借 Cortex-M7 内核和 480MHz 主频,成为高性能嵌入式应用的首选。但许多开发者从 F4/F7 迁移到 H7 后,常遇到 DMA 传输数据错乱、HardFault 等问题,根源往往是 D-Cache 与 DMA 的缓存一致性。本文从原理到实战,帮你彻底避开这些坑。

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

Cortex-M7 的 D-Cache 位于 CPU 和 AXI 总线之间。当 CPU 访问内存时,数据可能被缓存在 D-Cache 中,而 DMA 直接访问物理内存(SRAM)。这就导致:

  • CPU 写数据:数据可能还在 Cache 中,未写回 SRAM,DMA 读到旧数据。
  • DMA 写数据:DMA 将新数据写入 SRAM,但 CPU 读到的仍是 Cache 中的旧数据。

因此,必须通过 Clean(写回)Invalidate(无效化) 操作来同步 Cache 与内存。

二、地址对齐:容易被忽视的硬性要求

Cortex-M7 的 Cache 行大小为 32 字节。Clean/Invalidate 操作以 Cache 行为单位,若缓冲区地址或长度未按 32 字节对齐,会导致:

  • 误清除相邻变量的 Cache,引发数据损坏。
  • 操作越界,触发 HardFault。

避坑指南

  • DMA 缓冲区必须 32 字节对齐,长度建议为 32 的整数倍。
  • 使用 __attribute__((aligned(32)))ALIGN_32BYTES 宏。
// 定义 32 字节对齐的 DMA 缓冲区
__attribute__((aligned(32))) uint8_t dma_buffer[256];

三、MPU 配置:为 DMA 区域设置正确的内存属性

默认情况下,STM32H7 的 SRAM 区域是 Write-Back, Write-Allocate 的,这会导致 DMA 访问时数据不一致。推荐将 DMA 缓冲区所在区域配置为 Non-CacheableWrite-Through

3.1 使用 MPU 配置 Non-Cacheable 区域

以 STM32H743 为例,将 0x30000000 开始的 64KB SRAM 配置为 Non-Cacheable:

#include "stm32h7xx_hal.h"

void MPU_Config(void)
{
    HAL_MPU_Disable();

    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    // 配置 SRAM4 (0x38000000) 为 Non-Cacheable,用于 DMA
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = 0x38000000;
    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_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);
}

3.2 使用链接脚本将变量放入 Non-Cacheable 区域

在链接脚本中定义段,将 DMA 缓冲区放入该区域:

MEMORY
{
  RAM_D2 (xrw) : ORIGIN = 0x30000000, LENGTH = 288K
  RAM_D3 (xrw) : ORIGIN = 0x38000000, LENGTH = 64K
}

SECTIONS
{
  .dma_buffer (NOLOAD) :
  {
    . = ALIGN(32);
    *(.dma_buffer)
    . = ALIGN(32);
  } >RAM_D3
}

然后在代码中:

__attribute__((section(".dma_buffer"), aligned(32)))
uint8_t dma_buffer[256];

四、Clean 与 Invalidate 的正确使用

即使配置了 MPU,有时仍需手动维护 Cache。关键原则:

  • CPU 写,DMA 读:在启动 DMA 前,对缓冲区执行 Clean(写回)。
  • DMA 写,CPU 读:在 DMA 完成后,对缓冲区执行 Invalidate(无效化)。
  • CPU 读写,DMA 读写:先 Clean 再 Invalidate。

4.1 使用 CMSIS 函数

#include "cmsis_compiler.h"

// 写回并无效化
SCB_CleanInvalidateDCache_by_Addr((uint32_t *)dma_buffer, sizeof(dma_buffer));

// 仅写回
SCB_CleanDCache_by_Addr((uint32_t *)dma_buffer, sizeof(dma_buffer));

// 仅无效化
SCB_InvalidateDCache_by_Addr((uint32_t *)dma_buffer, sizeof(dma_buffer));

4.2 完整 DMA 发送示例

void DMA_SendData(uint8_t *data, uint16_t len)
{
    // 1. 确保数据已写入 SRAM
    SCB_CleanDCache_by_Addr((uint32_t *)data, len);

    // 2. 启动 DMA
    HAL_UART_Transmit_DMA(&huart1, data, len);
}

4.3 完整 DMA 接收示例

void DMA_ReceiveData(uint8_t *buffer, uint16_t len)
{
    // 1. 无效化 Cache,确保 CPU 读取时从 SRAM 获取
    SCB_InvalidateDCache_by_Addr((uint32_t *)buffer, len);

    // 2. 启动 DMA 接收
    HAL_UART_Receive_DMA(&huart1, buffer, len);
}

// DMA 完成回调中再次无效化
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buffer, RX_SIZE);
    // 处理数据...
}

五、实战避坑清单

  • 地址对齐:DMA 缓冲区必须 32 字节对齐,长度建议为 32 的倍数。
  • MPU 配置:将 DMA 区域设为 Non-Cacheable 或 Write-Through,避免手动维护。
  • Clean 时机:CPU 写数据后、启动 DMA 前。
  • Invalidate 时机:DMA 完成后、CPU 读数据前。
  • 避免频繁操作:Clean/Invalidate 开销较大,尽量批量处理。
  • 调试技巧:若数据错乱,先检查 Cache 操作是否遗漏或地址未对齐。
  • 使用 __DSB():在 Clean/Invalidate 后插入内存屏障,确保操作完成。
SCB_CleanDCache_by_Addr((uint32_t *)data, len);
__DSB(); // 确保写回完成

六、总结

STM32H7 的 D-Cache 与 DMA 一致性问题是高性能应用的“必修课”。核心思路:要么让 DMA 区域不可缓存(MPU 配置),要么在正确时机手动 Clean/Invalidate。同时务必保证地址 32 字节对齐。掌握这些要点,你就能在 H7 上稳定高效地使用 DMA,避免那些令人抓狂的随机错误。