一、为什么 Cache 会成为 DMA 的“猪队友”?

STM32H7 搭载 Cortex-M7,主频可达 480MHz,为了弥补 CPU 与存储器之间的速度鸿沟,芯片内部集成了 L1 数据 Cache(D-Cache) 和 指令 Cache(I-Cache)。Cache 的工作逻辑很简单:CPU 访问内存时,先把数据从主存读到 Cache 行(通常 32 字节),之后读写都优先命中 Cache,从而减少访问延迟。

但 DMA 是“独立于 CPU 的搬运工”,它直接访问物理内存,不经过 Cache。这就导致两种典型问题:

  • 读旧数据:CPU 刚更新了内存,数据还在 Cache 里没写回主存,DMA 却从主存读到旧值。
  • 写丢失:DMA 把新数据写入主存,但 CPU 访问时命中了 Cache 中的旧数据,看不到更新。

一句话总结:Cache 和 DMA 看到的内存可能不是同一份。

二、STM32H7 的 Cache 维护机制

Cortex-M7 提供了维护 Cache 一致性的指令,HAL 库封装为以下函数:

  • SCB_CleanDCache_by_Addr(addr, size):将 Cache 内容写回主存(Clean),用于 CPU 写、DMA 读 的场景。
  • SCB_InvalidateDCache_by_Addr(addr, size):丢弃 Cache 内容,强制下次从主存读取(Invalidate),用于 DMA 写、CPU 读 的场景。
  • SCB_CleanInvalidateDCache_by_Addr(addr, size):先写回再无效,用于双向传输。

关键细节:操作地址必须按 32 字节对齐,长度也建议向上取整到 32 字节,否则会误伤相邻数据。

三、实战配置:MPU + Cache 维护

最稳妥的方案是 MPU 配置 DMA 缓冲区为 Write-Through 或 Non-Cacheable,从根源避免一致性问题。以下以 ADC + DMA 采集为例。

3.1 MPU 配置(推荐)

#include "stm32h7xx_hal.h"

// 定义 DMA 缓冲区,强制 32 字节对齐
__attribute__((aligned(32))) uint16_t adc_dma_buf[256];

void MPU_Config(void)
{
    HAL_MPU_Disable();

    MPU_Region_InitTypeDef MPU_Init = {0};
    MPU_Init.Enable           = MPU_REGION_ENABLE;
    MPU_Init.Number           = MPU_REGION_NUMBER0;
    MPU_Init.BaseAddress      = (uint32_t)adc_dma_buf;
    MPU_Init.Size             = MPU_REGION_SIZE_1KB;   // 根据缓冲区大小调整
    MPU_Init.SubRegionDisable = 0x00;
    MPU_Init.TypeExtField     = MPU_TEX_LEVEL0;
    MPU_Init.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_Init.DisableExec      = MPU_INSTRUCTION_ACCESS_DISABLE;
    MPU_Init.IsShareable      = MPU_ACCESS_SHAREABLE;
    MPU_Init.IsCacheable      = MPU_ACCESS_NOT_CACHEABLE; // 关键:不缓存
    MPU_Init.IsBufferable     = MPU_ACCESS_BUFFERABLE;

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

3.2 手动维护 Cache(灵活方案)

若不想用 MPU,必须在 DMA 传输前后手动维护:

// ADC 采集完成回调(DMA 写内存,CPU 读)
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
    // 无效化 Cache,确保 CPU 读到 DMA 写入的新数据
    SCB_InvalidateDCache_by_Addr((uint32_t*)adc_dma_buf, sizeof(adc_dma_buf));
    // 此时再处理 adc_dma_buf 数据
}

// 发送数据前(CPU 写内存,DMA 读)
void UART_DMA_Send(uint8_t *data, uint16_t len)
{
    // 将 Cache 内容写回主存,确保 DMA 读到最新数据
    SCB_CleanDCache_by_Addr((uint32_t*)data, len);
    HAL_UART_Transmit_DMA(&huart1, data, len);
}

四、完整示例:ADC + DMA + Cache 维护

#include "stm32h7xx_hal.h"

ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;

__attribute__((aligned(32))) uint16_t adc_buf[128];
volatile uint8_t adc_ready = 0;

void ADC_DMA_Init(void)
{
    // ADC 配置省略,重点看 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);

    HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf, 128);
}

// DMA 半传输/完成回调中处理数据
void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc)
{
    SCB_InvalidateDCache_by_Addr((uint32_t*)adc_buf, 64 * sizeof(uint16_t));
    adc_ready = 1;
}

void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
    SCB_InvalidateDCache_by_Addr((uint32_t*)&adc_buf[64], 64 * sizeof(uint16_t));
    adc_ready = 2;
}

五、实战排查清单

遇到 DMA 数据错乱时,按以下顺序排查:

  • 确认缓冲区对齐:DMA 缓冲区必须 32 字节对齐,使用 __attribute__((aligned(32)))。
  • 检查 MPU 配置:DMA 缓冲区是否被配置为 Non-Cacheable 或 Write-Through。
  • 核对维护时机:CPU 读之前 Invalidate,CPU 写之后 Clean,顺序不能反。
  • 注意地址范围:维护的地址和长度必须覆盖整个 DMA 传输区域,且 32 字节对齐。
  • 区分读写方向:外设到内存用 Invalidate,内存到外设用 Clean,双向用 CleanInvalidate。
  • 中断优先级:DMA 中断优先级不要高于系统关键中断,避免维护操作被抢占导致时序错乱。

六、注意事项

  • 不要频繁全 Cache 维护:SCB_InvalidateDCache() 会清空整个 D-Cache,严重影响性能,务必使用按地址维护的版本。
  • 多缓冲区场景:若使用双缓冲,每个缓冲区都要独立维护,不能只维护首地址。
  • DMA 与 CPU 竞争:维护 Cache 期间若 DMA 仍在传输,可能读到半新半旧数据,建议在 DMA 传输完成中断中操作。
  • 调试时关闭 Cache:初期调试可暂时关闭 D-Cache 验证功能,确认无误后再开启并加入维护逻辑。

掌握 Cache 与 DMA 的协作规则,是 STM32H7 高性能应用开发的必修课。合理使用 MPU 配置 + 按地址维护,既能享受 Cache 带来的性能红利,又能保证数据一致性。