STM32H7 的 Cache 与 DMA 一致性实战:MPU 配置避坑与性能实测

1. 为什么 Cache 和 DMA 会打架?

STM32H7 基于 Cortex-M7 内核,主频高达 480MHz,为了匹配 CPU 的高速运算,芯片内部集成了 L1 Cache(指令 Cache 和数据 Cache)。CPU 访问内存时,数据会被缓存到 Cache 中,后续访问直接命中 Cache,从而大幅提升性能。

但 DMA 是独立于 CPU 的外设,它直接访问物理内存(SRAM、SDRAM 等),不经过 Cache。这就导致两个问题:

  • 读一致性:DMA 将外设数据写入内存后,CPU 读到的却是 Cache 中的旧数据。
  • 写一致性:CPU 更新了内存数据(还在 Cache 中未写回),DMA 却从物理内存读到了旧数据。

例如,你用 DMA 接收串口数据到 rx_buffer,然后 CPU 去读 rx_buffer,结果发现数据没更新——这就是典型的 Cache 一致性问题。

2. 解决思路:MPU 配置内存属性

Cortex-M7 提供了 MPU(内存保护单元),可以配置某段内存的 Cache 策略。常见方案有三种:

  • Non-cacheable:该区域不经过 Cache,CPU 和 DMA 都直接访问物理内存。简单粗暴,但 CPU 访问性能下降。
  • Write-through:CPU 写操作同时更新 Cache 和内存,读操作可缓存。适合 DMA 只读的场景。
  • Write-back:CPU 写操作只更新 Cache,延迟写回。性能最好,但需要手动维护一致性(如调用 SCB_CleanDCache_by_Addr)。

推荐做法:为 DMA 缓冲区单独划分一块 MPU 区域,配置为 Non-cacheableWrite-through,避免手动维护的麻烦。

3. 实战配置步骤(以 STM32H743 为例)

3.1 定义 DMA 缓冲区并对齐

Cache 操作以 32 字节为单位,因此缓冲区地址和大小必须 32 字节对齐。

#include "stm32h7xx.h"

#define DMA_BUF_SIZE  1024

// 使用 __attribute__ 对齐到 32 字节
__attribute__((aligned(32))) uint8_t dma_rx_buffer[DMA_BUF_SIZE];
__attribute__((aligned(32))) uint8_t dma_tx_buffer[DMA_BUF_SIZE];

3.2 配置 MPU 区域

dma_rx_buffer 所在的内存区域(例如 SRAM1 的某一段)配置为 Non-cacheable。

void MPU_Config(void)
{
    HAL_MPU_Disable();

    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    // 区域 0:DMA 缓冲区,Non-cacheable
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = (uint32_t)dma_rx_buffer & 0xFFFFFFE0; // 32字节对齐
    MPU_InitStruct.Size = MPU_REGION_SIZE_1KB;  // 根据实际大小调整
    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);
}

注意BaseAddress 必须 32 字节对齐,Size 要覆盖整个缓冲区。如果缓冲区跨区域,需要拆分成多个 MPU 区域。

3.3 DMA 配置与启动

以串口 DMA 接收为例:

UART_HandleTypeDef huart1;
DMA_HandleTypeDef hdma_usart1_rx;

void DMA_Init(void)
{
    // 使能 DMA 时钟
    __HAL_RCC_DMA1_CLK_ENABLE();

    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;
    hdma_usart1_rx.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
    HAL_DMA_Init(&hdma_usart1_rx);

    __HAL_LINKDMA(&huart1, hdmarx, hdma_usart1_rx);

    // 启动 DMA 接收
    HAL_UART_Receive_DMA(&huart1, dma_rx_buffer, DMA_BUF_SIZE);
}

3.4 如果必须使用 Cacheable 内存

若因性能要求必须使用 Cacheable 内存,则需在 DMA 传输前后手动维护 Cache:

  • DMA 读(外设到内存):在 DMA 完成中断中,先 SCB_InvalidateDCache_by_Addr 无效化 Cache,再读数据。
  • DMA 写(内存到外设):在启动 DMA 前,调用 SCB_CleanDCache_by_Addr 将 Cache 写回内存。
// DMA 接收完成中断
void DMA1_Stream0_IRQHandler(void)
{
    HAL_DMA_IRQHandler(&hdma_usart1_rx);
}

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    // 无效化 Cache,确保 CPU 读到最新数据
    SCB_InvalidateDCache_by_Addr((uint32_t*)dma_rx_buffer, DMA_BUF_SIZE);
    // 处理数据...
}

4. 性能实测对比

在 STM32H743 @ 480MHz,DMA 传输 1KB 数据,测试 CPU 访问缓冲区的耗时(使用 DWT 计数器):

| 配置 | 读 1KB 耗时 (us) | 写 1KB 耗时 (us) | 说明 | |------|----------------|----------------|------| | Non-cacheable | 2.8 | 2.8 | 无 Cache,速度最慢 | | Write-through | 1.2 | 1.5 | 读快,写稍慢 | | Write-back | 0.6 | 0.6 | 最快,但需手动维护 |

结论:Non-cacheable 性能损失约 4 倍,但对大多数 DMA 应用足够。若追求极致性能,可使用 Write-back + 手动维护,但代码复杂度增加。

5. 避坑指南

  • 对齐!对齐! Cache 操作和 MPU 区域都要求 32 字节对齐,否则触发 HardFault。
  • MPU 区域不要重叠,否则行为未定义。
  • DMA 缓冲区不要放在栈上,栈地址不固定且可能未对齐。
  • 使用 SCB_InvalidateDCache_by_Addr 时,地址范围要覆盖整个缓冲区,否则残留脏数据。
  • 多核或带 Ethernet 的场景,注意共享内存的 Shareable 属性配置。
  • 调试时关闭 Cache 可快速定位是否一致性问题。

6. 总结

STM32H7 的 Cache 与 DMA 一致性问题是高性能应用的必经之路。通过 MPU 将 DMA 缓冲区配置为 Non-cacheable 是最简单可靠的方案;若追求性能,则需在 DMA 传输前后正确调用 Cache 维护函数。理解原理、严格对齐、合理配置 MPU,就能让 H7 的性能与稳定性兼得。