STM32H7 MPU配置实战:外部SDRAM划成可缓存/设备内存后,DMA描述符还会不会踩坑?

1. 为什么STM32H7的MPU和DMA容易踩坑?

STM32H7 内核是 Cortex-M7,带有 L1 Cache(I-Cache 和 D-Cache)。D-Cache 能大幅提升 CPU 访问外部 SDRAM 的性能,但也会引入数据一致性问题:CPU 写数据可能停留在 Cache 中,DMA 直接访问物理内存时读到旧数据;反之 DMA 写入新数据,CPU 可能从 Cache 读到旧值。

MPU(Memory Protection Unit)允许我们将内存区域配置为不同的属性:

  • Normal, Write-Back, Write-Allocate:可缓存,适合 CPU 频繁读写的数据。
  • Normal, Non-Cacheable:非缓存,适合 DMA 缓冲区。
  • DeviceStrongly Ordered:设备内存,禁止缓存和乱序访问,适合外设寄存器。

但仅仅把 SDRAM 划成可缓存/设备内存还不够,DMA 描述符的存放位置同样关键。如果描述符放在可缓存区域,CPU 更新描述符后未清理 Cache,DMA 可能读到旧描述符,导致传输错误。

2. MPU配置步骤(以STM32H743为例)

假设外部 SDRAM 映射在 0xC0000000,大小 32MB。我们计划:

  • 前 16MB 作为可缓存区域(存放代码、数据)。
  • 后 16MB 作为非缓存区域(存放 DMA 缓冲区、描述符)。

2.1 在CubeMX中配置MPU

  1. 打开 System Core -> MPU,点击 Add 添加区域。
  2. 区域0:Base Address = 0xC0000000,Size = 16MB,TPU = Level 1,Cacheable = Write-Back, Write-Allocate,Shareable = No。
  3. 区域1:Base Address = 0xC1000000,Size = 16MB,TPU = Level 1,Cacheable = Non-Cacheable,Shareable = Yes(若多主设备访问)。
  4. 使能 MPU 和 D-Cache。

2.2 手动初始化代码(若不用CubeMX)

#include "stm32h7xx.h"

void MPU_Config(void)
{
    HAL_MPU_Disable();

    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    // 区域0:可缓存 SDRAM
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = 0xC0000000;
    MPU_InitStruct.Size = MPU_REGION_SIZE_16MB;
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE;
    MPU_InitStruct.IsCacheable = MPU_ACCESS_CACHEABLE;
    MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
    MPU_InitStruct.Number = MPU_REGION_NUMBER0;
    MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL1;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE;
    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    // 区域1:非缓存 SDRAM(DMA缓冲区)
    MPU_InitStruct.BaseAddress = 0xC1000000;
    MPU_InitStruct.Size = MPU_REGION_SIZE_16MB;
    MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
    MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
    MPU_InitStruct.IsShareable = MPU_ACCESS_SHAREABLE;
    MPU_InitStruct.Number = MPU_REGION_NUMBER1;
    MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

3. DMA描述符存放位置的影响

以以太网 DMA 或 MDMA 为例,描述符通常包含控制字、状态字、缓冲区地址等。若描述符放在可缓存区域,CPU 修改描述符后必须执行 SCB_CleanDCache_by_Addr() 将数据写回物理内存,否则 DMA 读到旧内容。同样,DMA 更新描述符状态后,CPU 读取前需 SCB_InvalidateDCache_by_Addr() 无效化 Cache。

最佳实践:将 DMA 描述符放在非缓存区域(如 0xC1000000 起始的 16MB)。这样 CPU 和 DMA 看到的内存完全一致,无需手动维护 Cache,简化代码并避免隐蔽错误。

3.1 示例:以太网DMA描述符定义

// 将描述符放在非缓存区域
__attribute__((section(".dma_desc"))) ETH_DMADescTypeDef DMARxDscrTab[ETH_RX_DESC_CNT];
__attribute__((section(".dma_desc"))) ETH_DMADescTypeDef DMATxDscrTab[ETH_TX_DESC_CNT];

在链接脚本中定义 .dma_desc 段到 0xC1000000

MEMORY
{
  RAM (xrw)  : ORIGIN = 0x24000000, LENGTH = 512K
  SDRAM_NC (xrw) : ORIGIN = 0xC1000000, LENGTH = 16M
}

SECTIONS
{
  .dma_desc :
  {
    . = ALIGN(32);
    *(.dma_desc)
  } >SDRAM_NC
}

4. 完整代码示例:SDRAM初始化 + MPU + DMA发送

// sdram.c
void SDRAM_Init(void)
{
    FMC_SDRAM_CommandTypeDef cmd;
    // ... 初始化FMC时序、GPIO等
    // 发送命令序列:时钟配置使能、预充电、刷新、模式寄存器
}

// main.c
uint8_t dma_buffer[1024] __attribute__((section(".dma_buffer")));

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MPU_Config();
    SDRAM_Init();
    SCB_EnableICache();
    SCB_EnableDCache();

    // 填充数据
    for (int i = 0; i < 1024; i++) dma_buffer[i] = i;

    // 启动DMA传输(假设使用DMA1_Stream0)
    HAL_DMA_Start(&hdma_memtomem_dma1_stream0, (uint32_t)dma_buffer, (uint32_t)0xC0000000, 1024);

    while (1) {}
}

5. 注意事项与避坑指南

  • Cache维护函数:若描述符或缓冲区在可缓存区域,必须使用 SCB_CleanDCache_by_Addr()SCB_InvalidateDCache_by_Addr(),注意地址 32 字节对齐。
  • MPU区域重叠:后配置的区域优先级更高,确保非缓存区域覆盖描述符地址。
  • DMA突发传输:非缓存区域可能降低性能,但换来了数据一致性。对性能敏感的场景可保留可缓存区域,并严格维护 Cache。
  • 多主设备访问:若 SDRAM 被 CPU 和 DMA 同时访问,建议将共享区域设为 Shareable,避免缓存一致性问题。
  • 链接脚本对齐:DMA 描述符通常要求 32 字节对齐,链接脚本中需 ALIGN(32)
  • 调试技巧:若 DMA 传输异常,先检查描述符地址是否落在非缓存区域,再用 SCB_InvalidateDCache() 后读取状态。

通过合理配置 MPU 并将 DMA 描述符置于非缓存区域,可以彻底避免 Cache 一致性导致的隐蔽 Bug,让 STM32H7 的性能与稳定性兼得。