STM32H7 的 D-Cache 与 DMA 一致性坑:从 Cache 维护 API 到 MPU 配置的完整避坑指南
STM32H7 系列凭借 480MHz 主频和 Cortex-M7 内核,成为高性能嵌入式应用的首选。但很多开发者开启 D-Cache 后,DMA 传输的数据会莫名其妙地错乱——发送的数据不对、接收的数据丢失。这背后是 Cache 与 DMA 的数据一致性问题。本文从原理到实践,带你彻底填平这个坑。
一、为什么会有 Cache 一致性问题?
Cortex-M7 的 D-Cache 位于 CPU 和主存之间。CPU 访问数据时,会先把数据从主存加载到 Cache,后续读写都直接操作 Cache,主存中的数据可能还是旧的。
- DMA 写内存(如外设接收数据):DMA 直接把数据写入主存,但 CPU 可能仍从 Cache 读取旧数据。
- DMA 读内存(如外设发送数据):CPU 把新数据写入 Cache,但尚未写回主存,DMA 读到的是旧数据。
这就是一致性问题。解决方式有两种:软件维护 Cache 或 配置 MPU 将 DMA 缓冲区设为 Non-Cacheable。
二、Cache 维护 API 的正确用法
CMSIS 提供了 SCB_CleanDCache_by_Addr、SCB_InvalidateDCache_by_Addr 等函数。关键点:
- Clean:将 Cache 中已修改的数据写回主存。用于 CPU 写、DMA 读 的场景(发送前调用)。
- Invalidate:丢弃 Cache 中的旧数据,强制从主存重新加载。用于 DMA 写、CPU 读 的场景(接收后调用)。
- 地址必须 32 字节对齐,长度建议向上取整到 32 字节的倍数,否则可能误伤相邻数据。
// 发送前:确保 CPU 写入的数据到达主存
SCB_CleanDCache_by_Addr((uint32_t*)tx_buf, sizeof(tx_buf));
HAL_UART_Transmit_DMA(&huart1, tx_buf, sizeof(tx_buf));
// 接收后:丢弃 Cache 旧数据,从主存重新加载
HAL_UART_Receive_DMA(&huart1, rx_buf, sizeof(rx_buf));
// 等待传输完成...
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, sizeof(rx_buf));
注意:
Invalidate会丢弃未写回的数据,若缓冲区同时被 CPU 写过,应先Clean再Invalidate。
三、MPU 配置:一劳永逸的方案
频繁调用维护 API 既繁琐又影响性能。更好的做法是用 MPU 将 DMA 缓冲区所在的内存区域配置为 Non-Cacheable,让硬件自动保证一致性。
以 STM32H7 为例,将 0x30000000 开始的 32KB(D2 SRAM)配置为 Non-Cacheable:
void MPU_Config(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_InitStruct = {0};
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0x30000000;
MPU_InitStruct.Size = MPU_REGION_SIZE_32KB;
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_SHAREABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL1;
MPU_InitStruct.SubRegionDisable = 0x00;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
配置后,DMA 缓冲区直接读写主存,无需任何 Cache 维护操作。
四、完整示例:UART DMA 收发
#include "stm32h7xx_hal.h"
#define BUF_SIZE 64
// 将缓冲区放到 Non-Cacheable 区域(0x30000000)
__attribute__((section(".RAM_D2"))) uint8_t rx_buf[BUF_SIZE];
__attribute__((section(".RAM_D2"))) uint8_t tx_buf[BUF_SIZE];
void UART_DMA_Init(void)
{
MPU_Config(); // 先配置 MPU
// ... UART 和 DMA 初始化代码
HAL_UART_Receive_DMA(&huart1, rx_buf, BUF_SIZE);
}
void Send_Data(void)
{
for (int i = 0; i < BUF_SIZE; i++) tx_buf[i] = i;
// 若缓冲区为 Cacheable,需 Clean;Non-Cacheable 则无需
HAL_UART_Transmit_DMA(&huart1, tx_buf, BUF_SIZE);
}
五、避坑注意事项
- 地址对齐:Cache 维护 API 要求地址 32 字节对齐,长度建议为 32 的倍数。
- MPU 区域大小:必须为 2 的幂,且基地址对齐到区域大小。
- DMA 描述符:若使用链表模式,描述符本身也应放在 Non-Cacheable 区域。
- 多缓冲区:每个 DMA 缓冲区都要单独处理,不能只维护一个。
- 性能权衡:Non-Cacheable 区域访问速度较慢,仅将 DMA 缓冲区放入,其他数据仍用 Cache。
-
调试技巧:若数据错乱,先检查是否忘记 Clean/Invalidate,或用
__DSB()确保操作完成。
掌握这些要点,你就能在 STM32H7 上放心使用 D-Cache 和 DMA,充分发挥芯片性能。