STM32H7 的 Cache 与 DMA 一致性:MPU 配置与 Clean/Invalidate 的实战避坑

STM32H7 系列搭载 Cortex-M7 内核,主频高达 480MHz,并配备 L1 数据 Cache(D-Cache)和指令 Cache(I-Cache)。Cache 的引入显著提升了 CPU 访问外部存储器的效率,但也给 DMA 传输带来了严重的数据一致性挑战。许多开发者在使用 DMA 时遇到数据错乱、传输不完整等问题,根源往往在于 Cache 与 DMA 的协同不当。本文将深入剖析原理,给出 MPU 配置与 Clean/Invalidate 的实战方案。

一、为什么 Cache 会导致 DMA 数据不一致?

Cortex-M7 的 D-Cache 采用写回(Write-Back)策略。当 CPU 写数据时,数据先写入 Cache,并不立即同步到物理内存(如 SRAM、SDRAM)。如果此时启动 DMA 从该内存地址读取数据发送到外设,DMA 取到的是旧数据,导致发送错误。

反之,当 DMA 从外设接收数据写入内存后,CPU 读取该内存时,若 Cache 中恰好有该地址的旧缓存行,CPU 会直接返回旧数据,而不会从内存重新加载,导致读取错误。

  • 写方向(CPU→内存→DMA):CPU 写数据后,需执行 Clean 操作,将 Cache 内容写回内存,再启动 DMA。
  • 读方向(DMA→内存→CPU):DMA 写内存后,需执行 Invalidate 操作,使 Cache 中对应行失效,强制 CPU 从内存重新加载。

二、MPU 配置:为 DMA 缓冲区划定非缓存区域

最彻底的解决方案是将 DMA 缓冲区配置为 Non-Cacheable,这样 CPU 和 DMA 都直接访问物理内存,无需 Clean/Invalidate。STM32H7 通过 MPU(Memory Protection Unit)实现。

2.1 MPU 配置步骤

  1. 使能 MPU:SCB->SHCSR |= SCB_SHCSR_MEMFAULTENA_Msk;
  2. 禁用 MPU:ARM_MPU_Disable();
  3. 配置 Region:设置基地址、大小、属性(Normal, Non-Cacheable)。
  4. 使能 MPU:ARM_MPU_Enable(MPU_CTRL_PRIVDEFENA_Msk);

2.2 完整 MPU 配置代码(以 0x30000000 开始的 32KB 为例)

#include "stm32h7xx.h"

void MPU_Config(void)
{
    ARM_MPU_Disable();

    // 配置 Region 0:DMA 缓冲区,非缓存、非共享、可读写
    ARM_MPU_SetRegion(
        0,                          // Region 编号
        0x30000000,                 // 基地址
        ARM_MPU_RASR(               // 属性
            0,                      // 禁用指令访问
            ARM_MPU_AP_FULL,        // 全权限
            0,                      // 非共享
            0,                      // 非缓存
            0,                      // 非缓冲
            0,                      // 非子区域禁用
            ARM_MPU_SIZE_32KB,      // 大小 32KB
            1                       // 使能 Region
        )
    );

    ARM_MPU_Enable(MPU_CTRL_PRIVDEFENA_Msk);
    __DSB();
    __ISB();
}

注意:ARM_MPU_RASR 宏参数需根据 CMSIS 版本调整,核心是设置 TEX=0, C=0, B=0 实现 Non-Cacheable。

三、Clean 与 Invalidate 的正确使用

若无法将缓冲区设为非缓存(例如缓冲区在 DTCM 中,DTCM 本身不经过 Cache),则必须手动维护 Cache。

3.1 关键函数

  • SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize):将 Cache 内容写回内存。
  • SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize):使 Cache 行失效。
  • SCB_CleanInvalidateDCache_by_Addr():先 Clean 再 Invalidate。

3.2 使用时机

  • 发送前(CPU 写数据,DMA 读):调用 SCB_CleanDCache_by_Addr()
  • 接收后(DMA 写数据,CPU 读):调用 SCB_InvalidateDCache_by_Addr()
  • 接收前(防止 DMA 覆盖前 Cache 脏数据):先 SCB_InvalidateDCache_by_Addr(),再启动 DMA。

3.3 代码示例:UART DMA 发送

#define DMA_BUF_SIZE 128
__attribute__((aligned(32))) uint8_t dma_tx_buf[DMA_BUF_SIZE];

void UART_DMA_Send(uint8_t *data, uint16_t len)
{
    memcpy(dma_tx_buf, data, len);
    SCB_CleanDCache_by_Addr((uint32_t*)dma_tx_buf, len);
    HAL_UART_Transmit_DMA(&huart1, dma_tx_buf, len);
}

3.4 代码示例:UART DMA 接收

__attribute__((aligned(32))) uint8_t dma_rx_buf[DMA_BUF_SIZE];

void UART_DMA_Receive_Start(void)
{
    SCB_InvalidateDCache_by_Addr((uint32_t*)dma_rx_buf, DMA_BUF_SIZE);
    HAL_UART_Receive_DMA(&huart1, dma_rx_buf, DMA_BUF_SIZE);
}

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    SCB_InvalidateDCache_by_Addr((uint32_t*)dma_rx_buf, DMA_BUF_SIZE);
    // 此时 dma_rx_buf 中为最新数据
}

四、实战避坑指南

  • 对齐问题:Cache 操作以 32 字节为单位。缓冲区地址和长度必须 32 字节对齐,否则 Clean/Invalidate 可能影响相邻数据。使用 __attribute__((aligned(32))) 强制对齐。
  • DTCM 与 Cache:DTCM 不经过 Cache,DMA 可直接访问,无需 Clean/Invalidate。但 DTCM 地址范围有限(通常 128KB),且 DMA 无法访问所有 DTCM 区域,需查阅参考手册。
  • 多缓冲区管理:若使用多个 DMA 缓冲区,确保每个缓冲区独立对齐,避免 Cache 行重叠。
  • 中断中操作:在 DMA 完成中断中执行 Invalidate 是安全的,但需注意中断优先级和 Cache 操作的原子性。
  • 性能权衡:频繁 Clean/Invalidate 会降低性能。对于高速数据流,优先使用 MPU 非缓存区域。
  • 调试陷阱:在调试器中查看变量可能触发 Cache 行为变化,建议通过内存窗口直接查看物理地址。

五、总结

STM32H7 的 Cache 与 DMA 一致性问题是嵌入式开发中的经典难题。核心原则是:要么让 DMA 缓冲区不经过 Cache(MPU 配置),要么在正确时机手动维护 Cache(Clean/Invalidate)。理解数据流向,严格对齐缓冲区,结合 MPU 与 Cache 操作,才能确保系统稳定可靠。希望本文的实战经验能帮助你避开这些坑,充分发挥 H7 的高性能优势。