STM32H7 的 D-Cache 与 DMA 一致性坑:用 MPU 配置 Non-Cacheable 区域的正确姿势
一、为什么 D-Cache 会和 DMA 打架?
STM32H7 的 Cortex-M7 内核带有一级 D-Cache(数据缓存),默认写回策略。CPU 访问内存时,数据可能只停留在 Cache 里,并未真正写入 SRAM。而 DMA 是独立于 CPU 的总线主设备,它直接访问物理内存,看不到 Cache 内容。
典型场景:
- CPU 准备好发送缓冲区,调用
HAL_UART_Transmit_DMA(),但数据还在 Cache 中,DMA 读到的是旧数据。 - DMA 接收数据到内存,CPU 读到的却是 Cache 中的旧值。
结果就是:数据错乱、偶发死机、调试时一切正常,发布后随机崩溃。
二、三种解决思路对比
-
手动维护 Cache:发送前
SCB_CleanDCache_by_Addr(),接收后SCB_InvalidateDCache_by_Addr()。- 缺点:调用频繁,地址必须 32 字节对齐,容易漏掉或重复失效。
-
将 DMA 缓冲区放到非缓存区:通过 MPU 配置某段 SRAM 为 Non-Cacheable。
- 优点:一劳永逸,CPU 和 DMA 看到同一份数据,无需手动维护。
- 使用 DTCM RAM:DTCM 默认不经过 Cache,但容量有限(通常 128KB),且部分 DMA 无法访问。
推荐方案:MPU 配置 Non-Cacheable 区域,兼顾性能与安全。
三、MPU 配置步骤(以 STM32H743 为例)
3.1 确定内存区域
假设使用 D2 SRAM(地址 0x30000000,大小 128KB)作为 DMA 缓冲区。
3.2 配置 MPU Region
MPU 共有 16 个 Region,每个 Region 需设置基地址、大小、属性和使能。关键属性:
-
TEX = 0b001,C = 0,B = 0→ Normal, Non-Cacheable -
AP = 0b011→ 全访问 -
XN = 0→ 允许执行(可选)
3.3 完整代码示例
#include "stm32h7xx.h"
#define DMA_BUFFER_ADDR 0x30000000UL
#define DMA_BUFFER_SIZE 0x20000UL // 128KB
void MPU_Config_NonCacheable(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_InitStruct = {0};
// 配置 Region 0 为 Non-Cacheable
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
MPU_InitStruct.BaseAddress = DMA_BUFFER_ADDR;
MPU_InitStruct.Size = MPU_REGION_SIZE_128KB;
MPU_InitStruct.SubRegionDisable = 0x00;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL1; // TEX = 0b001
MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE;
MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE; // C = 0
MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE; // B = 0
HAL_MPU_ConfigRegion(&MPU_InitStruct);
// 使能 MPU
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
3.4 在 main 中调用
int main(void)
{
HAL_Init();
SystemClock_Config();
MPU_Config_NonCacheable(); // 必须在使能 D-Cache 之前调用
SCB_EnableICache();
SCB_EnableDCache();
// 后续 DMA 缓冲区使用 DMA_BUFFER_ADDR 起始的地址
uint8_t *dma_buf = (uint8_t *)DMA_BUFFER_ADDR;
// ...
}
四、注意事项与常见坑
- 顺序不能错:先配置 MPU,再使能 D-Cache。否则已缓存的数据可能未失效。
- 地址对齐:Region 基地址必须按大小对齐,例如 128KB 区域基地址低 17 位必须为 0。
- 大小限制:MPU Region 大小必须是 2 的幂,且最小 32 字节。
-
不要与 DTCM 冲突:DTCM 地址
0x20000000默认不缓存,无需重复配置。 - DMA 描述符也要注意:如果使用链表式 DMA,描述符本身也应放在非缓存区。
- 多 Region 重叠:高编号 Region 优先级更高,注意规划。
- 调试时:可临时关闭 D-Cache 验证问题,但正式版务必开启。
五、验证方法
- 在 DMA 传输前后打印缓冲区数据,确认一致。
- 使用
SCB_InvalidateDCache_by_Addr()对比测试,观察差异。 - 用逻辑分析仪或示波器抓取总线波形,确认 DMA 实际访问。
六、总结
STM32H7 的 D-Cache 与 DMA 一致性问题是嵌入式开发中的经典坑。通过 MPU 将 DMA 缓冲区配置为 Non-Cacheable,可以从根源上避免数据不一致,且无需在每次传输时手动维护 Cache。记住:MPU 配置在前,D-Cache 使能在后,地址对齐要检查,Region 规划要合理。掌握这一招,你的 H7 项目将更加稳定可靠。