# 引言 STM32F4 系列(如 STM32F407、F429)内置了 D-Cache(数据缓存),用于加速 CPU 对片外存储器(如 SDRAM)的访问。然而,当 DMA 控制器直接访问内存时,它不经过 Cache,这会导致 CPU 与 DMA 看到的数据不一致。例如,CPU 写入数据到内存(可能暂存于 Cache),DMA 读取时可能拿到旧数据;反之,DMA 写入新数据,CPU 读取时可能命中 Cache 中的旧副本。这种不一致性在以太网、USB、SDIO 等高速外设中尤为致命。 本文针对 STM32F4 的 D-Cache(注意:F4 系列只有部分型号带 D-Cache,如 F429、F469,而 F407 没有,但原理通用),提供三种实用策略来维护一致性。 # 原理剖析 ## D-Cache 的工作机制 D-Cache 是 CPU 与主存之间的高速缓存,以缓存行(通常 32 字节)为单位。当 CPU 读数据时,若命中 Cache 则直接返回;写数据时,采用写回(Write-back)策略,即数据先写入 Cache,标记为脏,待时机成熟再写回主存。 ## DMA 的访问路径 DMA 控制器直接连接总线矩阵,绕过 CPU 和 Cache,直接读写主存。因此,DMA 看到的是主存的真实数据,而 CPU 可能看到的是 Cache 中的副本。 ## 不一致性场景 - **CPU 写,DMA 读**:CPU 写入数据到缓冲区,数据可能留在 Cache 中,尚未写回主存。DMA 读取主存时,得到的是旧数据。 - **DMA 写,CPU 读**:DMA 将外设数据写入主存,但 CPU 的 Cache 中可能已有该地址的旧副本,CPU 读取时命中 Cache,得到旧数据。 # 三种实用策略 ## 策略一:Cache 清理与失效(Clean & Invalidate) 这是最直接的方法,在 DMA 操作前后手动维护 Cache。 ### 原理 - **清理(Clean)**:将 Cache 中的脏数据写回主存,确保主存数据最新。 - **失效(Invalidate)**:使 Cache 中的行失效,下次 CPU 访问时重新从主存加载。 ### 配置步骤 1. 启用 D-Cache(若未启用)。 2. 在 DMA 发送前,调用 `SCB_CleanDCache()` 或 `SCB_CleanDCache_by_Addr()` 清理缓冲区。 3. 在 DMA 接收后,调用 `SCB_InvalidateDCache_by_Addr()` 使缓冲区失效。 ### 代码示例 ```c // 发送缓冲区,CPU 写入数据后,清理 Cache 再启动 DMA uint8_t tx_buf[128]; // 填充数据... SCB_CleanDCache_by_Addr((uint32_t*)tx_buf, sizeof(tx_buf)); HAL_UART_Transmit_DMA(&huart, tx_buf, sizeof(tx_buf)); // 接收缓冲区,DMA 完成后,使 Cache 失效再读取 uint8_t rx_buf[128]; HAL_UART_Receive_DMA(&huart, rx_buf, sizeof(rx_buf)); // 等待 DMA 完成回调... SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, sizeof(rx_buf)); // 现在可以安全读取 rx_buf ``` ### 注意事项 - 地址和长度需按 32 字节对齐,否则可能影响其他数据。 - 频繁清理/失效会降低性能,适合数据量小或低频场景。 - 使用 `HAL` 库时,部分驱动已内置处理,需确认。 ## 策略二:MPU 配置非缓存区域 通过内存保护单元(MPU)将 DMA 缓冲区所在的区域配置为不缓存(或写-through),从而避免不一致性。 ### 原理 MPU 可以设置内存区域的属性,如 `Normal, Non-cacheable`。这样 CPU 访问该区域时直接读写主存,绕过 Cache,保证一致性。 ### 配置步骤 1. 定义缓冲区区域,确保其地址和大小满足 MPU 对齐要求(通常 32 字节)。 2. 初始化 MPU,配置区域属性为 `Device` 或 `Normal, Non-cacheable`。 3. 启用 MPU。 ### 代码示例 ```c // 定义缓冲区,放在独立区域 __attribute__((section(".dma_buf"))) uint8_t dma_buf[256]; void MPU_Config(void) { MPU_Region_InitTypeDef MPU_InitStruct = {0}; // 禁用 MPU 进行配置 HAL_MPU_Disable(); // 配置区域 0:dma_buf 所在区域,非缓存 MPU_InitStruct.Enable = MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress = (uint32_t)dma_buf; 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_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); } ``` ### 注意事项 - MPU 区域大小必须是 2 的幂,且地址对齐。 - 非缓存区域访问速度较慢,但 DMA 一致性得到保证。 - 需在系统初始化时调用 `MPU_Config()`,并确保链接脚本将 `dma_buf` 放入正确段。 ## 策略三:DMA 描述符与缓冲区分区管理 将 DMA 描述符(如 DMA 控制结构)和实际数据缓冲区放置在不同的内存区域,分别管理缓存属性。 ### 原理 描述符通常由 CPU 频繁读写,而数据缓冲区由 DMA 和 CPU 交互。通过将描述符放在可缓存区域(性能高),数据缓冲区放在非缓存区域(一致性),或反之,根据实际需求优化。 ### 配置步骤 1. 分配两个内存区域:一个用于描述符(如 `__attribute__((section(".desc")))`),一个用于数据缓冲区(如 `__attribute__((section(".buf")))`)。 2. 在链接脚本中定义这些段,并设置 MPU 属性(如描述符可缓存,缓冲区非缓存)。 3. 初始化 DMA 时,描述符和缓冲区分别使用对应地址。 ### 代码示例 ```c // 链接脚本示例(.ld) // .desc : { *(.desc) } >RAM // .buf : { *(.buf) } >RAM // 定义段 __attribute__((section(".desc"))) DMA_HandleTypeDef hdma_desc; __attribute__((section(".buf"))) uint8_t data_buf[1024]; // MPU 配置:desc 区域可缓存,buf 区域非缓存 void MPU_Config_Split(void) { // 配置区域 0:desc 区域,可缓存 // 配置区域 1:buf 区域,非缓存 // 具体代码类似策略二,但需设置两个区域 } // DMA 初始化时,使用 data_buf 作为缓冲区,hdma_desc 作为描述符 ``` ### 注意事项 - 需要精心设计链接脚本,确保段地址符合 MPU 对齐要求。 - 描述符和缓冲区的大小需合理规划,避免浪费内存。 - 此策略适用于复杂系统,如多通道 DMA 或网络协议栈。 # 总结 在 STM32F4 系列使用 D-Cache 时,维护 DMA 一致性是确保系统稳定的关键。三种策略各有优劣: - **策略一** 简单直接,但性能损失较大,适合低频小数据。 - **策略二** 配置一次,长期有效,但牺牲了缓存性能,适合缓冲区固定且频繁 DMA 的场景。 - **策略三** 最灵活,可针对不同数据特性优化,但实现复杂,适合大型项目。 开发者应根据实际需求选择合适策略,并在设计初期就考虑内存布局和缓存配置。希望本文能帮助你避开 D-Cache 的坑,写出更健壮的嵌入式代码。