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

STM32H7 的 Cortex-M7 内核带 16KB I-Cache 和 16KB D-Cache。D-Cache 让 CPU 访问 SRAM 时无需等待,但 DMA 直接访问物理内存,不经过 Cache。当 CPU 写数据到 Cache 而未同步到 SRAM 时,DMA 读到的就是旧数据;反之 DMA 写入新数据到 SRAM,CPU 却可能从 Cache 读到旧值。这就是经典的 Cache 一致性问题。

关键结论:

  • CPU 写、DMA 读 → 必须 Clean(把 Cache 写回 SRAM)
  • DMA 写、CPU 读 → 必须 Invalidate(丢弃 Cache 旧数据,强制从 SRAM 重读)
  • 顺序错误会导致数据丢失或 HardFault

二、Clean 与 Invalidate 的正确操作顺序

2.1 发送方向(CPU→DMA)

// 1. CPU 填充缓冲区
fill_buffer(tx_buf, len);
// 2. Clean:将 Cache 内容写回 SRAM
SCB_CleanDCache_by_Addr((uint32_t*)tx_buf, len);
// 3. 启动 DMA 发送
HAL_DMA_Start(&hdma, (uint32_t)tx_buf, (uint32_t)&UART->TDR, len);

注意:Clean 必须在启动 DMA 之前,否则 DMA 可能读到未同步的旧数据。

2.2 接收方向(DMA→CPU)

// 1. 启动 DMA 接收(此时不要 Invalidate!)
HAL_DMA_Start(&hdma, (uint32_t)&UART->RDR, (uint32_t)rx_buf, len);
// 2. 等待 DMA 完成(中断或轮询)
while(!dma_done);
// 3. Invalidate:丢弃 Cache 旧数据,强制从 SRAM 重读
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, len);
// 4. CPU 读取数据
process_buffer(rx_buf, len);

致命错误:在 DMA 传输过程中 Invalidate,可能把 DMA 刚写入但未完成的数据丢弃,导致数据错乱。

三、MPU 配置:让 DMA 缓冲区“绕过”Cache

每次手动 Clean/Invalidate 繁琐且易漏。更优雅的方案是用 MPU 将 DMA 缓冲区配置为 Non-Cacheable,一劳永逸。

3.1 MPU 配置步骤

void MPU_Config(void)
{
    HAL_MPU_Disable();
    MPU_Region_InitTypeDef MPU_Init = {0};

    // 配置 SRAM1 的 0x30000000 起始 32KB 为 Non-Cacheable
    MPU_Init.Enable = MPU_REGION_ENABLE;
    MPU_Init.Number = MPU_REGION_NUMBER0;
    MPU_Init.BaseAddress = 0x30000000;
    MPU_Init.Size = MPU_REGION_SIZE_32KB;
    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_NOT_SHAREABLE;
    MPU_Init.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;  // 关键:禁止 Cache
    MPU_Init.IsBufferable = MPU_ACCESS_BUFFERABLE;
    HAL_MPU_ConfigRegion(&MPU_Init);

    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

3.2 链接脚本中放置 DMA 缓冲区

// 在 .ld 文件中定义段
MEMORY {
  DMA_RAM (xrw) : ORIGIN = 0x30000000, LENGTH = 32K
}
SECTIONS {
  .dma_buffer : { *(.dma_buffer) } > DMA_RAM
}

// C 代码中指定段
__attribute__((section(".dma_buffer"))) uint8_t adc_dma_buf[1024];

这样 DMA 缓冲区永远不经过 Cache,无需手动维护一致性。

四、完整实战:ADC + DMA 采集

#define ADC_BUF_LEN 256
__attribute__((section(".dma_buffer"))) uint16_t adc_buf[ADC_BUF_LEN];

void ADC_DMA_Init(void)
{
    // ADC 配置(略)
    hadc1.Init.ContinuousConvMode = ENABLE;
    hadc1.Init.DMAContinuousRequests = ENABLE;
    HAL_ADC_Init(&hadc1);

    // DMA 配置
    hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
    hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
    hdma_adc1.Init.Mode = DMA_CIRCULAR;
    HAL_DMA_Init(&hdma_adc1);
    __HAL_LINKDMA(&hadc1, DMA_Handle, hdma_adc1);

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

// 处理数据时直接读,无需 Invalidate
void Process_ADC_Data(void)
{
    uint32_t sum = 0;
    for(int i = 0; i < ADC_BUF_LEN; i++) {
        sum += adc_buf[i];
    }
    float avg = (float)sum / ADC_BUF_LEN;
}

五、避坑指南与注意事项

  • Invalidate 地址必须 32 字节对齐:Cortex-M7 Cache 行大小为 32 字节,非对齐地址会触发 HardFault。使用 SCB_InvalidateDCache_by_Addr 时确保地址和长度都是 32 的倍数。
  • 不要对同一区域同时 Clean 和 Invalidate:先 Clean 再 Invalidate 会丢失刚写回的数据,除非你明确知道自己在做什么。
  • DMA 传输中禁止 Invalidate:这是最常见的 HardFault 来源,务必等 DMA 完成中断后再操作。
  • MPU 配置后需调用 SCB_EnableDCache():MPU 只定义属性,Cache 使能需单独调用。
  • 调试时关闭 Cache:在 IDE 中可临时关闭 D-Cache 定位问题,但量产必须开启以发挥 H7 性能。
  • 使用 __DSB() 和 __ISB():在关键操作后插入内存屏障,确保指令顺序执行。

掌握以上要点,你就能在 STM32H7 上安全地同时使用 Cache 和 DMA,既享受高性能,又避免数据一致性陷阱。