一、为什么 Cache 会让 DMA 数据错乱?

STM32H7 基于 Cortex-M7,主频高达 480MHz,为了匹配内核速度,芯片内部集成了 L1 Cache(指令 Cache 与数据 Cache)。CPU 访问数据时,会先查 Cache,命中则直接读写,未命中才访问实际内存。

而 DMA 是“旁路”CPU 直接访问内存的。当 CPU 写数据到 Cache 但尚未写回内存时,DMA 从内存读到的就是旧数据;反之,DMA 把新数据写入内存,CPU 却可能从 Cache 中读到旧数据。这就是缓存一致性问题。

典型场景:

  • 串口 DMA 接收数据,CPU 读到的总是第一次的数据。
  • ADC 连续采样,DMA 搬运到数组,CPU 读到的值不变。
  • 以太网发送,DMA 发出的数据包内容错乱。

二、解决思路:MPU 配置 + Cache 维护

2.1 两种策略

  1. 将 DMA 缓冲区配置为 Write-Through 或 Non-Cacheable:从根源上避免 Cache 与内存不一致。
  2. 使用 Clean/Invalidate 操作:在 DMA 传输前后手动维护 Cache。

实际工程中常结合使用:对频繁小数据量传输用 Non-Cacheable 省去维护开销;对大数据块用 Write-Back 加手动维护提升性能。

2.2 MPU 配置步骤(以 Non-Cacheable 为例)

  • 使能 MPU:SCB_EnableMPU() 前需先 SCB_EnableICache() 和 SCB_EnableDCache()。
  • 定义 MPU 区域:设置基地址、大小、属性。
  • 属性中 TEX=1, C=0, B=0 表示 Normal Memory, Non-Cacheable。
  • 注意 32 字节对齐,且区域不能重叠。
#include "stm32h7xx.h"

// 定义 DMA 缓冲区,必须 32 字节对齐
__attribute__((aligned(32))) uint8_t dma_buffer[1024];

void MPU_Config(void)
{
    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    HAL_MPU_Disable();

    // 配置 DMA 缓冲区区域为 Non-Cacheable
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = (uint32_t)dma_buffer;
    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_LEVEL1; // TEX=1, C=0, B=0
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;

    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

三、Clean 与 Invalidate 的正确用法

若缓冲区配置为 Write-Back(默认),则必须手动维护。

  • Clean:将 Cache 中已修改的数据写回内存。用于 CPU 写、DMA 读 之前。
  • Invalidate:丢弃 Cache 中的旧数据,强制从内存重新加载。用于 DMA 写、CPU 读 之前。
  • Clean+Invalidate:既写回又丢弃,用于双向传输。

CMSIS 提供函数:

  • SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize)
  • SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize)
  • SCB_CleanInvalidateDCache_by_Addr(...)

关键点:地址必须 32 字节对齐,大小建议为 32 的整数倍。

// 发送前:CPU 填充数据后,Clean 确保内存数据最新
void DMA_Send(uint8_t *data, uint32_t len)
{
    memcpy(dma_buffer, data, len);
    SCB_CleanDCache_by_Addr((uint32_t*)dma_buffer, len);
    HAL_UART_Transmit_DMA(&huart1, dma_buffer, len);
}

// 接收后:DMA 完成中断中,Invalidate 丢弃旧 Cache
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    SCB_InvalidateDCache_by_Addr((uint32_t*)dma_buffer, RX_LEN);
    // 此时读取 dma_buffer 才是 DMA 写入的新数据
    ProcessData(dma_buffer, RX_LEN);
}

四、实战避坑要点

  • 对齐是生命线:所有 DMA 缓冲区必须 32 字节对齐,长度建议 32 的倍数。否则 Clean/Invalidate 可能破坏相邻数据。
  • Invalidate 前先 Clean:若缓冲区部分被 CPU 修改,直接 Invalidate 会丢失修改。安全做法:先 Clean 再 Invalidate。
  • 中断中调用需谨慎:Cache 维护函数执行时间较长,在高速中断中频繁调用可能影响实时性。
  • 多缓冲区管理:使用多个 DMA 缓冲区时,每个都要单独维护,不能只维护首地址。
  • MPU 区域覆盖完整:确保所有 DMA 缓冲区都在配置的 MPU 区域内,否则属性不生效。
  • 调试时关闭 Cache:初期调试可暂时关闭 D-Cache 验证逻辑,确认无误后再开启并加入维护代码。
  • 注意共享性:若系统有多主设备(如 MDMA、以太网 DMA),需设置 IsShareable 为 MPU_ACCESS_SHAREABLE。

五、总结

STM32H7 的 Cache 与 DMA 一致性问题是高性能应用的必经之路。核心原则:要么让 DMA 缓冲区不经过 Cache,要么在传输前后正确 Clean/Invalidate。推荐对关键 DMA 缓冲区使用 MPU 配置为 Non-Cacheable,简单可靠;对性能敏感的大数据块,采用 Write-Back + 手动维护。牢记 32 字节对齐、先 Clean 后 Invalidate、中断中慎用等要点,即可避开绝大多数坑。