STM32H7 的 DCache 与 DMA 一致性:MPU 配置与 clean/invalidate 的实战避坑
STM32H7 系列搭载 Cortex-M7 内核,主频高达 480MHz,并配备了 L1 数据缓存(DCache)。开启 DCache 后,CPU 访问频繁的数据会缓存在 Cache 中,大幅提升性能。然而,DMA 控制器直接访问物理内存,不经过 Cache,这就导致了经典的 Cache 一致性 问题:CPU 写入的数据可能还在 Cache 中未写回内存,DMA 读取到旧数据;或者 DMA 写入的新数据被 CPU 的 Cache 旧副本覆盖。
本文将从原理出发,手把手教你配置 MPU 并正确使用 clean/invalidate 操作,彻底解决这一痛点。
一、为什么会出现一致性问题?
Cortex-M7 的 DCache 采用写回(Write-Back) 策略。当 CPU 写数据时,若 Cache 命中,只更新 Cache 而不立即写回内存;当 CPU 读数据时,若 Cache 命中,直接返回 Cache 内容而不读内存。
DMA 传输则直接读写物理内存。因此:
- CPU 写 → DMA 读:CPU 写入的数据可能还在 Cache 中,DMA 读到旧内存数据。
- DMA 写 → CPU 读:DMA 写入新数据到内存,但 CPU 的 Cache 中可能还保留着旧数据,CPU 读到旧值。
解决思路有两种:
- 将 DMA 缓冲区所在内存区域配置为 Non-Cacheable(通过 MPU)。
- 在 DMA 传输前后,手动执行 Clean(写回)和 Invalidate(无效化)操作。
二、MPU 配置:将 DMA 缓冲区设为 Non-Cacheable
最稳妥的方法是利用 MPU 将 DMA 缓冲区所在的内存区域(如 SRAM1、SRAM2 或 SDRAM 的某段)配置为 Non-Cacheable。这样 CPU 和 DMA 都直接访问物理内存,无需额外维护操作。
2.1 MPU 配置步骤
- 使能 MPU:
SCB->SHCSR |= SCB_SHCSR_MEMFAULTENA_Msk;(可选,用于调试) - 禁用 MPU:
MPU->CTRL = 0; - 配置 Region:设置基地址、属性、大小。
- 使能 Region:
MPU->CTRL |= MPU_CTRL_ENABLE_Msk; - 使能 Cache:
SCB_EnableICache(); SCB_EnableDCache();
2.2 完整 MPU 配置代码
#include "stm32h7xx.h"
// 将地址 0x30000000 开始的 64KB 区域配置为 Non-Cacheable
void MPU_Config(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_InitStruct = {0};
// 配置 Region 0
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
MPU_InitStruct.BaseAddress = 0x30000000;
MPU_InitStruct.Size = MPU_REGION_SIZE_64KB;
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; // 关键:非缓存
MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
注意:Region 的基地址必须按大小对齐(如 64KB 区域基地址需 64KB 对齐)。
三、Clean 与 Invalidate 操作时机
如果无法将缓冲区设为 Non-Cacheable(例如使用外部 SDRAM 且性能要求高),则必须在 DMA 传输前后手动维护 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 读:在启动 DMA 前执行 Clean。
- DMA 写 → CPU 读:在 DMA 完成后执行 Invalidate。
- 双向传输:先 Clean,DMA 完成后再 Invalidate。
3.3 代码示例
#define DMA_BUF_SIZE 1024
__attribute__((aligned(32))) uint8_t dma_tx_buf[DMA_BUF_SIZE];
__attribute__((aligned(32))) uint8_t dma_rx_buf[DMA_BUF_SIZE];
// 发送前:Clean
void DMA_SendData(void)
{
// 填充数据到 dma_tx_buf
for (int i = 0; i < DMA_BUF_SIZE; i++) {
dma_tx_buf[i] = i & 0xFF;
}
// 关键:将 Cache 写回内存
SCB_CleanDCache_by_Addr((uint32_t *)dma_tx_buf, DMA_BUF_SIZE);
// 启动 DMA 传输
HAL_DMA_Start(&hdma_memtomem, (uint32_t)dma_tx_buf, (uint32_t)dest, DMA_BUF_SIZE);
}
// 接收完成后:Invalidate
void DMA_ReceiveCompleteCallback(void)
{
// 关键:无效化 Cache,确保 CPU 读取到 DMA 写入的新数据
SCB_InvalidateDCache_by_Addr((uint32_t *)dma_rx_buf, DMA_BUF_SIZE);
// 现在可以安全读取 dma_rx_buf
process_data(dma_rx_buf, DMA_BUF_SIZE);
}
四、实战避坑指南
-
地址对齐:
SCB_CleanDCache_by_Addr的地址和大小建议 32 字节对齐,否则可能影响相邻数据。 - 缓冲区大小:Invalidate 的大小必须覆盖 DMA 实际写入的范围,不能多也不能少。
- 中断中调用:Clean/Invalidate 操作耗时,避免在高速中断中频繁调用。
- 多缓冲区:若使用多个 DMA 缓冲区,确保每个缓冲区独立对齐并单独维护。
- MPU 与手动维护二选一:不要既配置 Non-Cacheable 又手动 Clean/Invalidate,浪费性能。
- 调试时关闭 Cache:初期调试可先关闭 DCache,确认逻辑正确后再开启并处理一致性问题。
五、总结
STM32H7 的 DCache 是性能利器,但必须谨慎处理与 DMA 的一致性。推荐优先使用 MPU 将 DMA 缓冲区设为 Non-Cacheable,简单可靠;若必须使用 Cacheable 内存,则严格遵循“写前 Clean,读后 Invalidate”的原则,并注意地址对齐和操作范围。掌握这些技巧,你就能在 H7 上既享受 Cache 的高速,又保证 DMA 数据的正确性。