一、为什么 D-Cache 会和 DMA 打架?
STM32H7 的 Cortex-M7 内核带有一级数据缓存(D-Cache),默认写通/写回策略下,CPU 访问的数据可能只停留在 Cache 中,并未写回 SRAM。而 DMA 是直接访问物理内存的,它看不到 Cache 里的新数据。
典型现象:
- CPU 写入缓冲区后启动 DMA 发送,DMA 发出的却是旧数据。
- DMA 接收数据到缓冲区,CPU 读到的却是 Cache 中的旧值。
- 数据时对时错,调试时加打印又正常(因为打印触发了内存访问)。
根本原因:CPU 与 DMA 对同一块内存的视图不一致。
二、解决思路:三种方案对比
-
方案1:操作前后手动 Clean/Invalidate Cache
使用
SCB_CleanDCache_by_Addr()和SCB_InvalidateDCache_by_Addr()。灵活但容易漏调用,且 Invalidate 有风险(可能丢弃未写回数据)。 - 方案2:将 DMA 缓冲区放到 Non-Cacheable 区域 通过 MPU 配置,让该区域绕过 D-Cache。一劳永逸,性能略降但最安全。
- 方案3:使用 DTCM RAM DTCM 默认不经过 Cache,但容量有限(通常 128KB),且 DMA 能否访问取决于总线矩阵。
推荐方案2:用 MPU 把 DMA 缓冲区所在 SRAM 区域设为 Non-Cacheable,简单可靠。
三、MPU 配置实战步骤
3.1 确定内存区域
假设使用 D2 SRAM(0x30000000 起),划出 32KB 给 DMA 缓冲区:
- 基地址:
0x30000000 - 大小:32KB(2^15),需满足 MPU 区域大小对齐。
3.2 MPU 配置要点
- 使能 MPU:
SCB->CTRL |= SCB_CTRL_MPU_Msk; - 选择区域号(0~15),配置基地址、属性、大小。
- 属性设为 Normal, Non-Cacheable:
- TEX=0b001, C=0, B=0
- 或直接使用
ARM_MPU_ATTR_NORMAL_NON_CACHEABLE
- 使能该区域和 MPU。
3.3 完整代码示例
#include "stm32h7xx.h"
#define DMA_BUFFER_ADDR 0x30000000UL
#define DMA_BUFFER_SIZE (32UL * 1024UL)
void MPU_Config_NonCacheable(void)
{
/* 关闭 MPU,配置前需禁用 */
ARM_MPU_Disable();
/* 配置 Region 0:DMA 缓冲区 Non-Cacheable */
ARM_MPU_SetRegion(
0, /* Region number */
ARM_MPU_RBAR(DMA_BUFFER_ADDR, /* Base address */
ARM_MPU_SH_NON, /* Shareable: Non-shareable */
0, /* AP: 读写权限,0 表示 RW */
1), /* XN: 禁止执行 */
ARM_MPU_RLAR(DMA_BUFFER_ADDR + DMA_BUFFER_SIZE - 1,
ARM_MPU_ATTR_NORMAL_NON_CACHEABLE, /* 属性 */
0) /* AttrIndex */
);
/* 使能 MPU */
ARM_MPU_Enable(MPU_CTRL_PRIVDEFENA_Msk);
}
注意:
ARM_MPU_RLAR的第二个参数是属性,ARM_MPU_ATTR_NORMAL_NON_CACHEABLE在 CMSIS 中定义为(0UL << 28) | (0UL << 24) | (0UL << 20) | (0UL << 16) | (0UL << 12) | (0UL << 8) | (0UL << 4) | (0UL << 0)的简化宏,实际需确认 CMSIS 版本。若没有该宏,可手动构造:(0x1UL << 28) | (0UL << 24) | (0UL << 20) | (0UL << 16) | (0UL << 12) | (0UL << 8) | (0UL << 4) | (0UL << 0)。
更稳妥的写法是直接使用寄存器:
void MPU_Config_NonCacheable(void)
{
MPU->CTRL = 0; /* 关闭 MPU */
/* Region 0: 0x30000000, 32KB, Normal Non-Cacheable */
MPU->RNR = 0;
MPU->RBAR = (DMA_BUFFER_ADDR & MPU_RBAR_ADDR_Msk) | MPU_RBAR_VALID_Msk | (0 << MPU_RBAR_REGION_Pos);
MPU->RASR = (0x1UL << MPU_RASR_TEX_Pos) | /* TEX = 001 */
(0UL << MPU_RASR_C_Pos) | /* C = 0 */
(0UL << MPU_RASR_B_Pos) | /* B = 0 */
(0UL << MPU_RASR_S_Pos) | /* S = 0 */
(3UL << MPU_RASR_AP_Pos) | /* AP = 011, 全访问 */
(0UL << MPU_RASR_XN_Pos) | /* 允许执行 */
(0UL << MPU_RASR_SRD_Pos) | /* 子区域禁用 */
(14UL << MPU_RASR_SIZE_Pos) | /* 2^(14+1) = 32KB */
(1UL << MPU_RASR_ENABLE_Pos); /* 使能该区域 */
MPU->CTRL = MPU_CTRL_ENABLE_Msk | MPU_CTRL_PRIVDEFENA_Msk;
__DSB();
__ISB();
}
3.4 在 main 中调用
int main(void)
{
HAL_Init();
SystemClock_Config();
MPU_Config_NonCacheable(); /* 必须在使能 D-Cache 之前或之后均可,但建议尽早 */
/* 使能 D-Cache */
SCB_EnableDCache();
/* 后续 DMA 操作直接使用 DMA_BUFFER_ADDR 即可,无需 Clean/Invalidate */
...
}
四、注意事项与避坑
- MPU 区域大小必须为 2 的幂,且基地址按大小对齐。32KB 对应 SIZE=14。
- 子区域(SRD) 可进一步细分,但一般不用。
-
使能 MPU 后,未覆盖的区域默认走背景区域,若设置了
PRIVDEFENA,特权模式可访问默认内存映射。 - D-Cache 使能后,栈、堆等仍走 Cache,只有 MPU 指定的 Non-Cacheable 区域绕过。
- 若使用 DTCM,注意 DMA 能否访问:D1 域 DMA 不能访问 DTCM,需用 MDMA 或放到 AXI SRAM。
- Invalidate 操作有风险:若缓冲区有未写回数据,Invalidate 会丢弃,务必先 Clean。
- 多缓冲区场景:每个 DMA 缓冲区都需单独配置 MPU 区域,或合并到一个大区域。
-
调试时:可用
SCB_InvalidateDCache_by_Addr临时验证,但正式代码建议用 MPU 方案。
五、总结
STM32H7 的 D-Cache 与 DMA 一致性问题是嵌入式开发中的经典坑。通过 MPU 将 DMA 缓冲区配置为 Non-Cacheable,可以从根源上避免数据不一致,代码简洁且稳定。记住:能用 MPU 解决的,就不要靠手动 Clean/Invalidate。希望本文的实战步骤能帮你少走弯路。