一、为什么 DMA 与 Cache 会“打架”?

STM32H7 搭载 Cortex-M7,主频可达 480MHz,内部集成 L1 数据 Cache(D-Cache)和指令 Cache(I-Cache)。D-Cache 的存在让 CPU 访问频繁使用的数据时无需每次都读 AXI SRAM,性能提升显著。但 DMA 控制器直接访问物理内存,不经过 Cache。

这就产生两个经典问题:

  • CPU 写、DMA 读:CPU 把数据写入缓冲区,数据可能还在 Cache 里(Write-Back 模式),DMA 从物理内存读到的却是旧数据。
  • DMA 写、CPU 读:DMA 把新数据写入物理内存,但 CPU 读到的却是 Cache 中的旧副本。

根本原因:同一块内存存在两个“视图”——CPU 看到的是 Cache,DMA 看到的是物理内存。

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

有两种主流方案:

  1. 将 DMA 缓冲区配置为 Non-Cacheable 或 Write-Through:CPU 访问直接穿透到物理内存,无需维护操作,但会损失部分性能。
  2. 保持 Cacheable,在 DMA 传输前后手动维护 Cache:性能最好,但代码复杂,容易遗漏。

实际工程中常混合使用:小数据量、高频传输用 Non-Cacheable;大数据量、低频传输用 Cache 维护。

三、MPU 配置实战

STM32H7 的 MPU 有 16 个区域,每个区域可独立设置属性。下面以 AXI SRAM 中一块 32KB 的 DMA 缓冲区为例,配置为 Non-Cacheable。

#include "stm32h7xx.h"

#define DMA_BUF_ADDR   0x24000000UL   // AXI SRAM 起始
#define DMA_BUF_SIZE   0x8000UL       // 32KB

void MPU_Config(void)
{
    HAL_MPU_Disable();

    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    /* 区域 0:背景区域,Normal Cacheable */
    MPU_InitStruct.Enable           = MPU_REGION_ENABLE;
    MPU_InitStruct.Number           = MPU_REGION_NUMBER0;
    MPU_InitStruct.BaseAddress      = 0x00000000;
    MPU_InitStruct.Size             = MPU_REGION_SIZE_4GB;
    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_NOT_SHAREABLE;
    MPU_InitStruct.IsCacheable      = MPU_ACCESS_CACHEABLE;
    MPU_InitStruct.IsBufferable     = MPU_ACCESS_BUFFERABLE;
    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    /* 区域 1:DMA 缓冲区,Non-Cacheable */
    MPU_InitStruct.Number           = MPU_REGION_NUMBER1;
    MPU_InitStruct.BaseAddress      = DMA_BUF_ADDR;
    MPU_InitStruct.Size             = MPU_REGION_SIZE_32KB;
    MPU_InitStruct.TypeExtField     = MPU_TEX_LEVEL1;
    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);
}

关键点:

  • 区域编号越小优先级越高,区域 1 覆盖区域 0 的对应地址。
  • TEX=1, C=0, B=0 对应 Normal Non-Cacheable。
  • 若希望 Write-Through 模式,设 TEX=0, C=1, B=0。
  • 配置后需调用 SCB_EnableDCache() 使能 D-Cache。

四、Cache 维护操作

若缓冲区保持 Cacheable,必须在 DMA 传输前后调用 CMSIS 提供的函数。

/* CPU 写数据 -> DMA 读:先 Clean,把 Cache 写回内存 */
void DMA_SendData(uint8_t *buf, uint32_t len)
{
    SCB_CleanDCache_by_Addr((uint32_t *)buf, len);
    HAL_DMA_Start(&hdma, (uint32_t)buf, (uint32_t)&periph_addr, len);
}

/* DMA 写数据 -> CPU 读:先 Invalidate,丢弃 Cache 旧副本 */
void DMA_ReceiveData(uint8_t *buf, uint32_t len)
{
    SCB_InvalidateDCache_by_Addr((uint32_t *)buf, len);
    HAL_DMA_Start(&hdma, (uint32_t)&periph_addr, (uint32_t)buf, len);
    /* 等待传输完成... */
    SCB_InvalidateDCache_by_Addr((uint32_t *)buf, len);
}

地址对齐要求:SCB_CleanDCache_by_Addr 的地址必须是 32 字节对齐,长度建议向上取整到 32 字节倍数,否则可能误伤相邻数据。

#define ALIGN_32(x)  (((x) + 31U) & ~31U)

void Safe_Clean(void *addr, uint32_t len)
{
    uint32_t aligned_addr = (uint32_t)addr & ~31U;
    uint32_t aligned_len  = ALIGN_32((uint32_t)addr + len - aligned_addr);
    SCB_CleanDCache_by_Addr((uint32_t *)aligned_addr, aligned_len);
}

五、完整示例:ADC + DMA 采集

#define ADC_BUF_LEN  256
__attribute__((aligned(32))) static uint16_t adc_buf[ADC_BUF_LEN];

void ADC_DMA_Init(void)
{
    /* 假设 MPU 已把 adc_buf 所在区域设为 Non-Cacheable */
    HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_buf, ADC_BUF_LEN);
}

/* DMA 完成回调中处理数据 */
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
    /* 若为 Cacheable,此处需 Invalidate */
    SCB_InvalidateDCache_by_Addr((uint32_t *)adc_buf, sizeof(adc_buf));
    ProcessData(adc_buf, ADC_BUF_LEN);
}

六、注意事项与避坑指南

  • 不要对同一区域既 Invalidate 又 Clean:顺序错误会导致数据丢失。写方向先 Clean,读方向先 Invalidate。
  • Invalidate 前确保 DMA 已停止:否则可能丢弃 DMA 刚写入但未完成的数据。
  • 多缓冲区(Double Buffer)要分别维护:每个缓冲区独立调用维护函数。
  • ETH、SDMMC、USB 等外设 DMA 同样适用:原理一致,别只盯着 ADC。
  • 调试时关闭 D-Cache 可快速定位问题:若关闭后正常,基本可确定是一致性问题。
  • MPU 配置要在使能 Cache 之前完成:否则属性不生效。
  • 使用 __attribute__((aligned(32))) 保证缓冲区对齐:避免维护操作越界。

七、总结

STM32H7 的 Cache 与 DMA 一致性问题是高性能应用的必修课。核心原则:要么让 DMA 缓冲区不经过 Cache(MPU 配置),要么在传输前后正确维护 Cache(Clean/Invalidate)。理解两者适用场景,结合对齐与顺序要求,就能在享受 H7 高性能的同时,保证数据万无一失。