STM32H7 的 D-Cache 与 DMA 一致性排查:从 MPU 配置到 Clean/Invalidate 的实战避坑
STM32H7 系列凭借 480MHz 主频和 Art Accelerator 成为高性能嵌入式首选,但开启 D-Cache 后,DMA 传输数据错乱、外设读取旧值等问题频发。根源在于 Cortex-M7 的 D-Cache 与 DMA 访问内存时缺乏硬件一致性。本文从原理到实战,帮你彻底理清。
一、为什么 DMA 和 D-Cache 会打架?
Cortex-M7 的 D-Cache 是写回(Write-Back) 策略:CPU 写数据时只更新 Cache,不立即写回 SRAM;读数据时若 Cache 未命中,则从 SRAM 加载并缓存。
DMA 控制器直接访问 SRAM,不经过 Cache。于是出现两种典型错误:
- CPU 写、DMA 读:CPU 写的数据还在 Cache 中(脏行),DMA 从 SRAM 读到旧数据。
- DMA 写、CPU 读:DMA 将新数据写入 SRAM,但 CPU 读的是 Cache 中的旧数据(若该地址曾被缓存)。
解决思路:在 DMA 传输前后,手动维护 Cache 一致性——Clean(写回) 和 Invalidate(无效化)。
二、MPU 配置:为 DMA 缓冲区划定安全区
最优雅的方案是利用 MPU 将 DMA 缓冲区配置为 Non-Cacheable,一劳永逸。但并非所有场景都适用(如需要 CPU 频繁访问的大缓冲区)。
2.1 MPU 配置步骤
- 使能 MPU:
SCB_EnableMPU()前先SCB_DisableMPU()。 - 配置 Region:基地址、大小、属性。
- 使能 Region 和 MPU。
#include "stm32h7xx.h"
void MPU_Config(void)
{
MPU_Region_InitTypeDef MPU_InitStruct = {0};
HAL_MPU_Disable();
// 将 0x30000000 开始的 64KB 配置为 Non-Cacheable, Non-Shared
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0x30000000;
MPU_InitStruct.Size = MPU_REGION_SIZE_64KB;
MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
MPU_InitStruct.SubRegionDisable = 0x00;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
注意:DMA 缓冲区必须按 Cache 行大小(32 字节)对齐,否则 Clean/Invalidate 会波及相邻数据。
三、Clean 与 Invalidate 的正确使用
若缓冲区仍为 Cacheable,则必须在 DMA 前后手动操作。
3.1 操作时机
-
CPU 写 → DMA 读(发送):DMA 启动前执行
SCB_CleanDCache_by_Addr(),把脏数据写回 SRAM。 -
DMA 写 → CPU 读(接收):DMA 完成后执行
SCB_InvalidateDCache_by_Addr(),丢弃 Cache 旧数据。
3.2 完整代码示例(UART DMA 接收)
#define BUF_SIZE 128
__attribute__((aligned(32))) uint8_t rx_buf[BUF_SIZE];
void UART_DMA_Start(void)
{
// 接收前无效化 Cache,确保 DMA 写入的数据不被 Cache 覆盖
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, BUF_SIZE);
HAL_UART_Receive_DMA(&huart1, rx_buf, BUF_SIZE);
}
// DMA 完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
// 接收完成后再次无效化,让 CPU 读到 DMA 写入的新数据
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, BUF_SIZE);
// 此时 rx_buf 中才是有效数据
process_data(rx_buf, BUF_SIZE);
}
发送方向同理:
void UART_DMA_Send(uint8_t *data, uint16_t len)
{
// 发送前 Clean,确保 DMA 从 SRAM 读到最新数据
SCB_CleanDCache_by_Addr((uint32_t*)data, len);
HAL_UART_Transmit_DMA(&huart1, data, len);
}
四、避坑指南
-
地址对齐:
SCB_*DCache_by_Addr的地址必须 32 字节对齐,长度建议为 32 的倍数,否则可能误伤相邻变量。 - 不要对同一缓冲区同时 Clean 和 Invalidate:接收场景只需 Invalidate,发送只需 Clean。
- 中断中谨慎操作:Cache 维护函数执行时间较长,避免在高速中断中频繁调用。
- DMA 描述符也要注意:若使用链表式 DMA,描述符本身若在 Cacheable 区域,同样需要 Clean。
- 多核/多主控场景:H7 双核间共享内存需配置为 Non-Cacheable 或使用硬件信号量。
- 调试时关闭 Cache:若问题诡异,可先关闭 D-Cache 验证是否为一致性问题。
五、总结
STM32H7 的 D-Cache 是把双刃剑。推荐优先使用 MPU 将 DMA 缓冲区设为 Non-Cacheable,简单可靠;若必须 Cacheable,则严格遵循 发送前 Clean、接收后 Invalidate 的原则,并注意地址对齐。掌握这些,DMA 数据错乱将不再是玄学。