一、问题背景:一个诡异的 HardFault

在 STM32H743 上做以太网 + ADC 采集时,程序偶尔进入 HardFault,且 DMA 接收缓冲区数据错乱。调试发现:CPU 读到的缓冲区内容与 DMA 实际写入不一致,甚至触发总线错误。

根因并不在 DMA 本身,而在于 Cortex-M7 的 D-Cache 与 DMA 的并发访问

二、原理:为什么 Cache 会让 DMA “出错”

Cortex-M7 默认开启 I-Cache 和 D-Cache。D-Cache 是写回(Write-Back)策略:

  • CPU 写数据时只更新 Cache,不立即写回 SRAM;
  • DMA 直接访问 SRAM,读到的可能是旧数据;
  • DMA 写入 SRAM 后,CPU 读到的却是 Cache 中的旧副本。

此外,若 DMA 缓冲区跨越了 MPU 未定义或不可共享的区域,还可能触发总线错误,表现为 HardFault。

解决思路有三条:

  • 将 DMA 缓冲区配置为 Non-Cacheable(推荐,简单可靠);
  • 使用 Cache 维护操作(Clean/Invalidate);
  • 将缓冲区放到 DTCM(不受 D-Cache 影响,但 DMA 无法访问 DTCM,需注意)。

三、配置步骤:用 MPU 把 DMA 缓冲区设为 Non-Cacheable

1. 确定缓冲区地址与大小

假设使用 0x30000000(SRAM1)起始的 32KB 作为 DMA 缓冲区,需 32 字节对齐。

2. 配置 MPU 区域

#include "stm32h7xx.h"

#define DMA_BUF_ADDR   0x30000000UL
#define DMA_BUF_SIZE   0x8000UL   // 32KB

void MPU_Config(void)
{
    HAL_MPU_Disable();

    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    // 区域 0:DMA 缓冲区,Non-Cacheable, Non-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_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);
}

3. 在 main 中尽早调用

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MPU_Config();          // 必须在使能 Cache 前或紧随其后
    SCB_EnableICache();
    SCB_EnableDCache();
    // ...
}

四、完整代码示例:DMA 收发 + Cache 维护

若无法使用 MPU(例如缓冲区地址不固定),可用 Cache 维护函数:

// 发送前:Clean,确保 SRAM 数据最新
void DMA_SendPrepare(void *buf, uint32_t len)
{
    SCB_CleanDCache_by_Addr((uint32_t *)buf, len);
}

// 接收后:Invalidate,丢弃 Cache 旧副本
void DMA_RecvComplete(void *buf, uint32_t len)
{
    SCB_InvalidateDCache_by_Addr((uint32_t *)buf, len);
}

注意:SCB_InvalidateDCache_by_Addr 的地址必须 32 字节对齐,长度建议向上取整到 32 字节。

五、注意事项与排查清单

  • DTCM 不能用于 DMA:DTCM 地址范围 0x20000000,DMA 无法访问,误用会直接 HardFault。
  • MPU 区域必须对齐:基地址和大小需按区域大小对齐,否则配置无效。
  • Cache 维护要成对:发送前 Clean,接收后 Invalidate,顺序不能反。
  • 多缓冲区场景:每个 DMA 缓冲区单独配置 MPU 区域或单独维护。
  • 调试技巧:在 HardFault_Handler 中打印 SCB->CFSRSCB->HFSRSCB->BFAR,快速定位总线错误。
  • 链接脚本配合:可将 DMA 缓冲区放到自定义段,再在 MPU 中统一配置,避免地址漂移。

六、总结

STM32H7 的 Cache 是性能利器,也是 DMA 的“隐形陷阱”。优先用 MPU 将 DMA 缓冲区设为 Non-Cacheable,能从根源避免一致性问题;若必须使用 Cache,则严格遵循 Clean/Invalidate 流程。掌握这套方法,HardFault 和数据错乱将不再神秘。