一、为什么 Cache 会让 DMA 数据“出错”?

STM32H7 内核运行频率高达 480MHz,为了匹配高速内核与相对较慢的存储器,芯片内置了 L1 Cache(I-Cache 和 D-Cache)。D-Cache 的存在使得 CPU 访问数据时可能直接命中缓存,而不去访问实际的 SRAM 或外部存储器。

DMA 控制器则独立于 CPU 工作,它直接读写物理内存,完全“看不见”Cache 的存在。这就导致了经典的一致性问题:

  • CPU 写,DMA 读:CPU 将数据写入 D-Cache,但尚未写回 SRAM。此时启动 DMA 发送,DMA 从 SRAM 读到的仍是旧数据,导致发送内容错误。
  • DMA 写,CPU 读:DMA 将新数据写入 SRAM,但 CPU 读取时命中了 D-Cache 中的旧数据,导致 CPU 读到过期内容。

解决这一问题的核心手段有两种:配置 MPU 将 DMA 缓冲区设为 Write-Through 或 Non-Cacheable,以及在恰当时机手动执行 Clean/Invalidate 操作。

二、MPU 配置:从根源上规避一致性问题

Cortex-M7 的 MPU(Memory Protection Unit)可以精细控制每一段内存的缓存属性。对于 DMA 缓冲区,推荐以下两种策略:

2.1 方案一:Non-Cacheable(最简单)

将 DMA 缓冲区所在的内存区域配置为 Non-Cacheable。CPU 对该区域的访问直接穿透到 SRAM,DMA 与 CPU 看到的数据始终一致。

  • 优点:无需手动维护 Cache,编程简单,不易出错。
  • 缺点:CPU 访问该区域速度下降,适合对性能不敏感或小数据量的 DMA。

2.2 方案二:Write-Through(性能与安全的折中)

配置为 Write-Through 模式。CPU 写操作同时更新 Cache 和 SRAM,读操作仍可命中 Cache。

  • 优点:读性能好,写操作自动同步,DMA 读数据安全。
  • 缺点:DMA 写数据后,CPU 读仍可能命中旧 Cache,需要配合 Invalidate 操作。

2.3 MPU 配置代码示例

以下代码将地址 0x30000000 开始的 64KB 区域(通常为 AXI SRAM)配置为 Write-Through,并启用 MPU。

#include "stm32h7xx.h"

void MPU_Config(void)
{
    HAL_MPU_Disable();

    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    /* 配置 Region 0:DMA 缓冲区,Write-Through,允许共享 */
    MPU_InitStruct.Enable           = MPU_REGION_ENABLE;
    MPU_InitStruct.Number           = MPU_REGION_NUMBER0;
    MPU_InitStruct.BaseAddress      = 0x30000000;
    MPU_InitStruct.Size             = MPU_REGION_SIZE_64KB;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.TypeExtField     = MPU_TEX_LEVEL0;
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.DisableExec      = MPU_INSTRUCTION_ACCESS_ENABLE;
    MPU_InitStruct.IsShareable      = MPU_ACCESS_SHAREABLE;
    MPU_InitStruct.IsCacheable      = MPU_ACCESS_CACHEABLE;
    MPU_InitStruct.IsBufferable     = MPU_ACCESS_NOT_BUFFERABLE; /* Write-Through */

    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    /* 可选:将其他 SRAM 区域配置为 Write-Back 以提升性能 */
    /* ... */

    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

注意:IsBufferable 设为 NOT_BUFFERABLE 且 IsCacheable 设为 CACHEABLE 时,即为 Write-Through 模式。若两者都设为 NOT_...,则为 Non-Cacheable。

三、Clean 与 Invalidate:手动维护一致性

当无法将缓冲区设为 Non-Cacheable 时,必须在 DMA 传输前后手动操作 Cache。

3.1 核心 API

CMSIS 提供了以下函数(定义在 core_cm7.h):

  • SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize):将 Cache 内容写回 SRAM。
  • SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize):丢弃 Cache 内容,强制下次读取从 SRAM 获取。
  • SCB_CleanInvalidateDCache_by_Addr(...):先 Clean 再 Invalidate。

3.2 使用场景与顺序

| 传输方向 | 操作顺序 | |---------|---------| | CPU 写 → DMA 读(发送) | 先 Clean,再启动 DMA | | DMA 写 → CPU 读(接收) | 启动 DMA 前 Invalidate,DMA 完成后再次 Invalidate |

关键点:

  • 对于发送,Clean 必须在启动 DMA 之前执行。
  • 对于接收,Invalidate 必须在启动 DMA 之前执行(防止 DMA 写期间 Cache 被回写覆盖),DMA 完成中断中再执行一次 Invalidate 确保 CPU 读到新数据。
  • 地址必须 32 字节对齐,长度需按 32 字节向上取整。

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)
{
    /* 1. 将数据拷贝到 DMA 缓冲区 */
    memcpy(dma_tx_buf, data, len);

    /* 2. Clean D-Cache,确保数据写回 SRAM */
    SCB_CleanDCache_by_Addr((uint32_t *)dma_tx_buf, len);

    /* 3. 启动 DMA 传输 */
    HAL_UART_Transmit_DMA(&huart1, dma_tx_buf, len);
}

3.4 完整代码示例:UART DMA 接收

#define DMA_RX_SIZE  256
__attribute__((aligned(32))) uint8_t dma_rx_buf[DMA_RX_SIZE];
volatile uint8_t rx_complete = 0;

void UART_DMA_StartReceive(void)
{
    /* 1. 启动前 Invalidate,丢弃旧 Cache */
    SCB_InvalidateDCache_by_Addr((uint32_t *)dma_rx_buf, DMA_RX_SIZE);

    /* 2. 启动 DMA 接收 */
    HAL_UART_Receive_DMA(&huart1, dma_rx_buf, DMA_RX_SIZE);
}

/* DMA 完成回调 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    /* 3. 传输完成后再次 Invalidate,确保 CPU 读到新数据 */
    SCB_InvalidateDCache_by_Addr((uint32_t *)dma_rx_buf, DMA_RX_SIZE);

    rx_complete = 1;
    /* 此时可以安全处理 dma_rx_buf 中的数据 */
}

四、实战避坑要点

  • 对齐是硬性要求:DMA 缓冲区必须 32 字节对齐,使用 __attribute__((aligned(32))) 修饰。长度参数也需按 32 字节向上取整,否则可能遗漏部分 Cache 行。
  • 不要对同一缓冲区同时 Clean 和 Invalidate:除非明确需要,否则 CleanInvalidate 可能引入不必要的性能开销和逻辑混乱。
  • Invalidate 前确保 DMA 已停止:如果 DMA 正在写缓冲区,此时 Invalidate 可能丢弃尚未写入的数据,导致数据丢失。
  • 多缓冲区场景:若使用多个 DMA 缓冲区,每个缓冲区都需要独立维护 Cache,不能只操作一个。
  • 中断优先级与 Cache 操作:Cache 维护函数本身不是原子操作,若在中断中调用,需确保不会与主循环中的 Cache 操作冲突。
  • 外部存储器(SDRAM/QSPI):若 DMA 访问外部存储器,除了 Cache 一致性,还需注意 MPU 配置和 FMC/QSPI 的时序设置。
  • 调试时关闭 Cache:在排查诡异数据错误时,可临时将 D-Cache 关闭,若问题消失,则基本可确定是一致性问题。

五、总结

STM32H7 的 Cache 与 DMA 一致性问题是高性能嵌入式开发的必修课。核心原则是:明确数据流向,选择合适策略。对于小数据量或低频率 DMA,直接配置 MPU 为 Non-Cacheable 最省心;对于高性能场景,Write-Through 配合精准的 Clean/Invalidate 操作是更优解。牢记对齐要求、操作顺序和缓冲区独立性,就能避开绝大多数“玄学”Bug。