一、问题现象:DMA 传输正常,却突然进入 HardFault

最近在调试 STM32H743 的以太网 DMA 接收时,遇到一个诡异现象:DMA 描述符和缓冲区都放在 0x30000000(AXI SRAM)区域,初始化后能正常收发几包数据,但运行一段时间后系统突然进入 HardFault。通过调试器查看 CFSR 寄存器,发现 MMARVALID 置位,说明触发了 MemManage 异常。

进一步定位,异常发生在 CPU 读取 DMA 缓冲区数据时。为什么 DMA 能写,CPU 一读就崩?答案藏在 Cortex-M7 的 MPU(内存保护单元)配置里。

二、原理:Cortex-M7 的 MPU 与 DMA 如何“打架”

2.1 MPU 的基本作用

Cortex-M7 的 MPU 允许你将内存划分为多个区域,并为每个区域设置访问权限(特权/用户、可读/可写/可执行)和内存属性(如 Cache 策略、Shareable 等)。当 CPU 访问违反权限时,就会触发 MemManage 异常。

2.2 DMA 与 Cache 一致性问题

STM32H7 的 DMA 通常配置为绕过 Cache(或使用 MPU 将缓冲区设为 Non-Cacheable),否则会出现数据不一致。但很多开发者只记得设置“Non-Cacheable”,却忽略了 MPU 的默认背景区域(Background Region)。

  • 如果使能了 MPU 且未配置背景区域,所有未覆盖的地址默认使用 PRIVDEFENA 行为:特权模式可访问,但权限为默认的“可读可写可执行”,且属性为 Strongly-Ordered 或 Device?实际上,复位后 MPU 关闭,所有内存为默认的 Normal,Non-shareable,可缓存。
  • 一旦你使能 MPU 并只配置了部分区域,未配置的区域会使用默认内存映射,但权限可能变为“禁止访问”(如果 PRIVDEFENA=0)。

2.3 典型错误:只配置了 Flash 和 SRAM,忘了 DMA 缓冲区

假设你配置了 MPU 区域 0 为 Flash(只读、可执行),区域 1 为 DTCM(可读写),然后使能 MPU。此时 0x30000000 的 AXI SRAM 没有被任何区域覆盖,且 PRIVDEFENA=0,那么 CPU 访问该地址就会触发 MemManage 异常。而 DMA 作为总线主设备,不受 MPU 限制,所以 DMA 能正常写入,CPU 一读就崩。

三、配置步骤:正确设置 MPU 保护 DMA 缓冲区

3.1 确定缓冲区地址与大小

假设 DMA 接收缓冲区定义在 AXI SRAM:

#define DMA_BUF_ADDR  0x30000000
#define DMA_BUF_SIZE  0x2000   // 8KB

3.2 配置 MPU 区域

使用 HAL 库的 HAL_MPU_ConfigRegion 函数,注意区域编号、基地址、大小(必须为 2 的幂,且至少 32 字节)、属性。

void MPU_Config(void)
{
    HAL_MPU_Disable();

    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    // 区域 0:AXI SRAM DMA 缓冲区,Non-Cacheable, Shareable
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.Number = MPU_REGION_NUMBER0;
    MPU_InitStruct.BaseAddress = DMA_BUF_ADDR;
    MPU_InitStruct.Size = MPU_REGION_SIZE_8KB;  // 必须覆盖整个缓冲区
    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);

    // 区域 1:整个 0x00000000-0x1FFFFFFF 背景区域,允许特权访问
    // 或者使用 PRIVDEFENA 位
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

关键点:

  • Size 必须大于等于缓冲区大小,且为 2 的幂。
  • IsShareable 设为 Shareable 可确保多总线主设备(CPU、DMA)之间数据一致性。
  • IsCacheable 和 IsBufferable 设为 Not Cacheable/Not Bufferable,避免 Cache 一致性问题。
  • 使能 MPU 时使用 MPU_PRIVILEGED_DEFAULT,这样未覆盖区域在特权模式下仍可访问,避免意外异常。

3.3 确保 DMA 描述符也在 MPU 区域内

如果 DMA 使用链接描述符(如以太网 DMA),描述符本身也必须位于 Non-Cacheable 区域,否则 CPU 修改描述符后可能未及时写回内存,导致 DMA 读取错误。

四、完整代码示例:以太网 DMA 接收缓冲区配置

#include "stm32h7xx_hal.h"

#define ETH_RX_BUF_ADDR  0x30000000
#define ETH_RX_BUF_SIZE  0x4000   // 16KB

void MPU_Config(void)
{
    HAL_MPU_Disable();

    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    // 区域 0:以太网 DMA 接收缓冲区
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.Number = MPU_REGION_NUMBER0;
    MPU_InitStruct.BaseAddress = ETH_RX_BUF_ADDR;
    MPU_InitStruct.Size = MPU_REGION_SIZE_16KB;
    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);

    // 区域 1:DMA 描述符区域(假设紧随其后)
    MPU_InitStruct.Number = MPU_REGION_NUMBER1;
    MPU_InitStruct.BaseAddress = ETH_RX_BUF_ADDR + ETH_RX_BUF_SIZE;
    MPU_InitStruct.Size = MPU_REGION_SIZE_1KB;
    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    // 使能 MPU,背景区域为特权可访问
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

int main(void)
{
    HAL_Init();
    MPU_Config();
    // 后续初始化 ETH、DMA 等
    while (1) {}
}

五、注意事项与避坑清单

  • MPU 区域大小必须为 2 的幂,且基地址需对齐到大小边界。例如 16KB 区域基地址必须是 0x4000 的倍数。
  • 区域重叠:后配置的区域优先级更高,但应避免重叠导致意外属性。
  • DMA 缓冲区不要放在 DTCM:STM32H7 的 DTCM 只能被 CPU 访问,DMA 无法访问。通常放在 AXI SRAM(0x24000000)或 SRAM1/2/3(0x30000000)。
  • Cache 维护:即使配置为 Non-Cacheable,也建议在 DMA 传输前后调用 SCB_InvalidateDCache_by_Addr 等函数,以防万一。
  • 使用 MPU_PRIVILEGED_DEFAULT:避免未覆盖区域在特权模式下不可访问,减少调试难度。
  • 调试时查看 CFSR 和 MMAR:MMAR 寄存器会给出触发异常的地址,快速定位问题区域。

六、总结

STM32H7 的 MPU 是一把双刃剑:用好了能提升系统稳定性和性能,用错了就会导致各种隐蔽异常。对于 DMA 缓冲区,务必将其配置为 Non-Cacheable、Shareable,并确保 MPU 区域完整覆盖。同时,使能 PRIVDEFENA 可避免未定义区域的访问异常。希望本文能帮你绕过这个坑,让 DMA 和 CPU 和谐共处。