引言

STM32H7 系列凭借 Cortex-M7 内核和 480MHz 主频,成为高性能嵌入式应用的首选。然而,高性能的代价之一便是 Cache 一致性问题。当 CPU 通过 D-Cache 访问内存,而 DMA 直接读写物理内存时,两者看到的可能不是同一份数据,导致数据错乱、系统死机等棘手 Bug。本文面向有经验的开发者,总结几种实用策略,帮助你在 STM32H7 上安全驾驭 Cache。

1. 问题根源:Cache 与 DMA 的“信息孤岛”

Cortex-M7 内置 I-Cache 和 D-Cache,其中 D-Cache 用于缓存数据。CPU 写数据时,可能只写入 Cache(写回策略),而 DMA 则直接访问 SRAM。此时,DMA 读到的可能是旧数据;反之,DMA 写入 SRAM 后,Cache 中仍保留旧值,CPU 读到的也是过时数据。

  • 写回(Write-back):CPU 写操作仅更新 Cache,直到被替换或显式 clean 才写回内存。
  • 写分配(Write-allocate):读缺失时,先从内存加载到 Cache,再修改。

STM32H7 默认开启 D-Cache,且使用写回策略,因此必须主动维护一致性。

2. 策略一:使用 MPU 配置内存区域为“非 Cacheable”

最简单粗暴的方式,是通过 MPU(内存保护单元)将 DMA 涉及的缓冲区设置为“非 Cacheable”或“写通(Write-through)”。这样 CPU 访问该区域时直接操作内存,绕过 Cache,彻底避免不一致。

配置步骤:

  1. MPU_Config 中定义区域,例如将 SRAM4(DMA 常用)设为非 Cacheable。
  2. 使用 HAL_MPU_ConfigRegion() 初始化。
  3. 使能 MPU。
// 示例:配置 SRAM4 为非 Cacheable
MPU_Region_InitTypeDef MPU_InitStruct = {0};

HAL_MPU_Disable();

MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0x38000000; // SRAM4 起始地址
MPU_InitStruct.Size = MPU_REGION_SIZE_64KB;
MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_InitStruct.IsBufferable = MPU_REGION_NOT_BUFFERABLE;
MPU_InitStruct.IsCacheable = MPU_REGION_NOT_CACHEABLE; // 关键:非 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_DISABLE;

HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_CONTROL_HRD_MEM);

优点:简单可靠,无需每次操作都手动维护。 缺点:牺牲了该区域的缓存性能,对于高频访问的缓冲区可能影响速度。

3. 策略二:软件维护——Clean 和 Invalidate 操作

如果缓冲区需要高性能,可以保持 Cacheable,但在 DMA 操作前后显式执行 Cache 维护指令。Cortex-M7 提供了 SCB_CleanDCache()SCB_InvalidateDCache() 等函数。

典型流程:

  • DMA 发送数据(CPU 写,DMA 读):

    1. CPU 写入数据到缓冲区。
    2. 调用 SCB_CleanDCache_by_Addr() 将缓冲区数据写回内存。
    3. 启动 DMA 发送。
  • DMA 接收数据(DMA 写,CPU 读):

    1. 启动 DMA 接收。
    2. 等待 DMA 完成。
    3. 调用 SCB_InvalidateDCache_by_Addr() 使 Cache 中的旧数据失效,强制 CPU 从内存重新读取。
// 示例:DMA 接收后使 Cache 失效
#define BUF_SIZE 1024
uint8_t rx_buffer[BUF_SIZE] __attribute__((aligned(32))); // 注意对齐

// 启动 DMA 接收(略)
// 等待 DMA 完成

// 使缓冲区对应的 Cache 行失效
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buffer, BUF_SIZE);

// 现在可以安全读取 rx_buffer

注意事项

  • 缓冲区地址必须 32 字节对齐(Cache line 大小),否则操作可能无效。
  • 使用 __attribute__((aligned(32))) 强制对齐。
  • 若缓冲区大小不是 32 的倍数,需向上取整,避免越界。

4. 策略三:双缓冲区 + 乒乓机制

对于高速数据流(如 ADC 采样),可以采用双缓冲区交替使用,配合 Cache 维护,实现零拷贝和高效处理。

原理:

  • 定义两个缓冲区 A 和 B。
  • 当 DMA 正在填充 A 时,CPU 处理 B(此时 B 已通过 invalidate 保证一致性)。
  • 完成后交换角色。

实现要点:

  • 每个缓冲区独立进行 Cache 操作。
  • 使用 DMA 双缓冲模式(如 STM32H7 的 DMA 支持双缓冲)。
  • 在中断中切换缓冲区,并触发 Cache 操作。
// 伪代码示例
#define BUF_SIZE 1024
uint8_t buf[2][BUF_SIZE] __attribute__((aligned(32)));
volatile uint8_t active_buf = 0;

void DMA_IRQHandler(void) {
    // DMA 传输完成中断
    uint8_t *processed = buf[active_buf];
    // 使 Cache 失效,准备 CPU 读取
    SCB_InvalidateDCache_by_Addr((uint32_t*)processed, BUF_SIZE);
    // 处理数据...
    
    // 切换缓冲区
    active_buf ^= 1;
    // 配置 DMA 使用下一个缓冲区(略)
}

优点:吞吐量高,适合实时性要求高的场景。 缺点:内存占用翻倍,代码复杂度增加。

5. 综合建议与注意事项

  • 优先使用 MPU:对于低速外设(如 UART、I2C)的缓冲区,直接配置非 Cacheable,省心省力。
  • 对齐至关重要:任何需要 Cache 操作的缓冲区,务必 32 字节对齐,否则行为未定义。
  • 避免混合使用:同一缓冲区不要既用 MPU 配置又用软件维护,容易混乱。
  • 检查编译优化:确保缓冲区变量不被编译器优化掉,使用 volatile__attribute__((used))
  • 调试技巧:若出现随机数据错误,先怀疑 Cache 一致性问题,用逻辑分析仪或断点观察内存值。

结语

STM32H7 的 Cache 一致性是高性能开发的必修课。通过 MPU 配置、软件维护或双缓冲策略,你可以根据应用场景灵活选择。记住,没有银弹,只有最适合的。希望本文能帮你避开那些“玄学” Bug,让代码跑得又快又稳。