STM32H7 的 D-Cache 与 DMA 数据一致性排查:从 MPU 配置到 clean/invalidate 时机
STM32H7 主频高达 480MHz,为了匹配内核速度,芯片内置了 L1 D-Cache。但 DMA 直接访问物理内存,不经过 Cache,若 CPU 与 DMA 共享缓冲区,就会出现数据不一致:CPU 写入的数据可能还在 Cache 中,DMA 读到旧值;DMA 写入的新数据可能被 Cache 中的旧数据覆盖。本文系统讲解如何通过 MPU 配置和 clean/invalidate 操作解决这一问题。
1. 为什么会出现数据一致性错误?
- 写通 vs 写回:H7 的 D-Cache 默认写回(Write-Back),CPU 写数据只更新 Cache,不立即写回内存。
- DMA 视角:DMA 控制器直接读写 SRAM,看不到 Cache 内容。
-
典型场景:
- CPU 填充发送缓冲区 → 启动 DMA 发送 → DMA 读到旧数据。
- DMA 接收数据到缓冲区 → CPU 读缓冲区 → 读到 Cache 中的旧数据。
2. 解决思路:MPU 配置 + Cache 维护
两种手段结合使用:
- MPU 配置:将 DMA 缓冲区所在内存区域配置为 Non-Cacheable 或 Write-Through,避免 Cache 介入。
- Cache 维护:对 Cacheable 区域,在 DMA 传输前后手动 clean/invalidate。
2.1 MPU 配置示例
将 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_LEVEL0;
MPU_InitStruct.SubRegionDisable = 0x00;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
注意:Non-Cacheable 会降低 CPU 访问速度,适合频繁 DMA 的小缓冲区。
2.2 Cache 维护函数
CMSIS 提供以下函数(需包含 core_cm7.h):
-
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. 正确使用时机
3.1 DMA 发送(内存 → 外设)
- CPU 填充缓冲区后,必须 clean,确保数据写入内存。
- 启动 DMA 传输。
- 传输完成中断中,可再次 clean(非必须)。
uint8_t tx_buf[64] __attribute__((aligned(32)));
void DMA_Send(void)
{
// 填充数据
for (int i = 0; i < 64; i++) tx_buf[i] = i;
// Clean D-Cache,将数据写回内存
SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, 64);
HAL_UART_Transmit_DMA(&huart1, tx_buf, 64);
}
3.2 DMA 接收(外设 → 内存)
- 启动 DMA 前,invalidate 缓冲区,丢弃旧 Cache 数据。
- DMA 传输完成中断中,再次 invalidate,确保 CPU 读到最新数据。
uint8_t rx_buf[64] __attribute__((aligned(32)));
void DMA_Receive_Start(void)
{
// 无效化 Cache,避免旧数据干扰
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, 64);
HAL_UART_Receive_DMA(&huart1, rx_buf, 64);
}
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
// 传输完成,无效化 Cache,读取最新数据
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, 64);
// 处理 rx_buf
}
4. 完整示例:UART DMA 回环
#include "stm32h7xx_hal.h"
UART_HandleTypeDef huart1;
DMA_HandleTypeDef hdma_usart1_tx;
DMA_HandleTypeDef hdma_usart1_rx;
#define BUF_SIZE 64
__attribute__((aligned(32))) uint8_t tx_buf[BUF_SIZE];
__attribute__((aligned(32))) uint8_t rx_buf[BUF_SIZE];
void SystemClock_Config(void);
static void MX_DMA_Init(void);
static void MX_USART1_UART_Init(void);
int main(void)
{
HAL_Init();
SystemClock_Config();
MPU_Config(); // 配置 MPU
SCB_EnableICache(); // 使能 I-Cache
SCB_EnableDCache(); // 使能 D-Cache
MX_DMA_Init();
MX_USART1_UART_Init();
// 准备发送数据
for (int i = 0; i < BUF_SIZE; i++) tx_buf[i] = i;
// 启动接收
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, BUF_SIZE);
HAL_UART_Receive_DMA(&huart1, rx_buf, BUF_SIZE);
// 发送数据
SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, BUF_SIZE);
HAL_UART_Transmit_DMA(&huart1, tx_buf, BUF_SIZE);
while (1) {}
}
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1) {
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, BUF_SIZE);
// 回显
SCB_CleanDCache_by_Addr((uint32_t *)rx_buf, BUF_SIZE);
HAL_UART_Transmit_DMA(&huart1, rx_buf, BUF_SIZE);
}
}
5. 注意事项与排查技巧
- 地址对齐:Cache 维护函数要求地址 32 字节对齐,缓冲区大小也应为 32 字节倍数。
- 避免频繁 clean/invalidate:影响性能,可权衡使用 Non-Cacheable 区域。
- DMA 描述符:若使用链表模式,描述符本身也需放在 Non-Cacheable 区域或手动维护。
-
调试方法:
- 在 DMA 传输前后读取内存与 Cache 内容对比。
- 使用
SCB_InvalidateDCache()全局无效化(慎用,影响所有 Cache)。 - 关闭 D-Cache 测试是否问题消失,快速定位。
- RTOS 环境:任务切换时可能触发 Cache 操作,需确保临界区保护。
6. 总结
STM32H7 的 D-Cache 与 DMA 共存需要开发者显式管理一致性。推荐策略:
- 对 DMA 缓冲区使用 MPU 配置为 Non-Cacheable,简单可靠。
- 若必须 Cacheable,严格遵循:发送前 clean,接收前 invalidate,接收后 invalidate。
- 始终保证 32 字节对齐,并利用 CMSIS 函数操作。
掌握这些要点,即可彻底解决 DMA 数据错乱问题,充分发挥 H7 的高性能。