STM32H7 的 L1 Cache 与 DMA 数据一致性:从 Cache 维护操作到 MPU 配置的完整避坑指南

1. 为什么 Cache 和 DMA 会打架?

STM32H7 基于 Cortex-M7 内核,主频高达 480MHz,为了弥补 CPU 与存储器之间的速度鸿沟,芯片内部集成了 L1 Cache(I-Cache 和 D-Cache)。D-Cache 默认关闭,一旦开启,CPU 访问 SRAM 或外部 SDRAM 时,数据会先被缓存到 Cache Line(通常 32 字节)中。

DMA 控制器则绕过 Cache,直接读写物理内存。这就导致两个经典问题:

  • DMA 读取外设数据到内存:DMA 把新数据写入 SRAM,但 CPU 可能仍从 D-Cache 中读取旧数据(Cache 未失效)。
  • CPU 写入内存后启动 DMA 发送:CPU 写入的数据可能还在 D-Cache 中未写回 SRAM,DMA 却从 SRAM 读到了旧数据。

根本原因:Cache 与 DMA 对内存的访问不同步。

2. 三种解决思路对比

| 方法 | 原理 | 优点 | 缺点 | |------|------|------|------| | Cache 维护操作 | 手动 Clean/Invalidate | 灵活,按需使用 | 易遗漏,性能损耗 | | MPU 配置 Write-Through | 写操作同时更新 Cache 和内存 | 自动一致,性能较好 | 读操作仍需注意 | | MPU 配置 Non-Cacheable | 该区域完全不使用 Cache | 彻底避免一致性问题 | 牺牲性能 |

推荐策略:对 DMA 缓冲区使用 MPU 配置为 Non-Cacheable 或 Write-Through,既简单又可靠。若必须使用 Cacheable 内存,则严格配合 Clean/Invalidate 操作。

3. Cache 维护操作详解

Cortex-M7 提供了以下 CMSIS 函数:

  • 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。

使用场景:

  • CPU 写 → DMA 读(发送):调用 SCB_CleanDCache_by_Addr(),确保数据写入内存。
  • DMA 写 → CPU 读(接收):调用 SCB_InvalidateDCache_by_Addr(),丢弃旧 Cache,读取新数据。

注意:地址必须 32 字节对齐,长度需为 32 字节的整数倍,否则可能影响相邻数据。

4. MPU 配置实战

MPU(内存保护单元)可将特定内存区域设为 Non-Cacheable 或 Write-Through。以 STM32H7 的 SRAM1 区域(0x30000000)为例,配置为 Non-Cacheable:

#include "stm32h7xx.h"

void MPU_Config(void)
{
    HAL_MPU_Disable();

    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    // 区域 0:SRAM1 起始 0x30000000,大小 128KB,Non-Cacheable
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.Number = MPU_REGION_NUMBER0;
    MPU_InitStruct.BaseAddress = 0x30000000;
    MPU_InitStruct.Size = MPU_REGION_SIZE_128KB;
    MPU_InitStruct.SubRegionDisable = 0x0;
    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);
}

若希望保留一定性能,可配置为 Write-Through:

MPU_InitStruct.IsCacheable = MPU_ACCESS_CACHEABLE;
MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0; // Write-Through, no write allocate

关键点:MPU 区域必须按 2 的幂次对齐,且大小不能重叠。配置后需调用 SCB_EnableDCache() 使能 D-Cache。

5. 完整代码示例:UART DMA 接收

假设使用 UART4 接收不定长数据到缓冲区 rx_buf[256],该缓冲区位于 Non-Cacheable 区域(0x30000000)。

#define RX_BUF_SIZE 256
__attribute__((section(".non_cacheable"))) uint8_t rx_buf[RX_BUF_SIZE];

void UART_DMA_Init(void)
{
    // 1. 配置 MPU(略,见上文)
    MPU_Config();

    // 2. 使能 D-Cache
    SCB_EnableICache();
    SCB_EnableDCache();

    // 3. 初始化 UART4 和 DMA
    // ... HAL_UART_Init, HAL_DMA_Init ...

    // 4. 启动 DMA 接收
    HAL_UART_Receive_DMA(&huart4, rx_buf, RX_BUF_SIZE);
}

// DMA 接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == UART4) {
        // 由于 rx_buf 是 Non-Cacheable,CPU 直接读取内存,无需 Invalidate
        process_data(rx_buf, RX_BUF_SIZE);
        HAL_UART_Receive_DMA(&huart4, rx_buf, RX_BUF_SIZE); // 重新启动
    }
}

若缓冲区位于 Cacheable 区域,则回调中必须调用:

SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, RX_BUF_SIZE);

6. 避坑指南

  • 地址对齐:Cache 维护操作地址必须 32 字节对齐,长度建议为 32 的倍数。
  • DMA 描述符:若使用链表模式,描述符本身也需放在 Non-Cacheable 区域。
  • 中断与 Cache:在中断中调用 Cache 维护函数可能影响实时性,尽量在 DMA 完成回调中处理。
  • 多缓冲区:使用双缓冲时,每个缓冲区都要独立维护 Cache。
  • 调试陷阱:开启 D-Cache 后,通过调试器查看内存可能看到旧数据,需手动 Clean/Invalidate 或关闭 Cache 调试。
  • 性能权衡:Non-Cacheable 区域访问速度较慢,仅将 DMA 缓冲区设为 Non-Cacheable,其他代码和数据仍使用 Cache。

7. 总结

STM32H7 的 L1 Cache 与 DMA 数据一致性问题是嵌入式开发中的高频“坑点”。核心解决方案有三:Cache 维护操作、MPU 配置 Write-Through、MPU 配置 Non-Cacheable。推荐对 DMA 缓冲区使用 MPU 配置为 Non-Cacheable,简单可靠;若追求性能,可配置为 Write-Through 并配合 Clean 操作。无论哪种方案,务必注意地址对齐和操作时机,才能确保数据万无一失。