# STM32H7 L1 Cache 未使能导致 DMA 数据不一致:从踩坑到彻底修复 ## 一、问题现象:数据像“幽灵”一样丢失 最近在调试一个基于 STM32H743 的数据采集系统,使用 SPI DMA 接收传感器数据。代码逻辑很简单:DMA 将数据搬运到内存缓冲区,主循环解析。但发现一个诡异现象:**DMA 中断标志已置位,缓冲区却偶尔出现旧数据或全零**。更奇怪的是,如果关闭优化或加延时,问题就消失。 排查了 DMA 配置、SPI 时序、中断优先级,均无异常。最后怀疑到 L1 Cache 头上——因为 STM32H7 的 Cortex-M7 内核带有 L1 Cache,而默认复位后 Cache 是关闭的,但我在初始化代码中使能了它(为了提升性能)。 ## 二、原理剖析:Cache 与 DMA 的“信息孤岛” ### 2.1 什么是 L1 Cache? Cortex-M7 内置了 I-Cache(指令缓存)和 D-Cache(数据缓存),用于加速 CPU 对内存的访问。当 CPU 读取数据时,会先查 Cache,命中则直接返回,未命中则从 RAM 加载到 Cache。写操作则可能采用 **write-back** 策略:数据先写入 Cache,标记为脏,延迟写回 RAM。 ### 2.2 DMA 的“旁路”特性 DMA 控制器直接访问 RAM,**不经过 Cache**。这意味着: - **DMA 写入 RAM**:CPU 的 Cache 中可能保留着旧数据,CPU 读到的还是旧值(Cache 未失效)。 - **DMA 读取 RAM**:如果 CPU 刚写过数据但仍在 Cache 中(write-back),DMA 读到的可能是旧值(Cache 未写回)。 这就是数据不一致的根源。 ### 2.3 为什么默认关闭? STM32H7 复位后 L1 Cache 默认关闭,这保证了与旧代码的兼容性。但很多开发者为了性能主动使能,却忽略了与 DMA 的协同,导致上述问题。 ## 三、修复方案:三种策略对比 ### 方案一:直接关闭 D-Cache(简单粗暴) 适用于对性能要求不高的场景。 ```c // 关闭 D-Cache(在 SystemInit 后调用) SCB_DisableDCache(); ``` ### 方案二:配置 MPU 将 DMA 缓冲区设置为非缓存区域(推荐) 使用 MPU 将 DMA 使用的内存区域配置为 **normal memory, non-cacheable**,这样 CPU 访问该区域时直接读写 RAM,与 DMA 保持一致。 ```c void MPU_Config(void) { MPU_Region_InitTypeDef MPU_InitStruct = {0}; // 禁用 MPU 进行配置 HAL_MPU_Disable(); // 配置 DMA 缓冲区区域(例如 0x30000000,大小 64KB) MPU_InitStruct.Enable = MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress = 0x30000000; MPU_InitStruct.Size = MPU_REGION_SIZE_64KB; MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS; MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE; MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE; MPU_InitStruct.IsShareable = MPU_ACCESS_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); // 使能 MPU HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT); } ``` 注意:MPU 配置必须在使能 Cache 之前完成,且要确保缓冲区地址对齐到区域大小。 ### 方案三:软件维护 Cache(灵活但需谨慎) 在 DMA 操作前后手动维护 Cache。 - **DMA 写入 RAM 后**,要使 CPU 的 Cache 失效,强制从 RAM 重新读取: ```c SCB_InvalidateDCache_by_Addr((uint32_t*)buffer, size); ``` - **DMA 读取 RAM 前**,要将 CPU 的 Cache 写回,确保 DMA 看到最新数据: ```c SCB_CleanDCache_by_Addr((uint32_t*)buffer, size); ``` 注意:地址必须 32 字节对齐,大小也需对齐。 ## 四、完整代码示例(基于 HAL 库) 以下是一个典型的 SPI DMA 接收流程,采用方案二(MPU 非缓存区)进行修复。 ```c // main.c 片段 // 定义 DMA 缓冲区(放在非缓存区域,例如 AXI SRAM 0x30000000) #if defined(__ICCARM__) #pragma location=0x30000000 uint8_t dma_rx_buf[1024]; #elif defined(__GNUC__) uint8_t dma_rx_buf[1024] __attribute__((section(".non_cacheable"))); #endif void SystemClock_Config(void); void MPU_Config(void); int main(void) { HAL_Init(); SystemClock_Config(); // 先配置 MPU,再使能 Cache MPU_Config(); SCB_EnableDCache(); SCB_EnableICache(); // SPI 和 DMA 初始化... MX_SPI1_Init(); MX_DMA_Init(); // 启动 DMA 接收 HAL_SPI_Receive_DMA(&hspi1, dma_rx_buf, sizeof(dma_rx_buf)); while (1) { // 主循环解析数据(此时 CPU 读取 dma_rx_buf 直接访问 RAM,与 DMA 一致) } } // 链接脚本中需定义 .non_cacheable 段,并映射到 0x30000000 区域 ``` ## 五、调试与验证技巧 1. **使用断点观察**:在 DMA 完成中断中设置断点,查看缓冲区内容。如果与预期不符,优先怀疑 Cache。 2. **对比测试**:临时关闭 Cache,看问题是否消失。 3. **查看 MPU 配置**:使用调试器查看 MPU->RBAR 和 MPU->RASR 寄存器,确认区域属性。 4. **注意内存区域**:STM32H7 的 RAM 分为 DTCM(0x20000000)、AXI SRAM(0x24000000)和 SRAM1-4(0x30000000 等)。DTCM 不支持 DMA,而 AXI SRAM 和 SRAM1-4 支持。建议将 DMA 缓冲区放在 AXI SRAM 或 SRAM1-4,并配置 MPU。 ## 六、注意事项与总结 - **MPU 配置必须在使能 Cache 之前**,否则不生效。 - **缓冲区地址对齐**:MPU 区域大小必须与缓冲区大小匹配,且基地址对齐到区域大小(例如 64KB 区域要求地址低 16 位为 0)。 - **软件维护 Cache 时**,地址和长度必须 32 字节对齐,否则会导致硬件错误。 - **不要混合使用方案二和方案三**,否则可能造成二次不一致。 - **性能权衡**:非缓存区域会降低 CPU 访问速度,但 DMA 缓冲区通常访问频率不高,影响可忽略。 总之,STM32H7 的 L1 Cache 是一把双刃剑。在涉及 DMA 的场景中,务必明确 Cache 策略。推荐使用 MPU 将 DMA 缓冲区配置为非缓存区域,既保证一致性,又保留 Cache 带来的性能提升。希望本文能帮你少踩一个经典的嵌入式大坑。