# 引言 在 STM32F4 系列(如 STM32F407)高性能 MCU 中,D-Cache 的引入极大提升了 CPU 访问外部存储器(如 SDRAM)的速度,但同时也带来了与 DMA 协同工作时的缓存一致性问题。当 DMA 描述符(如 DMA2D、以太网 DMA 描述符)由 CPU 在内存中创建或修改后,DMA 外设可能读取到 D-Cache 中的陈旧数据,导致数据传输错误。本文将深入分析问题根源,并给出三种实用策略,帮助开发者确保数据一致性。 # 问题根源 STM32F4 的 D-Cache 是写回(write-back)模式,CPU 写操作先更新 Cache,而不会立即写回主存。当 DMA 外设(如 DMA2D、以太网 MAC)通过 AHB 总线直接访问内存时,它绕过 Cache,直接读取主存。如果描述符数据仍停留在 Cache 中,DMA 将读到旧值。同理,DMA 写入内存的数据也可能被 CPU 从 Cache 中读到旧值。 # 策略一:Cache 清理与无效化(最直接) ## 原理 在 CPU 修改描述符后、启动 DMA 前,调用 `SCB_CleanDCache()` 将 Cache 中对应地址的数据写回主存。在 DMA 完成传输后、CPU 读取描述符前,调用 `SCB_InvalidateDCache()` 使 Cache 中对应行失效,强制从主存重新加载。 ## 配置步骤 1. 确保在 `system_stm32f4xx.c` 中启用了 D-Cache(`SCB_EnableDCache()`)。 2. 在修改描述符后,调用清理函数。 3. 在 DMA 传输完成后,调用无效化函数。 ## 代码示例 ```c // 假设描述符结构体 __attribute__((aligned(32))) DMA_Descriptor_t desc; // CPU 修改描述符 desc.ctrl = 0x1234; desc.addr = (uint32_t)buffer; // 清理 D-Cache,确保写回主存 SCB_CleanDCache_by_Addr((uint32_t*)&desc, sizeof(desc)); // 启动 DMA(以 DMA2D 为例) DMA2D->FGMAR = (uint32_t)&desc; DMA2D->CR |= DMA2D_CR_START; // 等待传输完成 while (!(DMA2D->ISR & DMA2D_ISR_TCIF)) ; // 无效化 D-Cache,确保读取最新数据 SCB_InvalidateDCache_by_Addr((uint32_t*)&desc, sizeof(desc)); // 现在可以安全读取 desc 中的状态 ``` ## 注意事项 - 清理/无效化操作有性能开销,频繁调用会影响效率。 - 地址必须 32 字节对齐(Cache line 大小),否则可能无效。 - 建议使用 `by_Addr` 变体,避免全 Cache 操作。 # 策略二:使用非缓存内存区域(推荐) ## 原理 将 DMA 描述符放置在 MPU 配置为非缓存(Non-cacheable)的内存区域中。这样 CPU 访问该区域时直接读写主存,无需 Cache 维护,从根源上避免一致性问题。 ## 配置步骤 1. 在链接脚本中定义一段独立内存区域,如 `DMA_DESC`。 2. 使用 MPU 将该区域配置为 `Device` 或 `Strongly-ordered` 属性(非缓存)。 3. 将描述符变量放置在该区域。 ## 代码示例 ```c // 链接脚本(.ld)中添加: // .dma_desc (NOLOAD) : { *(.dma_desc) } > RAM_DMA // 定义描述符变量,并指定段属性 __attribute__((section(".dma_desc"), aligned(32))) DMA_Descriptor_t desc; // MPU 配置(初始化时调用) void MPU_Config(void) { MPU_Region_InitTypeDef MPU_InitStruct; MPU_InitStruct.Enable = MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress = 0x20000000; // 假设 RAM_DMA 起始地址 MPU_InitStruct.Size = MPU_REGION_SIZE_4KB; MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS; MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL_0; // 非缓存 MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE; MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE; MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE; MPU_InitStruct.Number = MPU_REGION_NUMBER0; MPU_Init(&MPU_InitStruct); MPU_Enable(MPU_PRIVILEGED_DEFAULT); } // 使用描述符时无需任何 Cache 操作 ``` ## 注意事项 - 需要合理规划内存布局,确保区域大小足够。 - 非缓存区域访问速度较慢,但描述符访问频率低,影响可忽略。 - 确保 MPU 配置在启用 D-Cache 之前完成。 # 策略三:描述符对齐与双缓冲设计(高级) ## 原理 利用 Cache line 对齐特性,将描述符设计为 32 字节对齐,并采用双缓冲机制。CPU 在修改描述符 A 时,DMA 正在使用描述符 B,通过交替使用避免同时访问同一 Cache line。同时,在切换时进行必要的 Cache 操作,但频率降低。 ## 配置步骤 1. 定义两个描述符,均 32 字节对齐。 2. 使用一个标志位指示当前活跃描述符。 3. 在切换前清理旧描述符的 Cache,切换后无效化新描述符的 Cache。 ## 代码示例 ```c #define DESC_NUM 2 __attribute__((aligned(32))) DMA_Descriptor_t descs[DESC_NUM]; volatile uint8_t active_desc = 0; void DMA_Start_Transfer(void) { uint8_t next = active_desc ^ 1; // 修改下一个描述符 descs[next].ctrl = ...; descs[next].addr = ...; // 清理下一个描述符的 Cache(因为 DMA 将读取) SCB_CleanDCache_by_Addr((uint32_t*)&descs[next], sizeof(DMA_Descriptor_t)); // 启动 DMA 使用 next 描述符 DMA2D->FGMAR = (uint32_t)&descs[next]; DMA2D->CR |= DMA2D_CR_START; // 等待完成,然后无效化当前描述符(因为 CPU 可能读取状态) while (!(DMA2D->ISR & DMA2D_ISR_TCIF)) ; SCB_InvalidateDCache_by_Addr((uint32_t*)&descs[active_desc], sizeof(DMA_Descriptor_t)); // 切换活跃描述符 active_desc = next; } ``` ## 注意事项 - 描述符必须独立位于不同 Cache line,避免伪共享。 - 切换逻辑需保证 DMA 不会同时访问两个描述符。 - 适用于高速传输场景,可减少 Cache 操作次数。 # 总结 三种策略各有优劣:策略一简单但开销大;策略二推荐用于新设计,彻底避免问题;策略三适合性能敏感场景。实际开发中,应根据系统对实时性和复杂度的要求选择。无论哪种方案,理解 D-Cache 工作原理是前提。建议在项目初期就规划好内存布局,避免后期调试的困扰。