一、现象:DMA 数据错乱,关 Cache 就好?
在 STM32H7 上开发以太网、USB 或 SPI 高速传输时,你是否遇到过:
- DMA 发送的数据,接收端看到的是旧数据或随机值。
- DMA 接收的数据,CPU 读出来部分正确、部分错乱。
- 一旦关闭 D-Cache,一切正常;打开就出错。
这不是玄学,而是 Cache 与 DMA 的一致性(Coherency)问题。Cortex-M7 的 D-Cache 和 DMA 控制器各自独立访问内存,若没有正确配置 MPU,CPU 可能读到 Cache 中的旧数据,而 DMA 却把新数据写到了物理内存。
二、原理:为什么 Cache 会“欺骗”你?
2.1 Cache 的工作方式
- CPU 访问内存时,先查 D-Cache。若命中,直接返回 Cache 中的数据,不访问物理内存。
- 写操作默认是 写回(Write-Back):CPU 只写 Cache,标记为脏,稍后才写回物理内存。
- DMA 直接访问物理内存,不经过 Cache。
2.2 典型错误场景
场景 A:DMA 发送
- CPU 填充缓冲区数据(此时数据在 Cache 中,物理内存还是旧值)。
- 启动 DMA 发送,DMA 从物理内存读数据,发送的是旧值。
场景 B:DMA 接收
- DMA 将新数据写入物理内存。
- CPU 读缓冲区,若 Cache 中已有该地址的旧数据,则读到旧值。
2.3 解决思路
-
软件维护:手动调用
SCB_CleanDCache_by_Addr()和SCB_InvalidateDCache_by_Addr()。 - 硬件配置:通过 MPU 将 DMA 缓冲区所在内存区域配置为 Non-Cacheable 或 Write-Through,一劳永逸。
三、MPU 配置步骤(以 STM32H7 为例)
3.1 确定内存区域
假设 DMA 缓冲区放在 0x30000000(D2 SRAM),大小 32KB。
3.2 配置 MPU 区域
- 选择空闲的 MPU Region(如 Region 0)。
- 设置基地址、大小、属性。
- 属性关键位:
- TEX=0, C=0, B=0:Non-Cacheable。
- TEX=0, C=1, B=0:Write-Through, No Write Allocate。
- TEX=0, C=1, B=1:Write-Back, Write Allocate(默认,易出错)。
- 使能该 Region。
3.3 完整代码示例
#include "stm32h7xx.h"
#define DMA_BUFFER_ADDR 0x30000000UL
#define DMA_BUFFER_SIZE 0x8000UL // 32KB
void MPU_Config(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_InitStruct = {0};
// 配置 D2 SRAM 为 Non-Cacheable
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = DMA_BUFFER_ADDR;
MPU_InitStruct.Size = MPU_REGION_SIZE_32KB;
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_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);
// 使能 MPU
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
注意:
HAL_MPU_ConfigRegion内部会处理 TEX/C/B 位,直接使用IsCacheable和IsBufferable即可。
3.4 启动 DMA 前的额外操作
即使配置了 Non-Cacheable,若之前该区域已被 Cache 缓存,仍需先失效(Invalidate)一次:
SCB_InvalidateDCache_by_Addr((uint32_t*)DMA_BUFFER_ADDR, DMA_BUFFER_SIZE);
四、注意事项与避坑指南
- MPU 区域对齐:基地址必须按区域大小对齐(32KB 区域基地址低 15 位为 0)。
- 区域重叠:高编号 Region 优先级更高,避免冲突。
- DMA 描述符:若使用链表式 DMA,描述符本身也需放在 Non-Cacheable 区域。
- 性能权衡:Non-Cacheable 会降低 CPU 访问速度,仅对 DMA 缓冲区使用。
- 多核/多主:STM32H7 的 MDMA、BDMA 同样受 Cache 影响,需统一处理。
-
调试技巧:用
SCB_InvalidateDCache()全失效可快速验证问题。
五、总结
STM32H7 的 Cache 是性能利器,但 DMA 与 Cache 的共存必须通过 MPU 或手动维护来保证一致性。推荐将 DMA 缓冲区配置为 Non-Cacheable,并在初始化时失效一次 Cache。掌握这些,你的 DMA 传输将不再“随机”出错。