# STM32F4 使用 D-Cache 时 DMA 描述符缓存一致性维护的三种实用策略 ## 一、问题根源:D-Cache 与 DMA 的“视线盲区” STM32F4 系列(如 STM32F407、STM32F429)内置了 4KB 或 8KB 的 D-Cache,用于加速 CPU 对 SRAM 的访问。然而,DMA 控制器(如 DMA2D、SDIO、以太网 MAC)在传输数据时**直接访问物理内存,不经过 D-Cache**。这就导致两个经典问题: - **CPU 写数据到 SRAM 后,数据可能仍留在 Cache 中,尚未写回物理内存**。DMA 读取时拿到的是旧数据。 - **DMA 将新数据写入 SRAM 后,物理内存已更新,但 Cache 中仍保留旧副本**。CPU 读取时拿到的是过期数据。 对于 DMA 描述符(如 DMA2D 的控制结构体、SDIO 的 CID/DATA 寄存器映射),这种不一致会直接导致传输状态判断错误或数据错乱。 ## 二、策略一:全缓存刷新(简单粗暴,适合低频传输) ### 原理 在每次 DMA 操作前后,调用 `SCB_CleanDCache()` 或 `SCB_CleanInvalidateDCache()` 强制将整个 D-Cache 写回物理内存,并失效所有行。此方法不依赖任何硬件配置,代码量最小。 ### 配置步骤 1. 在启动文件或 `system_stm32f4xx.c` 中使能 D-Cache: ```c SCB_EnableDCache(); ``` 2. 在 DMA 传输前,清理 Cache 确保描述符和数据已写回: ```c SCB_CleanDCache(); ``` 3. 在 DMA 传输完成后,失效 Cache 使 CPU 重新从物理内存读取: ```c SCB_InvalidateDCache(); ``` ### 完整代码示例(以 DMA2D 为例) ```c // 假设 DMA2D 描述符位于 SRAM1,地址 0x20000000 DMA2D_HandleTypeDef hdma2d; void DMA2D_TransferWithFullCacheFlush(void) { // 1. 清理整个 D-Cache,确保描述符和源数据写回 SRAM SCB_CleanDCache(); // 2. 启动 DMA2D 传输 HAL_DMA2D_Start(&hdma2d, srcAddr, dstAddr, width, height); // 3. 等待传输完成(轮询或中断) HAL_DMA2D_PollForTransfer(&hdma2d, HAL_MAX_DELAY); // 4. 失效整个 D-Cache,使 CPU 看到 DMA 写入的新数据 SCB_InvalidateDCache(); // 此时读取目标缓冲区数据是安全的 } ``` ### 注意事项 - **性能开销大**:每次传输都刷新整个 Cache,对于高频 DMA(如摄像头采集)会严重拖慢系统。 - **实时性影响**:在中断中执行全缓存刷新可能导致中断延迟增加。 - **适用场景**:低频、大数据块传输,或系统对性能不敏感。 ## 三、策略二:MPU 配置描述符区域为非缓存(推荐) ### 原理 利用 Cortex-M4 的 MPU(内存保护单元),将包含 DMA 描述符的 SRAM 区域配置为 **非缓存(Non-cacheable)** 属性。这样 CPU 访问该区域时直接读写物理内存,绕过 D-Cache,从根本上避免一致性问题。 ### 配置步骤 1. 在系统初始化时,配置 MPU 区域: ```c void MPU_Config_NonCacheable(void) { MPU_Region_InitTypeDef MPU_InitStruct; // 禁用 MPU 进行配置 HAL_MPU_Disable(); // 配置区域:基地址 0x20000000,大小 4KB(覆盖描述符区域) MPU_InitStruct.Enable = MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress = 0x20000000; 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.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); } ``` 2. 在 `main` 函数初始化中调用 `MPU_Config_NonCacheable()`。 3. 将 DMA 描述符定义在该区域(例如通过链接脚本或直接指定地址)。 ### 完整代码示例(使用属性指定地址) ```c // 将描述符放在非缓存区域(假设 0x20000000 起始的 4KB) __attribute__((section(".non_cacheable"))) DMA2D_DescTypeDef dma2d_desc; // 在链接脚本中定义 .non_cacheable 段,并映射到 0x20000000 void DMA2D_TransferWithMPU(void) { // 无需手动刷新 Cache,直接操作描述符 dma2d_desc.control = ...; dma2d_desc.srcAddr = ...; HAL_DMA2D_Start(&hdma2d, ...); HAL_DMA2D_PollForTransfer(&hdma2d, HAL_MAX_DELAY); // 读取状态,数据一致 if (dma2d_desc.status == DONE) { ... } } ``` ### 注意事项 - **区域大小需对齐**:MPU 区域大小必须是 2 的幂次,且基地址对齐。 - **性能影响**:非缓存区域访问速度较慢,但仅影响描述符,不影响大数据缓冲区。 - **适用场景**:描述符频繁访问,且对实时性要求高,如网络协议栈、USB 等。 ## 四、策略三:DMA 中断中精准维护(精细控制) ### 原理 在 DMA 传输完成中断中,仅对描述符和关键数据缓冲区执行 **Clean 和 Invalidate** 操作,而不是全缓存刷新。使用 `SCB_CleanDCache_by_Addr()` 和 `SCB_InvalidateDCache_by_Addr()` 按地址范围操作,粒度更细,开销更小。 ### 配置步骤 1. 使能 D-Cache。 2. 在 DMA 传输前,清理描述符区域(确保描述符写回): ```c SCB_CleanDCache_by_Addr((uint32_t*)&dma2d_desc, sizeof(dma2d_desc)); ``` 3. 在 DMA 传输完成中断中,失效描述符和数据缓冲区: ```c void DMA2D_IRQHandler(void) { // 检查中断标志 if (__HAL_DMA2D_GET_FLAG(&hdma2d, DMA2D_FLAG_TC)) { // 失效描述符区域 SCB_InvalidateDCache_by_Addr((uint32_t*)&dma2d_desc, sizeof(dma2d_desc)); // 失效数据缓冲区(假设长度为 len) SCB_InvalidateDCache_by_Addr((uint32_t*)dataBuffer, len); // 清除标志 __HAL_DMA2D_CLEAR_FLAG(&hdma2d, DMA2D_FLAG_TC); // 通知任务处理 } } ``` ### 完整代码示例(结合 HAL 回调) ```c // 在 HAL 库中,DMA2D 传输完成回调 void HAL_DMA2D_TransferComplete_Callback(DMA2D_HandleTypeDef *hdma2d) { // 精准失效描述符和数据缓冲区 SCB_InvalidateDCache_by_Addr((uint32_t*)&dma2d_desc, sizeof(dma2d_desc)); SCB_InvalidateDCache_by_Addr((uint32_t*)dataBuffer, len); // 处理数据... } void StartTransfer(void) { // 清理描述符区域,确保 DMA 看到最新配置 SCB_CleanDCache_by_Addr((uint32_t*)&dma2d_desc, sizeof(dma2d_desc)); HAL_DMA2D_Start_IT(&hdma2d, src, dst, w, h); } ``` ### 注意事项 - **地址对齐**:`SCB_CleanDCache_by_Addr` 要求地址按 32 字节对齐,否则可能无效。 - **长度处理**:长度最好为 32 的倍数,否则需手动向上取整。 - **适用场景**:高频 DMA 传输,且描述符和数据缓冲区大小固定,适合网络、音频等流式处理。 ## 五、总结与选型建议 | 策略 | 优点 | 缺点 | 适用场景 | |------|------|------|----------| | 全缓存刷新 | 实现简单,无需额外配置 | 性能开销大,影响实时性 | 低频、大块传输,调试阶段 | | MPU 非缓存区域 | 无一致性风险,性能稳定 | 占用 MPU 区域,访问速度稍慢 | 描述符频繁访问,高实时性系统 | | 中断精准维护 | 开销最小,灵活 | 需注意对齐和长度,代码稍复杂 | 高频传输,流式数据处理 | 在实际项目中,建议**优先考虑 MPU 策略**,因为它从硬件层面隔离了问题,代码逻辑最清晰。若 MPU 区域不足或需要动态管理,则采用中断精准维护。全缓存刷新仅作为临时方案或调试辅助。 掌握这三种策略,你就能在 STM32F4 上放心使用 D-Cache 和 DMA 协同工作,避免那些令人头疼的“随机性”数据错误。