STM32H7 的 L1 Cache 与 DMA 一致性维护:从踩坑到正确配置

一、为什么 STM32H7 需要 L1 Cache?

STM32H7 搭载 Cortex-M7 内核,主频高达 480MHz,而外部存储器(如 SDRAM、QSPI Flash)的访问速度远低于内核。为了弥补速度差距,Cortex-M7 内置了 L1 Cache(I-Cache 和 D-Cache),分别用于缓存指令和数据。D-Cache 的引入显著提升了数据访问效率,但也带来了新的问题:当 DMA 直接访问内存时,CPU 和 DMA 看到的数据可能不一致,这就是 Cache 一致性问题。

二、Cache 与 DMA 冲突的根源

2.1 Cache 的工作原理

D-Cache 以行为单位(通常 32 字节)缓存内存数据。CPU 读取数据时,若命中 Cache 则直接返回,否则从主存加载到 Cache;写入时,若开启写回(Write-back)模式,数据先写入 Cache,标记为脏(Dirty),延迟写回主存。

2.2 冲突场景

  • DMA 写入,CPU 读取:DMA 将数据从外设搬运到内存,但 CPU 读取时可能命中 Cache 中的旧数据(Cache 未失效),导致读到过期数据。
  • CPU 写入,DMA 读取:CPU 修改数据后,数据可能仍在 Cache 中未写回主存,DMA 直接读取主存会得到旧数据。

2.3 典型踩坑案例

某项目中,使用 SPI DMA 接收传感器数据到 SDRAM 缓冲区,CPU 轮询 DMA 完成后读取缓冲区。结果发现数据时而正确时而错误,调试发现是 D-Cache 未失效导致 CPU 读到了缓存中的旧值。

三、正确配置:基于 HAL 库的解决方案

3.1 硬件与软件环境

  • 硬件:STM32H743 开发板,外部 SDRAM,SPI 外设
  • 软件:STM32CubeIDE,HAL 库

3.2 启用 Cache 并配置 MPU

首先,在 main.c 中启用 I-Cache 和 D-Cache,并配置 MPU 将 SDRAM 区域设置为非 Cacheable 或使用 Write-through 策略。推荐将 DMA 缓冲区所在内存区域配置为 Non-cacheable,避免手动维护。

// 启用 Cache
SCB_EnableICache();
SCB_EnableDCache();

// 配置 MPU,将 SDRAM 区域设为 Non-cacheable
MPU_Region_InitTypeDef MPU_InitStruct = {0};
HAL_MPU_Disable();
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0xC0000000; // SDRAM 起始地址
MPU_InitStruct.Size = MPU_REGION_SIZE_8MB;
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 = 0;
MPU_InitStruct.DisableExec = MPU_REGION_ENABLE_NO_EXEC;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_CONTROL_HRD_MEM);

3.3 使用 DMA 时的 Cache 维护函数

如果无法将缓冲区设为 Non-cacheable,则需要在 DMA 操作前后手动维护 Cache。HAL 库提供了两个关键函数:

  • SCB_InvalidateDCache_by_Addr:使 Cache 行失效,用于 DMA 写入后,CPU 读取前。
  • SCB_CleanDCache_by_Addr:将 Cache 数据写回主存,用于 CPU 写入后,DMA 读取前。

注意:操作地址必须 32 字节对齐,长度按 32 字节取整。

// DMA 接收完成后,使 Cache 失效
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buffer, sizeof(rx_buffer));

// CPU 写入数据后,清 Cache 写回
SCB_CleanDCache_by_Addr((uint32_t*)tx_buffer, sizeof(tx_buffer));

3.4 完整示例:SPI DMA 接收

以下代码演示了正确流程:

#define BUFFER_SIZE 1024
uint8_t rx_buffer[BUFFER_SIZE] __attribute__((aligned(32)));

// 启动 DMA 接收
HAL_SPI_Receive_DMA(&hspi, rx_buffer, BUFFER_SIZE);

// 等待 DMA 完成(中断或轮询)
while (HAL_SPI_GetState(&hspi) != HAL_SPI_STATE_READY);

// 使 Cache 失效,确保 CPU 读取最新数据
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buffer, BUFFER_SIZE);

// 现在可以安全读取 rx_buffer

四、注意事项与最佳实践

  • 对齐与长度:Cache 维护函数的地址和长度必须 32 字节对齐,否则可能遗漏部分数据。建议使用 __attribute__((aligned(32))) 声明缓冲区。
  • 避免频繁维护:频繁调用 Cache 维护函数会降低性能,建议将 DMA 缓冲区独立划分,并配置为 Non-cacheable。
  • MPU 配置优先级:MPU 配置必须在启用 Cache 之前完成,否则可能不生效。
  • 多缓冲区场景:若使用双缓冲,需分别维护每个缓冲区。
  • 调试技巧:出现数据异常时,可暂时禁用 D-Cache 验证是否为一致性问题。

五、总结

STM32H7 的 L1 Cache 是一把双刃剑,用得好能大幅提升性能,用不好则带来诡异的数据错误。理解 Cache 一致性问题的根源,掌握 MPU 配置和 Cache 维护函数的使用,是每个嵌入式开发者必备的技能。建议在项目初期就规划好内存区域的属性,避免后期踩坑。