引言

STM32H7 系列(如 STM32H743/750)搭载 Cortex-M7 内核,主频高达 400MHz,内置 L1-Cache(I-Cache 和 D-Cache),性能直逼入门级应用处理器。然而,高主频带来的 Cache 一致性(Cache Coherency)问题,成为许多开发者从 F1/F4 升级到 H7 后的第一道坎。本文基于实际项目经验,剖析常见陷阱,并给出可落地的解决方案。

一、Cache 一致性问题的根源

1.1 什么是 Cache 一致性

Cortex-M7 的 D-Cache 是写回(Write-back)模式,CPU 写数据时先写入 Cache,标记为脏(Dirty),直到被替换或显式清理(Clean)才写回主存。而 DMA 控制器直接访问主存(SRAM),不经过 Cache。当 CPU 和 DMA 共享同一块内存时,就会产生数据不一致:

  • CPU 写,DMA 读:CPU 数据还在 Cache 中,DMA 从主存读到旧数据。
  • DMA 写,CPU 读:DMA 更新主存,但 CPU 的 Cache 中仍是旧数据(命中 Cache 返回陈旧值)。

1.2 为什么 H7 更严重

  • 主频 400MHz,Cache 命中率极高,数据在 Cache 中停留时间更长。
  • 大量外设(以太网、USB、SDMMC、ADC)支持 DMA,交互频繁。
  • 默认情况下,H7 的 D-Cache 是开启的,但很多开发者沿用 F1 的代码,未做任何处理。

二、实战陷阱:一个典型的 DMA 接收错乱案例

假设使用 UART + DMA 接收不定长数据,缓冲区定义如下:

#define BUF_SIZE 256
uint8_t rx_buf[BUF_SIZE] __attribute__((section(".ARM.__at_0x24000000"))); // 放在 AXI SRAM

初始化时开启 D-Cache,DMA 配置为循环模式。运行后,发现接收数据偶尔出现整段丢失或错乱,尤其当数据量较大时。

原因分析

  • DMA 将数据写入主存 rx_buf,但 CPU 的 D-Cache 中可能残留旧数据。
  • CPU 读取 rx_buf 时,Cache 命中,返回旧值,导致数据错乱。

三、解决方案:从原理到代码

3.1 方案一:关闭 D-Cache(不推荐)

直接关闭 D-Cache 可以避免问题,但性能损失巨大(400MHz 主频优势丧失殆尽)。仅用于调试阶段快速验证。

SCB_DisableDCache();

3.2 方案二:使用 MPU 配置内存区域为 Non-cacheable

将 DMA 缓冲区所在内存区域配置为 Non-cacheable(或 Write-through),从硬件层面避免不一致。这是最稳妥的做法,适合高频交互的缓冲区。

配置步骤

  1. 启用 MPU(Memory Protection Unit)。
  2. 设置区域属性为 Normal memory, Non-cacheable。
  3. 将缓冲区放入该区域。

代码示例(使用 CMSIS 函数):

void MPU_Config(void)
{
    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    // 禁用 MPU 进行配置
    HAL_MPU_Disable();

    // 配置区域0:0x24000000,大小 256KB,Non-cacheable
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = 0x24000000;
    MPU_InitStruct.Size = MPU_REGION_SIZE_256KB;
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.IsBufferable = MPU_REGION_NOT_BUFFERABLE;
    MPU_InitStruct.IsCacheable = MPU_REGION_NOT_CACHEABLE;
    MPU_InitStruct.IsShareable = MPU_REGION_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);

    // 启用 MPU
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

注意:缓冲区必须放在 0x24000000 起始的 AXI SRAM,且大小需与 MPU 区域匹配(可配置多个区域)。

3.3 方案三:软件维护 Cache(Clean & Invalidate)

对于性能要求高、且缓冲区无法固定为 Non-cacheable 的场景(如动态分配),需在 DMA 操作前后手动维护 Cache。

关键操作

  • DMA 写入前:SCB_CleanDCache_by_Addr 将 CPU 数据写回主存。
  • DMA 读取后:SCB_InvalidateDCache_by_Addr 使 Cache 行失效,强制从主存重新加载。

代码示例(UART DMA 接收):

// 启动 DMA 接收前,确保缓冲区无脏数据
SCB_CleanDCache_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)
{
    if (huart->Instance == USART1) {
        // 使 Cache 失效,强制从主存读取最新数据
        SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, BUF_SIZE);
        // 处理数据...
    }
}

注意事项

  • 地址必须 32 字节对齐(Cache line 大小),长度最好为 32 的倍数,否则可能误伤相邻数据。
  • 频繁调用会带来少量性能开销,但远小于关闭 Cache 的损失。

3.4 方案四:双缓冲 + Cache 维护(推荐)

结合方案三,使用双缓冲(Ping-Pong)机制,在 DMA 写入一个缓冲区时,CPU 处理另一个缓冲区,并交替进行 Cache 维护,实现零等待。

#define BUF_SIZE 256
uint8_t buf[2][BUF_SIZE] __attribute__((aligned(32)));
volatile uint8_t active_buf = 0;

// DMA 中断中切换缓冲区
void DMA_IRQHandler(void)
{
    // 使当前缓冲区失效,准备读取
    SCB_InvalidateDCache_by_Addr((uint32_t*)buf[active_buf], BUF_SIZE);
    // 处理 buf[active_buf] 数据...
    
    // 切换缓冲区
    active_buf ^= 1;
    // 清理新缓冲区,准备 DMA 写入
    SCB_CleanDCache_by_Addr((uint32_t*)buf[active_buf], BUF_SIZE);
    // 重新启动 DMA 到 buf[active_buf]
}

四、常见误区与注意事项

  • 误区1:只 Clean 不 Invalidate:DMA 写入后必须 Invalidate,否则 CPU 可能读到 Cache 中的旧数据。
  • 误区2:忽略对齐:Cache 操作要求地址 32 字节对齐,否则会引发 HardFault 或数据错误。
  • 误区3:在中断中做复杂 Cache 操作:中断中应尽量缩短操作时间,可将 Cache 维护放在主循环或任务中。
  • 误区4:MPU 配置后不检查:配置完 MPU 后,务必检查 HAL_MPU_Enable 返回值,并确认区域是否生效。
  • 注意:H7 的 D-Cache 默认开启,但 I-Cache 默认关闭,需在启动代码中显式开启(SCB_EnableICache())。
  • 调试技巧:使用 ST-Link 的 Live Watch 或内存窗口,观察主存和 Cache 的差异,快速定位问题。

五、总结

STM32H7 的 Cache 一致性是高性能开发的必修课。核心思路是:要么让 DMA 区域绕过 Cache(MPU 配置),要么在软件上精确维护 Cache 状态。推荐组合使用:固定缓冲区用 MPU 配置为 Non-cacheable,动态数据用双缓冲 + Clean/Invalidate。掌握这些技巧,你就能在 400MHz 主频下游刃有余,避免数据错乱的坑。

希望本文能帮你少走弯路,欢迎在评论区交流实战经验!