STM32H7 的 Cache 与 DMA 一致性问题:从原理到失效场景复现
一、为什么 Cache 与 DMA 会“打架”?
STM32H7 的 Cortex-M7 内核包含 L1 D-Cache(数据缓存)和 I-Cache(指令缓存),默认开启。CPU 访问内存时,若数据在 Cache 中则直接命中,否则从物理内存加载并缓存。而 DMA 是独立于 CPU 的总线主设备,直接读写物理内存,不经过 Cache。
这就导致两个经典问题:
- 读旧数据:CPU 先读某地址,数据被缓存;DMA 更新了物理内存,但 CPU 再次读取时仍命中旧 Cache,得到过时值。
- 写丢失:CPU 写数据只更新了 Cache(写回策略),未同步到物理内存;DMA 从物理内存读取,得到旧值。
根本原因:Cache 与 DMA 看到的内存视图不一致。
二、失效场景复现
场景 1:DMA 接收数据,CPU 读不到新值
uint8_t dma_buf[64] __attribute__((aligned(32)));
// 初始化 DMA 接收
HAL_UART_Receive_DMA(&huart1, dma_buf, 64);
// 等待接收完成...
// CPU 直接读 dma_buf,可能得到旧数据(Cache 未失效)
process(dma_buf);
若 dma_buf 曾被 CPU 读过,其内容在 D-Cache 中。DMA 将新数据写入物理内存后,CPU 读到的仍是 Cache 中的旧值。
场景 2:CPU 写数据,DMA 发送旧值
uint8_t tx_buf[64] __attribute__((aligned(32)));
fill_data(tx_buf); // CPU 写,可能只写 Cache
HAL_UART_Transmit_DMA(&huart1, tx_buf, 64); // DMA 从物理内存读,旧值
写回策略下,CPU 写操作可能停留在 Cache,未写回物理内存,DMA 读到旧数据。
三、解决方案与配置步骤
3.1 内存属性配置:使用 MPU 或链接脚本
推荐将 DMA 缓冲区放在 Non-Cacheable 区域,一劳永逸。
方法 A:MPU 配置
void MPU_Config(void) {
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_Init = {0};
MPU_Init.Enable = MPU_REGION_ENABLE;
MPU_Init.BaseAddress = 0x30000000; // D2 SRAM 起始
MPU_Init.Size = MPU_REGION_SIZE_64KB;
MPU_Init.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_Init.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
MPU_Init.IsCacheable = MPU_ACCESS_NOT_CACHEABLE; // 关键
MPU_Init.IsShareable = MPU_ACCESS_SHAREABLE;
MPU_Init.Number = MPU_REGION_NUMBER0;
MPU_Init.TypeExtField = MPU_TEX_LEVEL0;
MPU_Init.SubRegionDisable = 0x00;
MPU_Init.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
HAL_MPU_ConfigRegion(&MPU_Init);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
方法 B:链接脚本指定段
// 在 .ld 中定义 non-cacheable 段
.ncache (NOLOAD) : {
. = ALIGN(32);
*(.ncache)
} > RAM_D2
__attribute__((section(".ncache"), aligned(32))) uint8_t dma_buf[64];
3.2 软件维护:Clean / Invalidate 操作
若缓冲区仍可缓存,必须在 DMA 传输前后手动维护 Cache:
-
DMA 发送前:
SCB_CleanDCache_by_Addr()将 Cache 写回物理内存。 -
DMA 接收后:
SCB_InvalidateDCache_by_Addr()丢弃 Cache,强制从物理内存重读。
// 发送前 Clean
SCB_CleanDCache_by_Addr((uint32_t*)tx_buf, sizeof(tx_buf));
HAL_UART_Transmit_DMA(&huart1, tx_buf, sizeof(tx_buf));
// 接收后 Invalidate
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, sizeof(rx_buf));
process(rx_buf);
注意:地址必须 32 字节对齐,长度也建议 32 字节对齐,否则可能误伤相邻数据。
3.3 完整示例:UART DMA 收发
#include "stm32h7xx_hal.h"
__attribute__((aligned(32))) uint8_t rx_buf[64];
__attribute__((aligned(32))) uint8_t tx_buf[64];
void DMA_Init(void) {
// 使能 D-Cache(默认已使能,此处仅示意)
SCB_EnableDCache();
// 配置 MPU 将 rx_buf/tx_buf 所在区域设为 Non-Cacheable
MPU_Config();
}
void Send_Data(void) {
fill_data(tx_buf, 64);
// 若未用 MPU,则 Clean
SCB_CleanDCache_by_Addr((uint32_t*)tx_buf, 64);
HAL_UART_Transmit_DMA(&huart1, tx_buf, 64);
}
void Receive_Data(void) {
HAL_UART_Receive_DMA(&huart1, rx_buf, 64);
}
// DMA 接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
// 若未用 MPU,则 Invalidate
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, 64);
process(rx_buf, 64);
}
四、注意事项与避坑指南
-
对齐要求:Cache 维护函数要求地址 32 字节对齐,缓冲区定义时加
__attribute__((aligned(32)))。 -
长度对齐:
SCB_*Cache_by_Addr的长度最好为 32 的整数倍,避免误清相邻变量。 - 避免频繁维护:Clean/Invalidate 有性能开销,优先用 MPU 将 DMA 缓冲区设为 Non-Cacheable。
- 多缓冲区:若使用多个 DMA 缓冲区,确保它们不共享同一 Cache Line(32 字节),否则 Invalidate 一个可能影响另一个。
-
I-Cache 与自修改代码:若运行时修改代码(如 Bootloader 跳转),需
SCB_InvalidateICache()。 - 调试陷阱:调试器可能绕过 Cache,导致“调试正常,脱机异常”,务必实机验证。
五、总结
STM32H7 的 Cache 与 DMA 一致性问题本质是缓存视图与物理内存视图的差异。解决方案有三层:MPU 配置 Non-Cacheable 区域(推荐)、链接脚本指定段、软件 Clean/Invalidate。理解原理后,结合对齐与长度约束,即可稳定驾驭 H7 的高性能 DMA 传输。