STM32H7 的 Cache 与 DMA 一致性陷阱:从现象到 MPU 配置的完整排查路径
一、现象:为什么 DMA 数据总是“慢半拍”?
在 STM32H7 上使用 DMA 时,你可能会遇到以下典型问题:
- ADC 连续采样:DMA 将 ADC 数据搬运到数组,但 CPU 读到的数据始终是第一次的值,或部分数据未更新。
- UART 空闲中断 + DMA 接收:DMA 接收完一帧数据,CPU 读取缓冲区却发现数据错乱、缺失或全是 0。
- SPI 发送:CPU 填充发送缓冲区后启动 DMA,实际发送出去的数据却是旧内容。
- 以太网通信:DMA 描述符和缓冲区数据不一致,导致丢包或协议栈异常。
这些现象的共同点是:CPU 与 DMA 看到的内存内容不一致。根源在于 STM32H7 的 L1 Cache(数据缓存 D-Cache)与 DMA 直接访问物理内存之间的冲突。
二、原理:Cache 与 DMA 为何“打架”?
STM32H7 内核(Cortex-M7)带有 L1 D-Cache,用于加速 CPU 对内存的访问。CPU 读写数据时,实际操作的是 Cache 中的副本,而非直接访问 SRAM。DMA 则绕过 Cache,直接读写物理 SRAM。
-
场景 1:DMA 写入内存,CPU 读取
DMA 将新数据写入 SRAM,但 CPU 之前已缓存了该地址的旧数据。CPU 再次读取时,命中 Cache,得到旧值,而不是 DMA 写入的新值。 -
场景 2:CPU 写入内存,DMA 读取
CPU 写入数据到 Cache,但未回写到 SRAM。此时启动 DMA,DMA 从 SRAM 读取到的是旧数据,导致发送/传输错误。 -
场景 3:Cache 行部分更新
Cache 行大小通常为 32 字节。若 CPU 只修改了部分数据,回写时可能覆盖 DMA 已写入的其他数据,造成数据损坏。
因此,必须通过软件手段维护 Cache 与 DMA 的一致性。
三、解决方案:MPU 配置 + Cache 维护操作
3.1 总体思路
- 将 DMA 缓冲区所在的内存区域配置为 Non-Cacheable(不缓存),彻底避免一致性问题。
- 或者,在 DMA 传输前后手动执行 Cache 清理(Clean) 和 无效化(Invalidate) 操作。
- 推荐使用 MPU 将 DMA 缓冲区映射为 Non-Cacheable,简单可靠。
3.2 MPU 配置步骤
- 使能 MPU:
SCB_EnableMPU()或直接配置 MPU 寄存器。 - 选择一个空闲的 MPU 区域(如 Region 0)。
- 设置基地址为 DMA 缓冲区起始地址(需 32 字节对齐)。
- 设置区域大小为最小覆盖缓冲区的大小(如 32B、64B、...、4GB)。
- 设置属性:
TEX=0, C=0, B=0, S=1(Shareable, Non-Cacheable)。 - 使能该区域。
3.3 完整代码示例
以下代码将 0x30000000 开始的 64KB 区域配置为 Non-Cacheable,用于 DMA 缓冲区(假设该区域位于 AXI SRAM 或 D2 SRAM)。
#include "stm32h7xx.h"
void MPU_Config(void)
{
MPU_Region_InitTypeDef MPU_InitStruct = {0};
// 禁用 MPU
HAL_MPU_Disable();
// 配置 Region 0:DMA 缓冲区区域,Non-Cacheable
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0x30000000; // 根据实际 SRAM 地址修改
MPU_InitStruct.Size = MPU_REGION_SIZE_64KB;
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);
// 使能 MPU
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
// 在 main 初始化中调用
int main(void)
{
HAL_Init();
SystemClock_Config();
MPU_Config(); // 必须在使能 Cache 之前配置
SCB_EnableICache();
SCB_EnableDCache();
// ... 其他初始化
}
关键点:MPU 配置必须在使能 D-Cache 之前完成,否则可能触发异常。
3.4 手动 Cache 维护(备选方案)
若无法使用 MPU,可在 DMA 传输前后调用 CMSIS 函数:
-
DMA 写入内存前(CPU 准备接收):
SCB_InvalidateDCache_by_Addr(addr, size); -
DMA 读取内存前(CPU 准备发送):
SCB_CleanDCache_by_Addr(addr, size); -
DMA 写入完成后(CPU 读取前):再次
SCB_InvalidateDCache_by_Addr。
注意:地址和大小必须 32 字节对齐,否则可能损坏相邻数据。
四、注意事项与避坑指南
- MPU 区域对齐:基地址和大小必须符合 MPU 对齐要求(基地址按大小对齐,大小至少 32B)。
- Cache 维护操作要成对:Clean 用于 CPU 写后、DMA 读前;Invalidate 用于 DMA 写后、CPU 读前。顺序错误会导致数据丢失。
- 避免部分 Cache 行操作:若缓冲区不是 Cache 行大小(32B)的整数倍,Invalidate 可能丢弃相邻数据。建议缓冲区按 32B 对齐并填充至整数倍。
- 多缓冲区场景:为每个 DMA 缓冲区单独配置 MPU 区域,或统一放在同一 Non-Cacheable 区域。
- 性能权衡:Non-Cacheable 区域会降低 CPU 访问速度,但 DMA 缓冲区通常访问不频繁,影响可接受。
-
调试技巧:若怀疑 Cache 问题,可临时禁用 D-Cache 验证;使用
SCB_InvalidateDCache()全局无效化(慎用,影响性能)。
五、总结
STM32H7 的 Cache 与 DMA 一致性问题是嵌入式开发中的经典陷阱。通过 MPU 将 DMA 缓冲区配置为 Non-Cacheable 是最简单可靠的方案;若必须使用 Cache,则需严格遵循 Clean/Invalidate 时序。理解原理、合理配置,才能充分发挥 H7 的高性能,同时保证数据正确性。