一、为什么 H7 上 DMA 会“偷走”你的数据?

STM32H7 的 Cortex-M7 内核带有 D-Cache(数据缓存)和 I-Cache。当 CPU 访问内存时,数据可能被缓存在 D-Cache 中,而 DMA 直接访问物理内存(SRAM/SDRAM)。如果 CPU 写数据后未回写(Clean),DMA 读到的就是旧值;如果 DMA 写数据后 CPU 仍读缓存中的旧值,就需要 Invalidate。

典型现象

  • ADC/DMA 采集的数据,CPU 读出来全是 0 或不变。
  • 串口 DMA 发送的数据错位、重复。
  • 以太网收包后协议栈解析异常。

根本原因:Cache 与 DMA 看到的内存视图不一致

二、MPU 配置:给 DMA 缓冲区“开小灶”

最稳妥的方案是用 MPU 将 DMA 缓冲区配置为 Non-CacheableWrite-Through,让 CPU 和 DMA 看到同一份数据。

2.1 MPU 区域规划

  • 区域 0:0x30000000(SRAM1),大小 128KB,属性 Normal Non-Cacheable。
  • 区域 1:0x30020000(SRAM2),大小 128KB,属性 Normal Write-Through。

2.2 配置代码(基于 HAL)

#include "stm32h7xx_hal.h"

void MPU_Config(void)
{
    HAL_MPU_Disable();

    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    /* 配置 SRAM1 为 Non-Cacheable */
    MPU_InitStruct.Enable           = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress      = 0x30000000;
    MPU_InitStruct.Size             = MPU_REGION_SIZE_128KB;
    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_LEVEL0;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.DisableExec      = MPU_INSTRUCTION_ACCESS_ENABLE;
    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    /* 配置 SRAM2 为 Write-Through */
    MPU_InitStruct.BaseAddress      = 0x30020000;
    MPU_InitStruct.Number           = MPU_REGION_NUMBER1;
    MPU_InitStruct.IsCacheable      = MPU_ACCESS_CACHEABLE;
    MPU_InitStruct.IsBufferable     = MPU_ACCESS_NOT_BUFFERABLE;
    MPU_InitStruct.TypeExtField     = MPU_TEX_LEVEL0;
    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

注意:MPU 配置必须在 main() 最开始、使能 Cache 之前调用。

三、Clean / Invalidate 的正确使用

如果不想牺牲性能,可将缓冲区设为 Cacheable,但必须在 DMA 传输前后手动维护一致性。

3.1 关键 API

  • SCB_CleanDCache_by_Addr(addr, size):将 Cache 数据写回内存。
  • SCB_InvalidateDCache_by_Addr(addr, size):丢弃 Cache 数据,强制从内存重读。
  • SCB_CleanInvalidateDCache_by_Addr(addr, size):先 Clean 再 Invalidate。

3.2 方向决定操作

  • CPU 写 → DMA 读(如发送):先 Clean
  • DMA 写 → CPU 读(如接收):先 Invalidate
  • 双向:先 CleanInvalidate

3.3 实战代码:ADC DMA 采集

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

void ADC_DMA_Start(void)
{
    /* 启动前 Invalidate,确保 CPU 不读旧缓存 */
    SCB_InvalidateDCache_by_Addr((uint32_t*)adc_buf, sizeof(adc_buf));
    HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf, ADC_BUF_SIZE);
}

void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
    /* 传输完成后 Invalidate,再读取数据 */
    SCB_InvalidateDCache_by_Addr((uint32_t*)adc_buf, sizeof(adc_buf));
    for (int i = 0; i < ADC_BUF_SIZE; i++) {
        process(adc_buf[i]);
    }
}

易错点

  • 地址必须 32 字节对齐,否则 SCB_* 函数可能无效或触发 HardFault。
  • 长度必须是 32 的倍数,建议用 __attribute__((aligned(32)))
  • 不要在 DMA 传输过程中 Invalidate,否则可能丢失未回写数据。

四、排查步骤与注意事项

4.1 快速定位

  1. 确认 DMA 缓冲区地址是否落在 MPU 配置的 Non-Cacheable 区域。
  2. 若为 Cacheable,检查传输前后是否调用了正确的 Clean/Invalidate。
  3. SCB_InvalidateDCache() 全局失效测试,若问题消失则确认是 Cache 问题。
  4. 检查链接脚本,确保缓冲区未被分配到 DTCM(DTCM 不经过 Cache,但 DMA 无法访问)。

4.2 注意事项

  • DTCM 与 DMA:DTCM 地址 0x20000000 不能被大多数 DMA 访问,必须放在 AXI SRAM 或 SDRAM。
  • 多缓冲区:每个缓冲区独立维护,不要跨区域操作。
  • 中断优先级:DMA 完成中断中操作 Cache 要快,避免影响实时性。
  • 性能权衡:Non-Cacheable 简单但降低 CPU 访问速度;Clean/Invalidate 高效但易错。

五、总结

STM32H7 的 Cache 与 DMA 一致性没有“银弹”,推荐优先使用 MPU 将 DMA 缓冲区设为 Non-Cacheable,简单可靠。若必须用 Cacheable,务必牢记:CPU 写后 Clean,DMA 写后 Invalidate,地址对齐 32 字节。掌握这些,你就能避开 H7 上最经典的“数据错乱”坑。