一、问题背景:一个诡异的 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->CFSR、SCB->HFSR、SCB->BFAR,快速定位总线错误。 - 链接脚本配合:可将 DMA 缓冲区放到自定义段,再在 MPU 中统一配置,避免地址漂移。
六、总结
STM32H7 的 Cache 是性能利器,也是 DMA 的“隐形陷阱”。优先用 MPU 将 DMA 缓冲区设为 Non-Cacheable,能从根源避免一致性问题;若必须使用 Cache,则严格遵循 Clean/Invalidate 流程。掌握这套方法,HardFault 和数据错乱将不再神秘。