STM32H7 的 D-Cache 与 DMA 一致性踩坑:用 MPU 配置 Non-Cacheable 区域的正确姿势

1. 问题现象:DMA 数据“不更新”

很多开发者从 F4/F7 迁移到 H7 后,会遇到这样的怪事:

  • DMA 接收串口/ADC 数据到缓冲区,CPU 读到的却始终是旧值。
  • DMA 发送缓冲区数据,实际发出的内容错乱或只发了一部分。
  • 单步调试正常,全速运行就出错。

根本原因:Cortex-M7 的 D-Cache 与 DMA 访问同一块内存时,数据不一致。

2. 原理:为什么会有缓存一致性问题?

  • D-Cache 工作方式:CPU 访问内存时,数据被缓存到 D-Cache。写操作可能只更新 Cache(Write-Back),读操作可能直接命中 Cache 而不读内存。
  • DMA 绕过 Cache:DMA 控制器直接访问物理内存,不经过 D-Cache。
  • 冲突场景:
    • CPU 写数据到缓冲区 → 数据在 Cache 中,内存未更新 → DMA 从内存读旧数据发送。
    • DMA 从外设搬数据到内存 → 内存已更新 → CPU 读时命中 Cache 旧数据。

解决思路有三种:

  1. 使用 SCB_CleanDCache_by_Addr() / SCB_InvalidateDCache_by_Addr() 手动维护。
  2. 将 DMA 缓冲区放在 Non-Cacheable 内存区域(推荐)。
  3. 使用 Write-Through 模式(仍有风险,不推荐)。

本文重点讲解 方法 2:通过 MPU 配置 Non-Cacheable 区域,一劳永逸。

3. MPU 配置步骤

3.1 确定内存区域

STM32H7 的 RAM 分布复杂,常见可用区域:

  • 0x24000000:AXI SRAM (512KB),DMA 可访问,推荐。
  • 0x30000000:SRAM1/2/3 (288KB),DMA 可访问。
  • 0x38000000:SRAM4 (64KB),DMA 可访问。

注意:DTCM (0x20000000) 和 ITCM (0x00000000) DMA 无法访问,不能用作 DMA 缓冲区。

3.2 配置 MPU Region

以 AXI SRAM 中划出 32KB 作为 Non-Cacheable 区域为例:

  • 基地址:0x24000000
  • 大小:32KB(2^15)
  • 属性:Normal, Non-Cacheable, Shareable
#include "stm32h7xx.h"

#define DMA_BUFFER_ADDR   0x24000000UL
#define DMA_BUFFER_SIZE   0x8000UL   // 32KB

void MPU_Config(void)
{
    HAL_MPU_Disable();

    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    // 配置 Region 0 为 Non-Cacheable
    MPU_InitStruct.Enable           = MPU_REGION_ENABLE;
    MPU_InitStruct.Number           = MPU_REGION_NUMBER0;
    MPU_InitStruct.BaseAddress      = DMA_BUFFER_ADDR;
    MPU_InitStruct.Size             = MPU_REGION_SIZE_32KB;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.TypeExtField     = MPU_TEX_LEVEL0;
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.DisableExec      = MPU_INSTRUCTION_ACCESS_DISABLE;
    MPU_InitStruct.IsShareable      = MPU_ACCESS_SHAREABLE;
    MPU_InitStruct.IsCacheable      = MPU_ACCESS_NOT_CACHEABLE;
    MPU_InitStruct.IsBufferable     = MPU_ACCESS_NOT_BUFFERABLE;

    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

关键点:

  • IsCacheable = NOT_CACHEABLE 是核心。
  • IsShareable = SHAREABLE 保证多总线主控(CPU、DMA)看到一致数据。
  • DisableExec = INSTRUCTION_ACCESS_DISABLE 防止误取指,提升安全性。

3.3 在链接脚本中保留该区域

确保编译器不会把普通变量分配到该区域,可在链接脚本中定义专用段:

MEMORY
{
  AXI_SRAM (xrw) : ORIGIN = 0x24000000, LENGTH = 512K
}

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

然后在代码中声明:

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

4. 完整示例:UART DMA 接收

#include "stm32h7xx_hal.h"

UART_HandleTypeDef huart1;
DMA_HandleTypeDef hdma_usart1_rx;

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

void UART_DMA_Init(void)
{
    // 1. 使能 DMA 时钟
    __HAL_RCC_DMA1_CLK_ENABLE();

    // 2. 配置 DMA
    hdma_usart1_rx.Instance = DMA1_Stream0;
    hdma_usart1_rx.Init.Request = DMA_REQUEST_USART1_RX;
    hdma_usart1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
    hdma_usart1_rx.Init.PeriphInc = DMA_PINC_DISABLE;
    hdma_usart1_rx.Init.MemInc = DMA_MINC_ENABLE;
    hdma_usart1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
    hdma_usart1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
    hdma_usart1_rx.Init.Mode = DMA_CIRCULAR;
    hdma_usart1_rx.Init.Priority = DMA_PRIORITY_HIGH;
    HAL_DMA_Init(&hdma_usart1_rx);

    __HAL_LINKDMA(&huart1, hdmarx, hdma_usart1_rx);

    // 3. 启动 DMA 接收
    HAL_UART_Receive_DMA(&huart1, uart_rx_buf, sizeof(uart_rx_buf));
}

// 主循环中直接读取 uart_rx_buf,无需 Clean/Invalidate
void Process_UART_Data(void)
{
    for (int i = 0; i < sizeof(uart_rx_buf); i++) {
        if (uart_rx_buf[i] != 0) {
            // 处理数据
        }
    }
}

5. 注意事项与常见坑

  • DTCM 不能用于 DMA:DTCM 地址 0x20000000 虽快,但 DMA 无法访问,必须用 AXI SRAM 或 SRAM1/2/3/4。
  • 对齐要求:DMA 缓冲区建议 32 字节对齐,避免 Cache Line 边界问题。
  • MPU Region 数量有限:Cortex-M7 只有 16 个 Region,规划好地址范围。
  • 不要混用:如果已配置 Non-Cacheable,就不要再手动 Clean/Invalidate,否则可能引入新问题。
  • 调试时注意:某些 IDE 的实时变量查看会触发 Cache 操作,可能掩盖问题,建议用内存窗口查看物理地址。
  • 多缓冲区场景:若多个 DMA 缓冲区分散,可合并到一个 Region,或使用多个 Region。
  • Cache 维护函数:若必须使用 Cacheable 区域,务必在 DMA 传输前后调用 SCB_CleanDCache_by_Addr() 和 SCB_InvalidateDCache_by_Addr(),且地址按 32 字节对齐。

6. 总结

STM32H7 的 D-Cache 是把双刃剑,用好了性能飞升,用不好 bug 缠身。通过 MPU 将 DMA 缓冲区配置为 Non-Cacheable,是最简单、最可靠的方案。记住三句话:

  • DMA 缓冲区放 AXI SRAM,别放 DTCM。
  • MPU 配 Non-Cacheable + Shareable。
  • 配好了就别再手动维护 Cache。

掌握这些,你就能在 H7 上愉快地使用 DMA 了。