# 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,避免手动维护。 ```c // 启用 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 字节取整。 ```c // 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 接收 以下代码演示了正确流程: ```c #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 维护函数的使用,是每个嵌入式开发者必备的技能。建议在项目初期就规划好内存区域的属性,避免后期踩坑。