问题现象与背景

在STM32H7系列MCU上开发以太网应用时,许多开发者会遇到一个棘手问题:系统初始化正常,但一旦启动以太网DMA传输(发送或接收),立即触发HardFault。调试器显示故障发生在DMA描述符访问阶段,而描述符本身位于SRAM中,看似一切正常。

根本原因往往在于MPU(内存保护单元)配置不当,导致DMA无法正确访问描述符内存区域,或Cache与DMA之间产生数据一致性问题,进而引发总线错误,最终表现为HardFault。

原理分析:MPU、Cache与DMA的三角关系

STM32H7的存储架构

STM32H7采用Cortex-M7内核,具有:

  • L1 Cache:I-Cache和D-Cache,提升CPU访问性能
  • MPU:可配置内存区域的访问权限、Cache策略、共享属性
  • DMA:直接访问物理内存,不经过Cache

关键矛盾点

  • CPU通过Cache访问SRAM,DMA直接访问SRAM
  • 若描述符区域被配置为Cacheable且未正确维护一致性,DMA可能读到旧数据
  • 若MPU将描述符区域配置为不可访问或错误属性,DMA访问将产生总线错误
  • STM32H7的以太网DMA描述符必须位于可被DMA访问且属性正确的内存中

典型错误配置

  • 描述符区域被MPU配置为DeviceStrongly Ordered,DMA访问受限
  • 描述符区域未配置MPU,默认属性导致Cache一致性问题
  • 描述符区域被配置为Non-shareable,多主设备访问冲突

排查路径

第一步:确认HardFault来源

使用调试器查看CFSR(Configurable Fault Status Register)和BFAR(BusFault Address Register):

void HardFault_Handler(void) {
    volatile uint32_t cfsr = SCB->CFSR;
    volatile uint32_t bfar = SCB->BFAR;
    volatile uint32_t hfsr = SCB->HFSR;
    // 在此设置断点,观察cfsr和bfar
    while(1);
}
  • CFSRBFARVALID置位,BFAR给出出错地址,通常指向描述符区域
  • CFSRIMPRECISERR置位,说明是写缓冲导致的非精确总线错误

第二步:检查MPU配置

查看MPU区域配置,确认描述符所在SRAM区域的属性:

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

    // 描述符区域:0x30000000 - 0x30003FFF (SRAM1, 16KB)
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = 0x30000000;
    MPU_InitStruct.Size = MPU_REGION_SIZE_16KB;
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
    MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE; // 关键:禁止Cache
    MPU_InitStruct.IsShareable = MPU_ACCESS_SHAREABLE;     // 关键:共享
    MPU_InitStruct.Number = MPU_REGION_NUMBER0;
    MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;

    HAL_MPU_ConfigRegion(&MPU_InitStruct);
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

要点

  • IsCacheable = NOT_CACHEABLE:避免Cache一致性问题
  • IsShareable = SHAREABLE:确保DMA与CPU访问一致
  • AccessPermission = FULL_ACCESS:允许读写
  • 区域大小必须覆盖所有描述符

第三步:验证链接脚本与内存分配

确保描述符数组被放置在正确的MPU区域:

// 在链接脚本中定义段
__attribute__((section(".eth_desc"))) ETH_DMADescTypeDef DMARxDscrTab[ETH_RX_DESC_CNT];
__attribute__((section(".eth_desc"))) ETH_DMADescTypeDef DMATxDscrTab[ETH_TX_DESC_CNT];

链接脚本片段:

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

第四步:检查DMA可访问性

STM32H7中,以太网DMA只能访问特定内存区域(如D2域SRAM)。确认描述符位于D2 SRAM(0x30000000起),而非DTCM(0x20000000),因为DTCM不支持DMA。

完整配置示例

#include "stm32h7xx_hal.h"

// 描述符定义,强制32字节对齐
__attribute__((aligned(32), section(".eth_desc")))
ETH_DMADescTypeDef DMARxDscrTab[ETH_RX_DESC_CNT];
__attribute__((aligned(32), section(".eth_desc")))
ETH_DMADescTypeDef DMATxDscrTab[ETH_TX_DESC_CNT];

// MPU配置函数
void MPU_Config(void) {
    HAL_MPU_Disable();

    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    // 配置D2 SRAM为Non-Cacheable, Shareable
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = 0x30000000;
    MPU_InitStruct.Size = MPU_REGION_SIZE_512KB; // 覆盖整个D2 SRAM
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
    MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
    MPU_InitStruct.IsShareable = MPU_ACCESS_SHAREABLE;
    MPU_InitStruct.Number = MPU_REGION_NUMBER0;
    MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;

    HAL_MPU_ConfigRegion(&MPU_InitStruct);
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

// 以太网初始化中调用
void ETH_Init(void) {
    MPU_Config();
    // ... 其他初始化
    HAL_ETH_Init(&heth);
}

注意事项

  • Cache维护:若必须使用Cacheable内存,需在DMA传输前后调用SCB_CleanDCache_by_Addr()SCB_InvalidateDCache_by_Addr()
  • 对齐要求:描述符必须32字节对齐,否则DMA可能访问错误
  • MPU区域重叠:避免多个MPU区域重叠导致属性冲突
  • DMA可访问域:确认使用D2域SRAM,DTCM和ITCM不支持以太网DMA
  • 调试技巧:在HardFault中打印CFSRBFAR,快速定位总线错误地址
  • HAL库版本:不同版本HAL库MPU配置API略有差异,注意兼容性

通过以上排查路径和正确配置,可彻底解决STM32H7以太网DMA描述符访问导致的HardFault问题,确保网络通信稳定可靠。