STM32H7 的 D-Cache 与 DMA 一致性坑:MPU 配置与 Clean/Invalidate 的正确顺序
STM32H7 系列搭载 Cortex-M7 内核,主频可达 480MHz,并配备 L1 数据缓存(D-Cache)。D-Cache 能显著提升 CPU 访问外部存储器的效率,但一旦与 DMA 控制器协同工作,就可能出现数据不一致问题。许多开发者在使用 DMA 传输时发现数据“莫名其妙”出错,根源往往在于 Cache 未正确维护。本文将系统讲解 D-Cache 与 DMA 的冲突原理、MPU 配置方法,以及 Clean/Invalidate 的正确使用顺序。
一、为什么 D-Cache 会与 DMA 冲突?
Cortex-M7 的 D-Cache 是写回(Write-Back)且写分配(Write-Allocate)的。CPU 写数据时,若命中 Cache,数据只写入 Cache,不会立即写回物理内存;CPU 读数据时,若命中 Cache,直接返回 Cache 内容,不访问物理内存。
而 DMA 控制器直接访问物理内存(SRAM、SDRAM 等),它“看不见”Cache。因此:
- CPU 写 → DMA 读:CPU 写入的数据可能还在 Cache 中,未同步到内存,DMA 读到旧数据。
- DMA 写 → CPU 读:DMA 将新数据写入内存,但 CPU 可能从 Cache 中读到旧数据。
这就是所谓“Cache 一致性”问题。
二、解决思路:MPU 配置 + Cache 维护
STM32H7 提供了两种手段:
- MPU 配置:将 DMA 缓冲区所在内存区域配置为 Non-Cacheable 或 Write-Through,从根源上避免不一致。
-
手动 Cache 维护:在 DMA 传输前后调用
SCB_CleanDCache_by_Addr()和SCB_InvalidateDCache_by_Addr()。
通常建议对频繁 DMA 的大缓冲区使用 MPU 配置为 Non-Cacheable,对小数据或临时缓冲区使用手动维护。
三、MPU 配置步骤(以 SDRAM 区域为例)
假设 DMA 缓冲区位于 SDRAM 地址 0xC0000000,大小 1MB。配置该区域为 Non-Cacheable:
#include "stm32h7xx_hal.h"
void MPU_Config(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_InitStruct = {0};
// 区域 0:SDRAM 0xC0000000 - 0xC00FFFFF (1MB) Non-Cacheable
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
MPU_InitStruct.BaseAddress = 0xC0000000;
MPU_InitStruct.Size = MPU_REGION_SIZE_1MB;
MPU_InitStruct.SubRegionDisable = 0x00;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE; // 关键:Non-Cacheable
MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
调用 MPU_Config() 后,CPU 对该区域的访问将绕过 D-Cache,DMA 与 CPU 看到的内存完全一致。
四、手动 Cache 维护的正确顺序
若缓冲区仍为 Cacheable,则必须在 DMA 传输前后手动维护 Cache。顺序至关重要:
1. CPU 写 → DMA 读(发送方向)
- 步骤1:CPU 填充缓冲区数据。
-
步骤2:调用
SCB_CleanDCache_by_Addr(),将 Cache 中“脏”数据写回内存。 - 步骤3:启动 DMA 发送。
uint8_t tx_buf[1024] __attribute__((aligned(32)));
// 填充数据
for (int i = 0; i < 1024; i++) tx_buf[i] = i;
// Clean D-Cache,确保数据写入物理内存
SCB_CleanDCache_by_Addr((uint32_t*)tx_buf, 1024);
// 启动 DMA 发送
HAL_UART_Transmit_DMA(&huart1, tx_buf, 1024);
2. DMA 写 → CPU 读(接收方向)
- 步骤1:启动 DMA 接收。
- 步骤2:等待 DMA 传输完成(中断或轮询)。
-
步骤3:调用
SCB_InvalidateDCache_by_Addr(),丢弃 Cache 中的旧数据,强制 CPU 从内存重新读取。 - 步骤4:CPU 读取缓冲区。
uint8_t rx_buf[1024] __attribute__((aligned(32)));
// 启动 DMA 接收
HAL_UART_Receive_DMA(&huart1, rx_buf, 1024);
// 等待传输完成(假设使用中断,此处简化为轮询)
while (HAL_UART_GetState(&huart1) != HAL_UART_STATE_READY);
// Invalidate D-Cache,丢弃旧数据
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, 1024);
// 现在可以安全读取 rx_buf
process_data(rx_buf, 1024);
3. 双向传输(如 SPI 全双工)
- 发送前:Clean TX 缓冲区。
- 接收后:Invalidate RX 缓冲区。
- 注意:若 TX 和 RX 是同一缓冲区,则先 Clean 再启动,传输完成后 Invalidate。
五、关键注意事项
-
地址对齐:
SCB_CleanDCache_by_Addr()和SCB_InvalidateDCache_by_Addr()要求地址 32 字节对齐,长度也建议为 32 字节倍数。使用__attribute__((aligned(32)))修饰缓冲区。 - Invalidate 的风险:如果缓冲区中有些数据是 CPU 刚写入但尚未 Clean 的,Invalidate 会直接丢弃,导致数据丢失。因此 Invalidate 前确保没有未 Clean 的写操作。
- 不要过度维护:频繁 Clean/Invalidate 会降低性能。对于大数据量、高频率的 DMA,优先使用 MPU 配置为 Non-Cacheable。
- 中断与 Cache:在 DMA 完成中断中调用 Invalidate 是常见做法,但需确保中断优先级配置正确,避免在 Invalidate 过程中 CPU 访问同一缓冲区。
- 多缓冲区管理:使用多个缓冲区时,每个缓冲区都要独立维护 Cache,不能遗漏。
-
编译器优化:使用
volatile或内存屏障(__DSB())确保操作顺序,但 Cache 维护函数内部已包含屏障。
六、总结
STM32H7 的 D-Cache 与 DMA 一致性问题是嵌入式开发中的经典“坑”。核心原则:DMA 看不到 Cache,CPU 与 DMA 共享数据时必须保证内存与 Cache 同步。优先使用 MPU 将 DMA 缓冲区设为 Non-Cacheable;若必须 Cacheable,则严格遵循“发送前 Clean,接收后 Invalidate”的顺序,并注意地址对齐和操作时机。掌握这些,你的 H7 项目将更加稳定可靠。