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 维护

两种手段结合使用:

  1. MPU 配置:将 DMA 缓冲区所在内存区域配置为 Non-Cacheable 或 Write-Through,避免 Cache 介入。
  2. 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 的高性能。