为什么 DMA 一开 D-Cache 就翻车?

STM32H7 的 Cortex-M7 内核带 16KB D-Cache 和 I-Cache。开启 D-Cache 后,CPU 读写内存会先经过 Cache,而 DMA 是直接访问物理内存(SRAM)的“旁路”通道。两者看到的数据可能不一致:

  • CPU 写数据 → 只更新了 Cache,SRAM 还是旧值,DMA 发送出去的是旧数据。
  • DMA 接收数据 → 只写了 SRAM,Cache 里还是旧值,CPU 读到的还是旧数据。

这就是经典的 Cache 一致性问题。解决手段只有两个:Clean(写回) 和 Invalidate(无效化)。

Clean 与 Invalidate 的时机

先记住一句话:Clean 是“把 Cache 里的脏数据写回 SRAM”,Invalidate 是“把 Cache 里的数据标记为无效,下次读时从 SRAM 重新加载”。

| 场景 | 操作 | 时机 | |------|------|------| | CPU 写缓冲区,DMA 发送 | Clean | DMA 启动前 | | DMA 接收数据,CPU 读取 | Invalidate | DMA 完成后 | | DMA 接收数据,CPU 读取 | Clean(可选) | DMA 启动前,防止脏数据覆盖 |

关键坑点:

  • Invalidate 必须在 DMA 完成后、CPU 读取前执行,否则读到旧数据。
  • Invalidate 前如果 CPU 写过该区域,必须先 Clean,否则未写回的数据会被丢弃。
  • 不要对正在被 DMA 写入的区域执行 Invalidate,否则可能丢数据。

正确使用 Cache 维护函数

CMSIS 提供了两个函数:

void SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize);
void SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize);

注意 dsize 是字节数,且地址必须 32 字节对齐(Cache Line 大小)。缓冲区定义时务必对齐:

__attribute__((aligned(32))) uint8_t dma_buf[1024];

MPU 配置:让 DMA 缓冲区绕过 Cache

更优雅的方案是用 MPU 把 DMA 缓冲区配置为 Non-Cacheable,这样根本不需要手动维护 Cache。

void MPU_Config(void)
{
    HAL_MPU_Disable();

    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    MPU_InitStruct.Enable           = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress      = 0x30000000;  // D2 SRAM 地址
    MPU_InitStruct.Size             = MPU_REGION_SIZE_64KB;
    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;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.DisableExec      = MPU_INSTRUCTION_ACCESS_DISABLE;

    HAL_MPU_ConfigRegion(&MPU_InitStruct);
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

把 DMA 缓冲区放到 0x30000000(D2 SRAM)并配置为 Non-Cacheable,DMA 和 CPU 都直接访问物理内存,彻底避免一致性问题。

完整实战:ADC + DMA 采集

以 ADC1 连续采集 1024 个点为例,缓冲区放在 D2 SRAM 并配置 MPU:

#include "stm32h7xx_hal.h"

#define ADC_BUF_SIZE 1024
__attribute__((section(".dma_buffer"), aligned(32)))
uint16_t adc_buf[ADC_BUF_SIZE];

ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;

void ADC_DMA_Init(void)
{
    // DMA 配置
    hdma_adc1.Instance                 = DMA1_Stream0;
    hdma_adc1.Init.Request             = DMA_REQUEST_ADC1;
    hdma_adc1.Init.Direction           = DMA_PERIPH_TO_MEMORY;
    hdma_adc1.Init.PeriphInc           = DMA_PINC_DISABLE;
    hdma_adc1.Init.MemInc              = DMA_MINC_ENABLE;
    hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
    hdma_adc1.Init.MemDataAlignment    = DMA_MDATAALIGN_HALFWORD;
    hdma_adc1.Init.Mode                = DMA_CIRCULAR;
    hdma_adc1.Init.Priority            = DMA_PRIORITY_HIGH;
    HAL_DMA_Init(&hdma_adc1);
    __HAL_LINKDMA(&hadc1, DMA_Handle, hdma_adc1);

    // ADC 配置(略,按 CubeMX 生成即可)
    HAL_ADC_Init(&hadc1);
    HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_buf, ADC_BUF_SIZE);
}

// 读取数据前,若缓冲区是 Cacheable,必须 Invalidate
void ADC_ReadBuffer(uint16_t *out, uint32_t len)
{
    SCB_InvalidateDCache_by_Addr((uint32_t *)adc_buf, ADC_BUF_SIZE * 2);
    memcpy(out, adc_buf, len * 2);
}

链接脚本中定义 .dma_buffer 段到 D2 SRAM:

.dma_buffer (NOLOAD) :
{
    . = ALIGN(32);
    *(.dma_buffer)
    . = ALIGN(32);
} > RAM_D2

避坑清单

  • 缓冲区地址和长度必须 32 字节对齐,否则维护函数可能越界或无效。
  • Invalidate 前先 Clean,避免丢弃 CPU 未写回的数据。
  • 不要在 DMA 传输过程中操作 Cache,否则数据竞争。
  • MPU 配置后要调用 SCB_EnableDCache(),且顺序不能乱。
  • 使用 __attribute__((aligned(32))) 或链接脚本对齐,别依赖编译器默认。
  • 调试时若出现 HardFault,先检查 MPU 区域是否与其它外设地址冲突。

小结

D-Cache 与 DMA 的一致性问题本质是“两个主设备看到的内存视图不同”。解决方案要么手动 Clean/Invalidate,要么用 MPU 把 DMA 缓冲区设为 Non-Cacheable。前者灵活但易错,后者一劳永逸但牺牲该区域的 Cache 性能。实际项目中,建议把 DMA 缓冲区统一放到 D2 SRAM 并配置 Non-Cacheable,既简单又可靠。