# STM32F4 D-Cache与DMA描述符缓存一致性维护的三种正确姿势 ## 1. 问题根源:D-Cache与DMA的“盲区” STM32F4系列(如STM32F407、STM32F429)内置了D-Cache(数据缓存),用于加速CPU对内存的访问。然而,DMA控制器直接访问物理内存(SRAM或SDRAM),**不经过D-Cache**。这导致两个经典问题: - **CPU写,DMA读**:CPU更新描述符后,数据可能还滞留在D-Cache中,DMA从内存读取到的是旧数据。 - **DMA写,CPU读**:DMA更新描述符后,内存中的数据是最新的,但CPU读取时可能命中D-Cache中的旧缓存行,导致读到过期数据。 对于DMA描述符(如以太网DMA描述符、SDMMC描述符),这种不一致会直接导致传输错误或系统挂起。 ## 2. 方案一:全Cache无效化(简单粗暴,适合低频操作) ### 原理 在每次DMA传输前,将整个D-Cache无效化(Invalidate),强制CPU从内存重新读取。在传输完成后,同样执行无效化,确保DMA写入的数据对CPU可见。 ### 配置步骤 1. 启用D-Cache(在启动代码或主函数中)。 2. 在DMA操作前后调用`SCB_InvalidateDCache()`。 ### 代码示例 ```c // 启用D-Cache SCB_EnableDCache(); // 以太网DMA描述符示例 ETH_DMADescTypeDef *pDesc = &tx_desc[0]; // 准备描述符(CPU写) pDesc->tdes0 = ...; // 设置控制字 pDesc->tdes1 = ...; // 设置缓冲区地址 // 关键:使整个D-Cache无效化,确保DMA看到最新描述符 SCB_InvalidateDCache(); // 启动DMA传输 HAL_ETH_Transmit(&heth, pDesc, 1); // 等待传输完成,再次无效化,使CPU看到DMA更新的状态位 while (HAL_ETH_GetTxDataFreeEntries(&heth) == 0); SCB_InvalidateDCache(); ``` ### 注意事项 - 性能损失大:每次操作都冲刷整个Cache,不适合高频DMA场景。 - 简单可靠:适用于描述符数量少、操作频率低的场景(如低速串口)。 ## 3. 方案二:MPU配置描述符区域为非缓存(推荐,性能与安全兼顾) ### 原理 通过MPU(内存保护单元)将DMA描述符所在的SRAM区域设置为**非缓存(Non-cacheable)**。这样CPU访问该区域时直接读写物理内存,绕过D-Cache,从根本上避免一致性问题。 ### 配置步骤 1. 确定描述符区域地址和大小(需对齐到32字节)。 2. 配置MPU区域,设置属性为`MPU_REGION_NO_CACHE`。 3. 使能MPU。 ### 代码示例 ```c // 假设描述符位于SRAM1,起始地址0x20000000,大小4KB #define DESC_REGION_BASE 0x20000000 #define DESC_REGION_SIZE (4 * 1024) void MPU_Config_NonCacheable(void) { MPU_Region_InitTypeDef MPU_InitStruct; // 禁用MPU进行配置 HAL_MPU_Disable(); // 配置区域0:描述符区域为非缓存 MPU_InitStruct.Enable = MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress = DESC_REGION_BASE; MPU_InitStruct.Size = MPU_REGION_SIZE_4KB; 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.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); } // 在主函数中调用 int main(void) { HAL_Init(); MPU_Config_NonCacheable(); // ... 其他初始化 } ``` ### 注意事项 - 描述符区域必须与MPU区域对齐(通常为32字节)。 - 非缓存区域的访问速度略低于缓存区域,但描述符访问频率低,影响可忽略。 - 此方案无需在每次DMA操作时手动维护缓存,代码简洁,性能稳定。 ## 4. 方案三:手动Clean/Invalidate操作(精细控制,适合高频DMA) ### 原理 仅对描述符所在的缓存行执行Clean(写回)或Invalidate(无效化)操作,而不是整个Cache。使用CMSIS提供的函数:`SCB_CleanDCache_by_Addr()`和`SCB_InvalidateDCache_by_Addr()`。 ### 配置步骤 1. 确保描述符地址按32字节对齐(缓存行大小)。 2. CPU写描述符后,调用Clean函数将数据写回内存。 3. DMA更新描述符后,调用Invalidate函数使CPU缓存失效。 ### 代码示例 ```c // 描述符结构体(假设32字节对齐) __ALIGNED(32) ETH_DMADescTypeDef tx_desc[2]; // CPU准备描述符 void Prepare_Desc(ETH_DMADescTypeDef *desc) { desc->tdes0 = ...; desc->tdes1 = ...; // 写完后Clean该描述符对应的缓存行 SCB_CleanDCache_by_Addr((uint32_t *)desc, sizeof(*desc)); } // DMA完成后,读取描述符状态 void Check_Desc(ETH_DMADescTypeDef *desc) { // 先Invalidate,确保读到最新数据 SCB_InvalidateDCache_by_Addr((uint32_t *)desc, sizeof(*desc)); if (desc->tdes0 & ETH_DMATXDESC_OWN) { // 处理完成 } } // 使用示例 Prepare_Desc(&tx_desc[0]); HAL_ETH_Transmit(&heth, &tx_desc[0], 1); // 等待中断或轮询 Check_Desc(&tx_desc[0]); ``` ### 注意事项 - 地址必须32字节对齐,否则函数可能无效或触发异常。 - 需要精确计算描述符大小,确保覆盖整个缓存行。 - 适合高频DMA场景,性能最佳,但代码复杂度稍高。 ## 5. 总结与选型建议 | 方案 | 优点 | 缺点 | 适用场景 | |------|------|------|----------| | 全Cache无效化 | 简单,无需MPU配置 | 性能差,影响实时性 | 低频DMA,调试阶段 | | MPU非缓存区域 | 性能好,代码简洁 | 需要MPU资源,区域管理 | 大多数生产项目,推荐 | | 手动Clean/Invalidate | 性能最优,精细控制 | 代码复杂,易出错 | 高频DMA,性能敏感 | **核心建议**:在STM32F4项目中,优先考虑使用MPU将描述符区域配置为非缓存,这是最平衡的方案。若DMA频率极高且描述符数量多,则采用手动Clean/Invalidate。全Cache无效化仅作为临时解决方案。 ## 6. 常见坑点 - **描述符对齐**:无论哪种方案,描述符结构体建议使用`__ALIGNED(32)`,避免跨缓存行。 - **MPU区域重叠**:配置MPU时,确保描述符区域不与代码或数据区域重叠,否则可能引发总线错误。 - **中断上下文**:在中断中操作描述符时,同样需要维护缓存一致性,不可忽略。 - **调试工具**:使用调试器查看内存时,注意关闭缓存或手动刷新,否则看到的数据可能不真实。 掌握以上三种姿势,你就能在STM32F4上从容应对D-Cache与DMA描述符的缓存一致性问题,让系统稳定运行在高速DMA场景下。