为什么 STM32H7 的 DMA 会“丢数据”?

STM32H7 主频高达 480MHz,为了匹配内核速度,Cortex-M7 引入了 D-Cache(数据缓存)。CPU 访问数据时先查 Cache,命中则直接返回,不访问 SRAM。而 DMA 是独立于 CPU 的总线主设备,它直接读写 SRAM,不经过 Cache

这就带来一个经典问题:

  • CPU 写数据到缓冲区,数据可能还在 Cache 中,未写回 SRAM,DMA 读到的就是旧数据。
  • DMA 把新数据写入 SRAM,但 CPU 读的是 Cache 中的旧副本,看不到新数据。

因此,DMA 与 D-Cache 共存时,必须做一致性维护

一致性问题的三个层面

1. 地址对齐与 Cache Line

Cortex-M7 的 D-Cache Line 大小通常为 32 字节。Cache 维护操作(clean/invalidate)以 Cache Line 为单位。

  • 如果缓冲区起始地址或长度不是 32 字节对齐,clean/invalidate 可能影响相邻变量,造成“误伤”。
  • 例如:一个 10 字节的 DMA 接收缓冲区,若未对齐,invalidate 时会把同一 Cache Line 中其他变量也失效,导致数据丢失。

建议:DMA 缓冲区使用 __attribute__((aligned(32))) 强制 32 字节对齐,长度也尽量按 32 字节对齐。

2. Cache 维护操作:Clean 与 Invalidate

  • Clean:将 Cache 中已修改的数据写回 SRAM。用于 CPU 写、DMA 读的场景。
  • Invalidate:丢弃 Cache 中的副本,强制下次从 SRAM 读取。用于 DMA 写、CPU 读的场景。
  • Clean+Invalidate:两者都做,用于双向传输。

注意:invalidate 前必须确保 Cache 中没有未写回的数据,否则会丢失 CPU 的修改。

3. MPU 配置:把 DMA 缓冲区设为 Non-Cacheable

更彻底的做法是用 MPU 将 DMA 缓冲区所在内存区域配置为 Non-CacheableWrite-Through。这样 CPU 访问该区域时不经过 D-Cache,从根本上避免一致性问题。

  • 优点:无需手动维护 Cache,代码简单。
  • 缺点:CPU 访问该区域变慢,适合 DMA 频繁但 CPU 计算不多的缓冲区。

完整排查链路

  1. 确认 DMA 缓冲区地址是否 32 字节对齐。
  2. 确认是否在 DMA 传输前后正确执行了 Cache 维护。
  3. 确认 MPU 是否将 DMA 区域配置为 Non-Cacheable。
  4. 确认 DMA 传输方向与 Cache 操作匹配。
  5. 使用调试器查看 SRAM 与 Cache 内容,验证一致性。

代码示例:ADC + DMA + D-Cache 安全采集

以下示例使用 MPU 将 DMA 缓冲区设为 Non-Cacheable,并保留手动维护接口。

#include "stm32h7xx.h"

#define ADC_DMA_BUFFER_SIZE  256

/* 32 字节对齐,避免 Cache Line 误伤 */
__attribute__((aligned(32))) 
uint16_t adc_dma_buffer[ADC_DMA_BUFFER_SIZE];

/* MPU 配置:将 adc_dma_buffer 所在区域设为 Non-Cacheable */
void MPU_Config_DMA_NonCacheable(void)
{
    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    HAL_MPU_Disable();

    /* 区域基地址必须按区域大小对齐,这里取缓冲区地址向下对齐到 32 字节 */
    uint32_t base = ((uint32_t)adc_dma_buffer) & ~0x1F;

    MPU_InitStruct.Enable           = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress      = base;
    MPU_InitStruct.Size             = MPU_REGION_SIZE_256B; /* 根据实际大小调整 */
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.IsBufferable     = MPU_ACCESS_NOT_BUFFERABLE;
    MPU_InitStruct.IsCacheable      = MPU_ACCESS_NOT_CACHEABLE; /* 关键:Non-Cacheable */
    MPU_InitStruct.IsShareable      = MPU_ACCESS_SHAREABLE;
    MPU_InitStruct.Number           = MPU_REGION_NUMBER0;
    MPU_InitStruct.TypeExtField     = MPU_TEX_LEVEL0;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.DisableExec      = MPU_INSTRUCTION_ACCESS_ENABLE;

    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

/* 若未使用 MPU,可手动维护 Cache */
void DMA_Buffer_Clean(void *addr, uint32_t size)
{
    SCB_CleanDCache_by_Addr((uint32_t *)addr, size);
}

void DMA_Buffer_Invalidate(void *addr, uint32_t size)
{
    SCB_InvalidateDCache_by_Addr((uint32_t *)addr, size);
}

/* ADC + DMA 初始化 */
void ADC_DMA_Init(void)
{
    /* 1. 配置 MPU,使缓冲区 Non-Cacheable */
    MPU_Config_DMA_NonCacheable();

    /* 2. 初始化 DMA,目标地址为 adc_dma_buffer */
    /* ... HAL_DMA_Init ... */

    /* 3. 启动 ADC DMA 循环采集 */
    /* HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_dma_buffer, ADC_DMA_BUFFER_SIZE); */
}

/* 若使用 Cacheable 缓冲区,DMA 启动前需 Clean,DMA 完成后需 Invalidate */
void ADC_DMA_Start_WithCache(void)
{
    /* CPU 可能修改过缓冲区,先 Clean 写回 */
    DMA_Buffer_Clean(adc_dma_buffer, sizeof(adc_dma_buffer));

    /* 启动 DMA */
    /* HAL_ADC_Start_DMA(...); */
}

void ADC_DMA_Complete_Callback(void)
{
    /* DMA 写入新数据,Invalidate 使 CPU 读取 SRAM 新值 */
    DMA_Buffer_Invalidate(adc_dma_buffer, sizeof(adc_dma_buffer));

    /* 此时 CPU 读取 adc_dma_buffer 才是最新数据 */
}

注意事项

  • 对齐是前提:DMA 缓冲区必须 32 字节对齐,否则 Cache 维护会误伤相邻数据。
  • Clean 与 Invalidate 顺序:CPU 写后 DMA 读,先 Clean;DMA 写后 CPU 读,先 Invalidate。
  • MPU 区域大小:MPU 区域大小必须是 2 的幂,且基地址按大小对齐。
  • 不要对同一区域同时使用 MPU Non-Cacheable 和手动维护:二者选其一,避免重复操作。
  • DMA 描述符与缓冲区分离:若使用链表,描述符本身也要注意对齐和 Cache 维护。
  • 调试技巧:在调试器中查看 SRAM 地址内容,对比 CPU 读取值,可快速定位一致性问题。

小结

STM32H7 的 D-Cache 与 DMA 一致性问题的核心是“CPU 与 DMA 看到的内存视图不同”。解决思路有三层:地址对齐、Cache 维护、MPU 配置。实际项目中,推荐将 DMA 缓冲区用 MPU 设为 Non-Cacheable,简单可靠;若必须使用 Cacheable 内存,则严格按方向执行 Clean/Invalidate,并保证 32 字节对齐。掌握这条完整链路,就能让 H7 的高性能与 DMA 的实时性兼得。