一、问题现象:DMA 数据错乱从何而来

在 STM32H7 上使用 DMA 传输 ADC 采样、串口数据或 SPI 帧时,常遇到以下现象:

  • DMA 传输完成中断触发,但缓冲区数据部分为旧值或全 0
  • CPU 写入缓冲区后启动 DMA,发送出去的数据却是修改前的
  • 数据偶发错误,且与编译器优化等级、内存布局相关

根本原因在于 Cortex-M7 的 D-Cache 与 DMA 控制器访问内存的路径不同

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

2.1 两条访问路径

  • CPU 路径:CPU 读写内存时,若地址在 Cacheable 区域,数据会先进入 D-Cache,不一定立即写回 SRAM。
  • DMA 路径:DMA 控制器直接访问 SRAM,不经过 D-Cache。

因此:

  • CPU 写数据到缓冲区 → 数据可能还在 Cache 中,未写入 SRAM → DMA 读到旧数据。
  • DMA 写数据到缓冲区 → 数据已写入 SRAM → CPU 读时若 Cache 中有旧副本,则读到旧数据。

2.2 常见但不够优雅的解决方案

  • 使用 SCB_CleanDCache_by_Addr()SCB_InvalidateDCache_by_Addr() 手动维护一致性。
  • 缺点:调用时机易错、地址对齐要求严格、性能开销大。

2.3 推荐方案:MPU 配置 Non-Cacheable 区域

将 DMA 缓冲区所在的内存区域通过 MPU 配置为 Non-Cacheable,CPU 和 DMA 都直接访问 SRAM,从根本上避免一致性问题。

三、实操步骤:MPU 配置 Non-Cacheable 区域

以 STM32H743 为例,将 0x30000000 开始的 32KB D2 SRAM 配置为 Non-Cacheable,用于 DMA 缓冲区。

3.1 确定内存区域

  • D2 SRAM 地址范围:0x30000000 ~ 0x30048000(不同型号略有差异)
  • 选择其中一段,例如 0x30000000 起始的 32KB

3.2 配置 MPU

#include "stm32h7xx.h"

void MPU_Config(void)
{
    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    HAL_MPU_Disable();

    /* 配置 Region 0:D2 SRAM 32KB 为 Non-Cacheable */
    MPU_InitStruct.Enable           = MPU_REGION_ENABLE;
    MPU_InitStruct.Number           = MPU_REGION_NUMBER0;
    MPU_InitStruct.BaseAddress      = 0x30000000;
    MPU_InitStruct.Size             = MPU_REGION_SIZE_32KB;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.TypeExtField     = MPU_TEX_LEVEL0;
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.DisableExec      = MPU_INSTRUCTION_ACCESS_DISABLE;
    MPU_InitStruct.IsShareable      = MPU_ACCESS_NOT_SHAREABLE;
    MPU_InitStruct.IsCacheable      = MPU_ACCESS_NOT_CACHEABLE;
    MPU_InitStruct.IsBufferable     = MPU_ACCESS_NOT_BUFFERABLE;

    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    /* 使能 MPU */
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

3.3 在 main 中调用

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MPU_Config();          // 必须在使能 D-Cache 之前配置
    SCB_EnableICache();
    SCB_EnableDCache();

    // 后续初始化 DMA 和外设
    ...
}

3.4 定义 DMA 缓冲区

#define DMA_BUF_SIZE  1024

/* 强制放在 0x30000000 区域,且 32 字节对齐 */
__attribute__((section(".dma_buffer"), aligned(32)))
uint8_t dma_buffer[DMA_BUF_SIZE];

在链接脚本中添加段:

.dma_buffer (NOLOAD) :
{
  . = ALIGN(32);
  *(.dma_buffer)
  . = ALIGN(32);
} > RAM_D2

四、完整示例:DMA 串口接收

UART_HandleTypeDef huart1;
DMA_HandleTypeDef hdma_usart1_rx;

void DMA_Init(void)
{
    __HAL_RCC_DMA1_CLK_ENABLE();

    hdma_usart1_rx.Instance = DMA1_Stream0;
    hdma_usart1_rx.Init.Request = DMA_REQUEST_USART1_RX;
    hdma_usart1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
    hdma_usart1_rx.Init.PeriphInc = DMA_PINC_DISABLE;
    hdma_usart1_rx.Init.MemInc = DMA_MINC_ENABLE;
    hdma_usart1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
    hdma_usart1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
    hdma_usart1_rx.Init.Mode = DMA_NORMAL;
    hdma_usart1_rx.Init.Priority = DMA_PRIORITY_HIGH;
    HAL_DMA_Init(&hdma_usart1_rx);

    __HAL_LINKDMA(&huart1, hdmarx, hdma_usart1_rx);
}

void Start_Receive(void)
{
    HAL_UART_Receive_DMA(&huart1, dma_buffer, DMA_BUF_SIZE);
}

五、注意事项

  • MPU 配置必须在 SCB_EnableDCache() 之前,否则可能不生效。
  • DMA 缓冲区必须 32 字节对齐,即使配置为 Non-Cacheable,对齐仍能避免其他问题。
  • 若使用 SCB_InvalidateDCache_by_Addr,地址必须按 Cache Line(32 字节)对齐,长度也需对齐。
  • 不要将整个 SRAM 都设为 Non-Cacheable,会严重降低性能。
  • 不同 STM32H7 型号的 SRAM 地址范围不同,请查阅参考手册。
  • 使用 __attribute__((section())) 时,确保链接脚本正确分配。

六、验证方法

  • 在 DMA 传输前后打印缓冲区内容,观察是否一致。
  • 使用调试器查看内存窗口,确认数据正确写入 SRAM。
  • 对比开启/关闭 D-Cache 的行为,确认问题消失。

通过 MPU 将 DMA 缓冲区配置为 Non-Cacheable,是 STM32H7 上解决 Cache 一致性问题最干净、最可靠的方法。掌握这一技能,能让你在高性能嵌入式开发中少走弯路。