引言

STM32H7 系列(如 STM32H743)凭借 400MHz 主频和 Cortex-M7 内核,成为高性能嵌入式应用的首选。然而,Cortex-M7 内置的 D-Cache(数据缓存)和 MPU(内存保护单元)若配置不当,轻则数据不一致,重则随机死机。本文基于一次实际项目中的故障,详细讲解排查过程与解决方案。

问题现象

某项目使用 STM32H743,主频 400MHz,外接 SDRAM 和 QSPI Flash。系统运行几分钟到几小时不等,随机出现 HardFault 或死机。复现困难,且与中断频率、DMA 传输相关。初步排查排除了电源和时钟问题,怀疑与 Cache 一致性有关。

原理分析

D-Cache 的工作机制

Cortex-M7 的 D-Cache 是写回(Write-back)模式,CPU 写数据时先写入 Cache,标记为脏(Dirty),后续由 Cache 控制器回写内存。读数据时,若命中 Cache 则直接返回,否则从内存加载。这种机制大幅提升性能,但带来一致性问题:

  • CPU 与外设(如 DMA)共享内存时:DMA 直接访问内存,而 CPU 可能只修改了 Cache 中的数据,导致 DMA 读到旧数据。
  • 外设写内存后,CPU 读取时可能命中过期的 Cache 行,得到错误数据。

MPU 的作用

MPU 用于定义内存区域的属性,包括 Cache 策略(Write-back、Write-through、Non-cacheable)、访问权限等。默认情况下,STM32H7 的 MPU 未启用,所有内存区域按默认属性(可能为 Write-back)处理,这往往不是我们想要的。

死机根因

当 DMA 与 CPU 共享缓冲区时,若缓冲区被标记为 Cacheable(Write-back),则:

  1. CPU 写入数据到缓冲区,数据留在 Cache 中,未立即回写内存。
  2. DMA 启动传输,直接从内存读取,得到不完整或旧数据。
  3. 若 DMA 写内存(如接收数据),CPU 读取时命中 Cache 中的旧行,导致数据错误。
  4. 数据错误引发逻辑混乱,最终导致 HardFault 或死机。

排查方法

1. 确认是否 Cache 问题

  • 临时禁用 D-Cache(通过 SCB_DisableDCache()),观察问题是否消失。若消失,则基本确定是 Cache 一致性问题。
  • 在关键缓冲区操作前后,使用 SCB_CleanDCache()SCB_InvalidateDCache() 强制同步,测试是否改善。

2. 检查 MPU 配置

  • 查看是否启用了 MPU,以及各内存区域的属性。重点检查 DMA 缓冲区、外设寄存器映射区域(如 FMC、QSPI)是否被错误配置为 Cacheable。

3. 使用调试器定位死机位置

  • 在 HardFault_Handler 中读取 HFSR、CFSR、BFAR 等寄存器,分析异常类型。若 BFAR 指向 DMA 缓冲区地址,则高度怀疑 Cache 问题。

解决方案

标准配置步骤

  1. 启用 MPU,为不同内存区域设置合理属性。
  2. DMA 缓冲区:设置为 Non-cacheable(或 Write-through),避免一致性维护。
  3. 外设内存映射(如 FMC SDRAM、QSPI Flash):根据实际需求,通常设置为 Write-through 或 Non-cacheable。
  4. 内部 SRAM(如 DTCM)可保持 Write-back,因为 CPU 独占。
  5. 若必须使用 Cacheable 区域与 DMA 交互,则需在 DMA 操作前后手动 Clean/Invalidate。

代码示例

以下为 STM32H743 的 MPU 配置示例(使用 HAL 库):

#include "stm32h7xx_hal.h"

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

    // 禁用 MPU
    HAL_MPU_Disable();

    // 配置 SDRAM 区域(FMC Bank1,地址 0xC0000000,大小 8MB)
    // 设置为 Write-through,避免 DMA 一致性问题
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = 0xC0000000;
    MPU_InitStruct.Size = MPU_REGION_SIZE_8MB;
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_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_LEVEL0;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    // 配置 QSPI Flash 区域(0x90000000,大小 8MB)
    // 设置为 Non-cacheable,因为 QSPI 常用于存储代码或只读数据
    MPU_InitStruct.BaseAddress = 0x90000000;
    MPU_InitStruct.Size = MPU_REGION_SIZE_8MB;
    MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
    MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
    MPU_InitStruct.Number = MPU_REGION_NUMBER1;
    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    // 配置 DMA 缓冲区(假设在 AXI SRAM 0x24000000,大小 64KB)
    // 设置为 Non-cacheable
    MPU_InitStruct.BaseAddress = 0x24000000;
    MPU_InitStruct.Size = MPU_REGION_SIZE_64KB;
    MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
    MPU_InitStruct.Number = MPU_REGION_NUMBER2;
    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    // 启用 MPU
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);

    // 启用 D-Cache 和 I-Cache
    SCB_EnableDCache();
    SCB_EnableICache();
}

// 在 main 函数中调用
int main(void)
{
    HAL_Init();
    SystemClock_Config(); // 配置 400MHz
    MPU_Config();
    // ... 其他初始化
}

手动维护 Cache 一致性(若必须使用 Cacheable 区域)

// 在 DMA 发送前,将缓冲区数据从 Cache 回写到内存
SCB_CleanDCache_by_Addr((uint32_t*)buffer, size);

// 在 DMA 接收完成后,使 Cache 中的对应行失效,以便 CPU 重新从内存读取
SCB_InvalidateDCache_by_Addr((uint32_t*)buffer, size);

注意事项

  • MPU 区域大小必须是 2 的幂,且起始地址需对齐到区域大小。
  • 区域优先级:编号越小优先级越高,配置时注意避免重叠。
  • D-Cache 启用后,所有外设寄存器访问(如 UART->DR)应使用 __IOvolatile,否则可能被优化。
  • 使用 HAL 库时HAL_MPU_ConfigRegion 会自动处理 TEX 等字段,但需理解其含义。
  • 调试时,可先禁用 D-Cache 验证问题,再逐步启用并调整 MPU。
  • 对于 SDRAM,若性能要求高,可考虑 Write-back + 定期 Clean,但需确保 DMA 操作前正确同步。

总结

STM32H7 的高性能离不开 D-Cache,但必须配合正确的 MPU 配置。本文通过实际案例,展示了因 D-Cache 与 MPU 配置不当导致的随机死机问题,并给出了标准配置和代码示例。开发者应牢记:DMA 与 CPU 共享的内存区域,要么配置为 Non-cacheable,要么手动维护一致性。遵循这一原则,可避免绝大多数 Cache 相关故障。