STM32H7 的 Cache 与 DMA 一致性:MPU 配置与 Clean/Invalidate 的实战避坑
STM32H7 系列搭载 Cortex-M7 内核,主频高达 480MHz,并配备 L1 数据 Cache(D-Cache)和指令 Cache(I-Cache)。Cache 的引入显著提升了 CPU 访问外部存储器的效率,但也给 DMA 传输带来了严重的数据一致性挑战。许多开发者在使用 DMA 时遇到数据错乱、传输不完整等问题,根源往往在于 Cache 与 DMA 的协同不当。本文将深入剖析原理,给出 MPU 配置与 Clean/Invalidate 的实战方案。
一、为什么 Cache 会导致 DMA 数据不一致?
Cortex-M7 的 D-Cache 采用写回(Write-Back)策略。当 CPU 写数据时,数据先写入 Cache,并不立即同步到物理内存(如 SRAM、SDRAM)。如果此时启动 DMA 从该内存地址读取数据发送到外设,DMA 取到的是旧数据,导致发送错误。
反之,当 DMA 从外设接收数据写入内存后,CPU 读取该内存时,若 Cache 中恰好有该地址的旧缓存行,CPU 会直接返回旧数据,而不会从内存重新加载,导致读取错误。
- 写方向(CPU→内存→DMA):CPU 写数据后,需执行 Clean 操作,将 Cache 内容写回内存,再启动 DMA。
- 读方向(DMA→内存→CPU):DMA 写内存后,需执行 Invalidate 操作,使 Cache 中对应行失效,强制 CPU 从内存重新加载。
二、MPU 配置:为 DMA 缓冲区划定非缓存区域
最彻底的解决方案是将 DMA 缓冲区配置为 Non-Cacheable,这样 CPU 和 DMA 都直接访问物理内存,无需 Clean/Invalidate。STM32H7 通过 MPU(Memory Protection Unit)实现。
2.1 MPU 配置步骤
- 使能 MPU:
SCB->SHCSR |= SCB_SHCSR_MEMFAULTENA_Msk; - 禁用 MPU:
ARM_MPU_Disable(); - 配置 Region:设置基地址、大小、属性(Normal, Non-Cacheable)。
- 使能 MPU:
ARM_MPU_Enable(MPU_CTRL_PRIVDEFENA_Msk);
2.2 完整 MPU 配置代码(以 0x30000000 开始的 32KB 为例)
#include "stm32h7xx.h"
void MPU_Config(void)
{
ARM_MPU_Disable();
// 配置 Region 0:DMA 缓冲区,非缓存、非共享、可读写
ARM_MPU_SetRegion(
0, // Region 编号
0x30000000, // 基地址
ARM_MPU_RASR( // 属性
0, // 禁用指令访问
ARM_MPU_AP_FULL, // 全权限
0, // 非共享
0, // 非缓存
0, // 非缓冲
0, // 非子区域禁用
ARM_MPU_SIZE_32KB, // 大小 32KB
1 // 使能 Region
)
);
ARM_MPU_Enable(MPU_CTRL_PRIVDEFENA_Msk);
__DSB();
__ISB();
}
注意:
ARM_MPU_RASR宏参数需根据 CMSIS 版本调整,核心是设置TEX=0, C=0, B=0实现 Non-Cacheable。
三、Clean 与 Invalidate 的正确使用
若无法将缓冲区设为非缓存(例如缓冲区在 DTCM 中,DTCM 本身不经过 Cache),则必须手动维护 Cache。
3.1 关键函数
-
SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize):将 Cache 内容写回内存。 -
SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize):使 Cache 行失效。 -
SCB_CleanInvalidateDCache_by_Addr():先 Clean 再 Invalidate。
3.2 使用时机
-
发送前(CPU 写数据,DMA 读):调用
SCB_CleanDCache_by_Addr()。 -
接收后(DMA 写数据,CPU 读):调用
SCB_InvalidateDCache_by_Addr()。 -
接收前(防止 DMA 覆盖前 Cache 脏数据):先
SCB_InvalidateDCache_by_Addr(),再启动 DMA。
3.3 代码示例:UART DMA 发送
#define DMA_BUF_SIZE 128
__attribute__((aligned(32))) uint8_t dma_tx_buf[DMA_BUF_SIZE];
void UART_DMA_Send(uint8_t *data, uint16_t len)
{
memcpy(dma_tx_buf, data, len);
SCB_CleanDCache_by_Addr((uint32_t*)dma_tx_buf, len);
HAL_UART_Transmit_DMA(&huart1, dma_tx_buf, len);
}
3.4 代码示例:UART DMA 接收
__attribute__((aligned(32))) uint8_t dma_rx_buf[DMA_BUF_SIZE];
void UART_DMA_Receive_Start(void)
{
SCB_InvalidateDCache_by_Addr((uint32_t*)dma_rx_buf, DMA_BUF_SIZE);
HAL_UART_Receive_DMA(&huart1, dma_rx_buf, DMA_BUF_SIZE);
}
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
SCB_InvalidateDCache_by_Addr((uint32_t*)dma_rx_buf, DMA_BUF_SIZE);
// 此时 dma_rx_buf 中为最新数据
}
四、实战避坑指南
-
对齐问题:Cache 操作以 32 字节为单位。缓冲区地址和长度必须 32 字节对齐,否则 Clean/Invalidate 可能影响相邻数据。使用
__attribute__((aligned(32)))强制对齐。 - DTCM 与 Cache:DTCM 不经过 Cache,DMA 可直接访问,无需 Clean/Invalidate。但 DTCM 地址范围有限(通常 128KB),且 DMA 无法访问所有 DTCM 区域,需查阅参考手册。
- 多缓冲区管理:若使用多个 DMA 缓冲区,确保每个缓冲区独立对齐,避免 Cache 行重叠。
- 中断中操作:在 DMA 完成中断中执行 Invalidate 是安全的,但需注意中断优先级和 Cache 操作的原子性。
- 性能权衡:频繁 Clean/Invalidate 会降低性能。对于高速数据流,优先使用 MPU 非缓存区域。
- 调试陷阱:在调试器中查看变量可能触发 Cache 行为变化,建议通过内存窗口直接查看物理地址。
五、总结
STM32H7 的 Cache 与 DMA 一致性问题是嵌入式开发中的经典难题。核心原则是:要么让 DMA 缓冲区不经过 Cache(MPU 配置),要么在正确时机手动维护 Cache(Clean/Invalidate)。理解数据流向,严格对齐缓冲区,结合 MPU 与 Cache 操作,才能确保系统稳定可靠。希望本文的实战经验能帮助你避开这些坑,充分发挥 H7 的高性能优势。