一、现象:DMA 数据为何“时对时错”?

在 STM32H7 上使用 DMA 搬运 ADC 采样、串口数据或外设缓冲区时,常遇到以下现象:

  • 单步调试时数据完全正确,全速运行却随机出错;
  • 关闭 D-Cache 后一切正常,开启后 DMA 目标缓冲区数据错乱;
  • 发送 DMA 数据时,源缓冲区内容被“旧值”覆盖,或接收数据部分丢失;
  • 使用 SCB_InvalidateDCache 后偶尔出现更严重的错误。

这些现象的根本原因在于 Cortex-M7 的 D-Cache 与 DMA 控制器访问内存时缺乏一致性。CPU 通过 Cache 读写,DMA 直接访问物理内存,两者看到的数据可能不同步。

二、原理:为什么 D-Cache 会与 DMA 冲突?

Cortex-M7 的 D-Cache 是写回(Write-Back)、写分配(Write-Allocate) 策略。当 CPU 写数据时,数据先写入 Cache,并不立即写回主存;当 CPU 读数据时,若 Cache 未命中,则从主存加载到 Cache。

DMA 则绕过 Cache,直接读写主存。因此:

  • CPU 写 → DMA 读:CPU 写入的数据可能还在 Cache 中,DMA 读到的是旧的主存数据。
  • DMA 写 → CPU 读:DMA 写入新数据到主存,但 CPU 可能从 Cache 中读到旧数据。
  • Cache 行(Cache Line):M7 的 Cache 行大小为 32 字节。若 DMA 缓冲区未按 32 字节对齐,Invalidate 操作可能丢弃相邻变量,导致数据损坏。

三、排查流程:从现象到 MPU 配置

3.1 确认问题是否由 Cache 引起

  • 临时关闭 D-Cache(SCB_DisableDCache()),若问题消失,则基本确定是 Cache 一致性问题。
  • 注意:关闭 Cache 会降低性能,仅用于验证。

3.2 检查 DMA 缓冲区属性

  • 缓冲区是否定义在可 Cache 区域(如 DTCM、AXI SRAM)?
  • 是否按 32 字节对齐?使用 __attribute__((aligned(32)))
  • 是否使用了 volatile?对 DMA 缓冲区,volatile 只能防止编译器优化,不能解决 Cache 一致性。

3.3 使用 Cache 维护操作(临时方案)

  • DMA 发送前:调用 SCB_CleanDCache_by_Addr() 将 CPU 写入的数据写回主存。
  • DMA 接收后:调用 SCB_InvalidateDCache_by_Addr() 丢弃 Cache 中的旧数据,强制从主存重新加载。
  • 注意:Invalidate 操作必须按 32 字节对齐,且不能作用于包含其他变量的 Cache 行。
// 示例:DMA 发送前清理 Cache
#define BUFFER_SIZE  128
__attribute__((aligned(32))) uint8_t tx_buffer[BUFFER_SIZE];

void DMA_Send(void) {
    SCB_CleanDCache_by_Addr((uint32_t*)tx_buffer, BUFFER_SIZE);
    HAL_DMA_Start(&hdma, (uint32_t)tx_buffer, (uint32_t)&UART->DR, BUFFER_SIZE);
}

// 示例:DMA 接收后无效化 Cache
__attribute__((aligned(32))) uint8_t rx_buffer[BUFFER_SIZE];

void DMA_ReceiveCompleteCallback(void) {
    SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buffer, BUFFER_SIZE);
    // 此时 CPU 读取 rx_buffer 才能获得 DMA 写入的新数据
}

3.4 终极方案:MPU 配置为 Non-Cacheable

频繁调用 Cache 维护函数会增加 CPU 开销,且容易遗漏。更优雅的方式是使用 MPU 将 DMA 缓冲区所在内存区域配置为 Non-CacheableWrite-Through

以 STM32H7 为例,将 AXI SRAM 的某一段(如 0x24000000 ~ 0x2400FFFF)配置为 Non-Cacheable:

#include "stm32h7xx_hal.h"

void MPU_Config(void) {
    HAL_MPU_Disable();

    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    // 区域 0:DMA 缓冲区,Non-Cacheable
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = 0x24000000;
    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_LEVEL0;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;

    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

配置后,该区域所有访问都绕过 D-Cache,DMA 与 CPU 看到的内存完全一致,无需再手动维护 Cache。

四、完整示例:ADC + DMA + D-Cache 安全采集

#define ADC_BUFFER_SIZE  256
__attribute__((aligned(32))) uint16_t adc_buffer[ADC_BUFFER_SIZE];

void ADC_DMA_Init(void) {
    // 假设已配置好 ADC 和 DMA,DMA 目标为 adc_buffer
    HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buffer, ADC_BUFFER_SIZE);
}

// DMA 传输完成回调
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) {
    // 若未使用 MPU Non-Cacheable,必须无效化 Cache
    SCB_InvalidateDCache_by_Addr((uint32_t*)adc_buffer, ADC_BUFFER_SIZE * 2);

    // 此时读取 adc_buffer 才是 DMA 写入的最新值
    uint16_t value = adc_buffer[0];
    // ... 处理数据
}

五、注意事项与避坑指南

  • 对齐:所有 DMA 缓冲区必须 32 字节对齐,否则 Invalidate 会破坏相邻数据。
  • Cache 行共享:若多个变量位于同一 Cache 行,Invalidate 会丢弃其他变量的修改,建议将 DMA 缓冲区单独放在一个区域。
  • MPU 配置时机:在 main() 初始化阶段、使能 Cache 之前配置 MPU。
  • 性能权衡:Non-Cacheable 区域会降低 CPU 访问速度,适合 DMA 缓冲区,不适合频繁 CPU 读写的数据。
  • 多核场景:STM32H7 双核(CM4 + CM7)共享内存时,还需考虑核间通信的 Cache 一致性。
  • 调试技巧:使用 SCB_InvalidateDCache() 全无效化可快速验证,但会严重影响性能,仅用于调试。

六、总结

STM32H7 的 D-Cache 与 DMA 冲突是嵌入式开发中的经典问题。排查时先关闭 Cache 确认现象,再检查缓冲区对齐与属性,临时可用 Cache 维护函数,最终推荐使用 MPU 将 DMA 区域配置为 Non-Cacheable。理解 Cache 行、写回策略与 MPU 机制,才能写出既高效又稳定的代码。