一、问题现象与根源

在STM32H7上使用DMA搬运数据时,常遇到:

  • DMA发送的数据不是最新写入的;
  • DMA接收的数据CPU读不到,或读到旧值;
  • 偶发数据错位,调试时正常,全速运行出错。

根源在于D-Cache与DMA访问同一块内存时,CPU看到的是Cache中的副本,而DMA直接访问物理内存。当CPU写数据时,若命中Cache且未回写,DMA读到的是旧内存;当DMA写数据时,CPU若命中Cache,读到的仍是旧缓存。

解决思路有三:

  • 使用SCB_CleanDCache_by_Addr / SCB_InvalidateDCache_by_Addr手动维护;
  • 将DMA缓冲区放到Non-Cacheable区域(推荐,一劳永逸);
  • 使用DTCM/ITCM(但DMA无法访问DTCM,需注意)。

本文重点讲解MPU配置Non-Cacheable区域的实操。

二、MPU配置原理

Cortex-M7的MPU(Memory Protection Unit)可将物理地址划分为多个区域,每个区域独立设置属性。关键属性:

  • TEX、C、B:决定Cache策略。C=0、B=0、TEX=0 即为Non-Cacheable。
  • AP:访问权限,通常设为全访问。
  • XN:禁止取指,数据区可设1。

STM32H7的MPU有16个区域,区域0~3可重叠,优先级由区域号决定(编号越大优先级越高)。我们通常将DMA缓冲区所在SRAM区域配置为Non-Cacheable。

三、实操步骤

步骤1:确定DMA缓冲区地址

假设使用D2域SRAM1,起始地址0x30000000,大小32KB。

#define DMA_BUF_ADDR   0x30000000
#define DMA_BUF_SIZE   0x8000   // 32KB

步骤2:配置MPU区域

在系统初始化时(如main开头)调用:

#include "stm32h7xx.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.Number           = MPU_REGION_NUMBER0;
    MPU_InitStruct.BaseAddress      = DMA_BUF_ADDR;
    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;  // C=0
    MPU_InitStruct.IsBufferable     = MPU_ACCESS_NOT_BUFFERABLE; // B=0

    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    // 可选:配置其他区域,如Flash为Cacheable
    // ...

    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

步骤3:将DMA缓冲区定位到该区域

在链接脚本或代码中指定:

__attribute__((section(".dma_buffer"))) uint8_t dma_buffer[DMA_BUF_SIZE];

并在链接脚本中把.dma_buffer放到0x30000000:

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

步骤4:DMA配置与使用

// DMA发送示例
HAL_DMA_Start(&hdma_memtomem, (uint32_t)dma_buffer, (uint32_t)&UART->TDR, DMA_BUF_SIZE);

此时CPU写dma_buffer会直接写内存,DMA读到的就是最新数据。

四、完整代码示例

#include "stm32h7xx_hal.h"

#define DMA_BUF_ADDR   0x30000000
#define DMA_BUF_SIZE   0x8000

__attribute__((section(".dma_buffer"))) uint8_t dma_buffer[DMA_BUF_SIZE];

void MPU_Config(void)
{
    HAL_MPU_Disable();

    MPU_Region_InitTypeDef MPU_InitStruct = {0};
    MPU_InitStruct.Enable           = MPU_REGION_ENABLE;
    MPU_InitStruct.Number           = MPU_REGION_NUMBER0;
    MPU_InitStruct.BaseAddress      = DMA_BUF_ADDR;
    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);
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MPU_Config();

    // 初始化DMA、UART等
    // ...

    for (int i = 0; i < DMA_BUF_SIZE; i++) {
        dma_buffer[i] = i & 0xFF;
    }

    // 启动DMA发送
    HAL_DMA_Start(&hdma_uart_tx, (uint32_t)dma_buffer, (uint32_t)&huart1.Instance->TDR, DMA_BUF_SIZE);

    while (1) {}
}

五、注意事项

  • MPU区域对齐:基地址必须按区域大小对齐,如32KB区域基地址需32KB对齐。
  • Cache维护:若无法使用Non-Cacheable,务必在DMA传输前后调用SCB_CleanDCache_by_Addr和SCB_InvalidateDCache_by_Addr。
  • DTCM限制:DTCM地址0x20000000无法被DMA访问,不要将DMA缓冲区放在此处。
  • 多区域重叠:若同一地址被多个MPU区域覆盖,高编号区域生效,注意优先级。
  • 性能权衡:Non-Cacheable会降低CPU访问速度,仅对DMA缓冲区使用,其他内存保持Cacheable。
  • 调试技巧:若仍异常,可临时关闭D-Cache验证,或使用__DSB()确保写操作完成。

通过MPU将DMA缓冲区设为Non-Cacheable,可从根本上避免Cache一致性问题,是STM32H7开发中的推荐做法。