一、问题现象:DMA 数据错位

在 STM32H7 项目中,使用 DMA 搬运 ADC 采样、串口收发或 SPI 数据时,经常出现以下现象:

  • DMA 发送的数据与内存中实际数据不一致,接收端收到错位或随机字节。
  • DMA 接收完成后,CPU 读取缓冲区发现数据部分正确、部分为旧值。
  • 单步调试正常,全速运行出错;关闭 Cache 后问题消失。

这些现象几乎都指向同一个根因:Cortex-M7 的 D-Cache 与 DMA 直接访问物理内存之间缺乏一致性维护

二、原理:Cache 一致性为何成为问题

STM32H7 基于 Cortex-M7 内核,带有 L1 D-Cache 和 I-Cache。CPU 访问内存时,数据可能被缓存在 D-Cache 中,而 DMA 控制器直接访问 AXI SRAM 或 DTCM 等物理内存,不经过 Cache。

  • DMA 写内存,CPU 读:DMA 将新数据写入物理内存,但 CPU 可能仍从 D-Cache 读到旧数据。
  • CPU 写内存,DMA 读:CPU 写入的数据可能还在 D-Cache 中未回写到物理内存,DMA 读到旧数据。

解决方式有两种:

  1. 将 DMA 缓冲区放在 MPU 配置为 Non-Cacheable 的内存区域,一劳永逸。
  2. 在 DMA 传输前后手动执行 Cache 清理(Clean)和无效化(Invalidate),但需注意时序和地址对齐。

推荐做法:用 MPU 将 DMA 缓冲区所在区域配置为 Non-Cacheable 或 Write-Through,避免手动维护的复杂性和风险。

三、MPU 配置步骤

MPU(Memory Protection Unit)可将内存区域属性设置为 Normal、Device 或 Strongly-ordered,并控制 Cache 策略。

3.1 确定 DMA 缓冲区地址

假设使用 AXI SRAM 的 0x24000000 起始的 32KB 区域作为 DMA 缓冲区。

3.2 配置 MPU Region

以 HAL 库为例,在 main() 初始化早期调用:

#include "stm32h7xx_hal.h"

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

    HAL_MPU_Disable();

    /* Region 0: 0x24000000, 32KB, Non-Cacheable, 可共享 */
    MPU_InitStruct.Enable           = MPU_REGION_ENABLE;
    MPU_InitStruct.Number           = MPU_REGION_NUMBER0;
    MPU_InitStruct.BaseAddress      = 0x24000000;
    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_SHAREABLE;
    MPU_InitStruct.IsCacheable      = MPU_ACCESS_NOT_CACHEABLE;
    MPU_InitStruct.IsBufferable     = MPU_ACCESS_NOT_BUFFERABLE;

    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

关键点:

  • IsCacheable = NOT_CACHEABLEIsBufferable = NOT_BUFFERABLE 使该区域绕过 D-Cache。
  • IsShareable = SHAREABLE 保证多主机(CPU 与 DMA)之间一致性。
  • 区域大小必须与基地址对齐,例如 32KB 区域基地址低 15 位为 0。

3.3 将 DMA 缓冲区定义到该区域

#define DMA_BUF_ADDR  0x24000000
#define DMA_BUF_SIZE  1024

static uint8_t *dma_rx_buf = (uint8_t *)DMA_BUF_ADDR;

或使用链接脚本将 .dma_buffer 段定位到该地址。

四、完整代码示例:UART DMA 接收

以下示例使用 UART4 + DMA1 Stream0 接收不定长数据,缓冲区位于 Non-Cacheable 区域。

#include "stm32h7xx_hal.h"

UART_HandleTypeDef huart4;
DMA_HandleTypeDef hdma_uart4_rx;

#define DMA_BUF_ADDR  0x24000000
#define DMA_BUF_SIZE  256

static uint8_t *rx_buf = (uint8_t *)DMA_BUF_ADDR;

void UART4_Init(void)
{
    huart4.Instance = UART4;
    huart4.Init.BaudRate = 115200;
    huart4.Init.WordLength = UART_WORDLENGTH_8B;
    huart4.Init.StopBits = UART_STOPBITS_1;
    huart4.Init.Parity = UART_PARITY_NONE;
    huart4.Init.Mode = UART_MODE_TX_RX;
    huart4.Init.HwFlowCtl = UART_HWCONTROL_NONE;
    huart4.Init.OverSampling = UART_OVERSAMPLING_16;
    HAL_UART_Init(&huart4);
}

void DMA_Init(void)
{
    __HAL_RCC_DMA1_CLK_ENABLE();

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

    __HAL_LINKDMA(&huart4, hdmarx, hdma_uart4_rx);
}

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MPU_Config();          /* 必须先配置 MPU */
    UART4_Init();
    DMA_Init();

    HAL_UART_Receive_DMA(&huart4, rx_buf, DMA_BUF_SIZE);

    while (1)
    {
        /* 处理 rx_buf 中的数据,无需手动 Cache 维护 */
    }
}

五、排查方法与注意事项

5.1 快速定位

  • 关闭 D-Cache 测试:在 SCB_EnableDCache() 前加条件编译,若问题消失,则确认是 Cache 一致性导致。
  • 检查 MPU 配置是否生效:读取 MPU->RBARMPU->RASR 确认区域属性。
  • 检查 DMA 缓冲区地址是否落在配置的 Non-Cacheable 区域内。

5.2 注意事项

  • MPU 配置必须在使能 Cache 之前完成,否则已缓存的数据可能残留。
  • DMA 缓冲区大小不能超过 MPU Region 大小,否则超出部分仍走 Cache。
  • 不要对 Non-Cacheable 区域调用 SCB_CleanDCacheSCB_InvalidateDCache,虽然无害但无意义,且可能引入额外开销。
  • 若必须使用 Cacheable 区域,则需在 DMA 发送前 SCB_CleanDCache_by_Addr,接收后 SCB_InvalidateDCache_by_Addr,并确保地址 32 字节对齐。
  • DTCM 不支持 DMA,不要把 DMA 缓冲区放在 DTCM(0x20000000)。
  • 多缓冲区切换时,每个缓冲区都应位于 Non-Cacheable 区域,或分别维护 Cache。

六、总结

STM32H7 的 Cache 与 DMA 冲突是嵌入式开发中的经典问题。通过 MPU 将 DMA 缓冲区配置为 Non-Cacheable 和 Shareable,可以从根本上避免数据错位。本文给出的配置步骤和代码可直接用于 UART、SPI、ADC 等 DMA 场景。掌握 MPU 与 Cache 一致性机制,是发挥 H7 高性能的前提。