# 引言 在 STM32F4 系列高性能 MCU 上,D-Cache 能显著提升 CPU 访问外部 RAM 的速度,但同时也引入了缓存一致性问题。当 DMA 控制器(如 DMA2D、SDMMC、以太网 MAC)与 CPU 共享内存时,若双方对同一地址的读写顺序不当,DMA 可能读取到 Cache 中的脏数据,或者 CPU 读取到 DMA 写入但尚未刷新到内存的旧数据。对于 DMA 描述符(如链表节点),这种问题尤为致命,因为描述符一旦被 DMA 错误解析,整个传输链将崩溃。 # 问题根源:D-Cache 与 DMA 的“信息孤岛” D-Cache 是 CPU 与主存之间的高速缓存,以 32 字节(或更大)的行(Line)为单位。当 CPU 写内存时,数据可能只写入 Cache 行,并未同步到主存(写回策略)。而 DMA 直接访问主存,不经过 Cache。因此,若 CPU 修改了描述符但 Cache 未回写,DMA 会读取到旧值;反之,若 DMA 更新了描述符(如传输完成标志),但 Cache 中仍保留旧数据,CPU 读取时可能看不到更新。 # 三种正确姿势 ## 姿势一:全局 Cache 清理与失效(简单粗暴) ### 原理 在每次 DMA 操作前,调用 `SCB_CleanDCache()` 将整个 D-Cache 的脏数据回写到主存;在 DMA 操作完成后,调用 `SCB_InvalidateDCache()` 使整个 Cache 失效,强制 CPU 从主存重新读取。此方法简单可靠,但开销较大,适合低频 DMA 场景。 ### 配置步骤 1. 使能 D-Cache(在 `SystemInit()` 后调用 `SCB_EnableDCache()`)。 2. 在 DMA 启动前,调用 `SCB_CleanDCache()`。 3. 在 DMA 完成中断中,调用 `SCB_InvalidateDCache()`。 ### 代码示例 ```c // 使能 D-Cache SCB_EnableDCache(); // DMA 传输前 SCB_CleanDCache(); HAL_DMA_Start_IT(&hdma, (uint32_t)src, (uint32_t)dst, len); // DMA 完成中断回调 void DMA_IRQHandler(void) { SCB_InvalidateDCache(); // 处理数据 } ``` ### 注意事项 - 全局清理/失效会带来性能损失,尤其在频繁 DMA 时。 - 若 DMA 与 CPU 并发访问,需确保在 Clean 之后、DMA 启动之前,CPU 不再修改相关内存。 ## 姿势二:MPU 配置描述符区域为非缓存(精准隔离) ### 原理 利用 MPU(Memory Protection Unit)将 DMA 描述符所在的 RAM 区域设置为“非缓存”(Normal memory, Non-cacheable)。这样 CPU 访问该区域时直接读写主存,DMA 与 CPU 看到的永远一致。此方法性能最佳,但需合理规划内存布局。 ### 配置步骤 1. 在链接脚本中,为描述符分配独立区域(如 `.dma_desc` 段)。 2. 初始化 MPU,配置该区域属性为 Non-cacheable。 3. 使能 MPU。 ### 代码示例 ```c // 链接脚本中定义段 __attribute__((section(".dma_desc"))) DMA_Desc_TypeDef desc[8]; // MPU 配置 void MPU_Config(void) { MPU_Region_InitTypeDef MPU_InitStruct; HAL_MPU_Disable(); MPU_InitStruct.Enable = MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress = (uint32_t)&desc; MPU_InitStruct.Size = MPU_REGION_SIZE_256B; // 根据实际大小 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_SHAREABLE; MPU_InitStruct.Number = MPU_REGION_NUMBER0; MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0; MPU_InitStruct.SubRegionDisable = 0; MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE; HAL_MPU_ConfigRegion(&MPU_InitStruct); HAL_MPU_Enable(MPU_CONTROL_HRD_MEM); } // 初始化时调用 MPU_Config(); ``` ### 注意事项 - 描述符区域必须与普通数据区域分开,避免误配置。 - MPU 区域大小需为 2 的幂次,且对齐。 - 若使用 FreeRTOS,需确保 MPU 配置在任务调度前完成。 ## 姿势三:精确的 Cache 维护指令(细粒度控制) ### 原理 使用 `SCB_CleanDCache_by_Addr()` 和 `SCB_InvalidateDCache_by_Addr()` 仅对描述符地址范围进行操作。在 CPU 修改描述符后,Clean 该地址范围;在 DMA 更新描述符后,Invalidate 该范围。此方法开销最小,但需确保地址对齐到 32 字节。 ### 配置步骤 1. 定义描述符数组,并确保其起始地址 32 字节对齐(使用 `__attribute__((aligned(32)))`)。 2. 在每次修改描述符后,调用 Clean 函数。 3. 在 DMA 完成中断中,调用 Invalidate 函数。 ### 代码示例 ```c __attribute__((aligned(32))) DMA_Desc_TypeDef desc[8]; // CPU 修改描述符后 SCB_CleanDCache_by_Addr((uint32_t*)&desc[0], sizeof(DMA_Desc_TypeDef)); HAL_DMA_Start_IT(&hdma, (uint32_t)&desc[0], ...); // DMA 完成中断中 SCB_InvalidateDCache_by_Addr((uint32_t*)&desc[0], sizeof(DMA_Desc_TypeDef)); ``` ### 注意事项 - 地址和长度必须对齐到 32 字节,否则行为未定义。 - 若描述符大小不是 32 的倍数,需向上取整。 - 此方法适用于描述符数量少且访问频繁的场景。 # 总结与选型建议 | 方法 | 优点 | 缺点 | 适用场景 | |------|------|------|----------| | 全局 Clean/Invalidate | 简单可靠 | 性能开销大 | 低频 DMA,如按键扫描 | | MPU 非缓存区域 | 性能最佳 | 需规划内存布局 | 高频 DMA,如以太网、摄像头 | | 精确地址维护 | 开销最小 | 需对齐,代码稍复杂 | 中等频率,描述符固定 | 在实际项目中,推荐优先考虑 MPU 方案,因为它从根源上消除了不一致性,且对性能影响最小。若内存紧张,可采用精确地址维护。全局清理仅作为调试或原型验证手段。 # 结语 D-Cache 是一把双刃剑,用得好能大幅提升性能,用不好则会导致难以排查的随机故障。掌握上述三种姿势,你就能在 STM32F4 上放心地让 DMA 与 CPU 协同工作。记住,缓存一致性不是“可选优化”,而是 DMA 正确性的基石。希望本文能帮你少踩几个坑,让代码跑得更稳。