# STM32F4 168MHz 下 DMA+双缓冲串口收发:Cache 一致性维护的三种实用策略 ## 1. 问题根源:Cache 与 DMA 的“各自为政” STM32F4 系列(如 STM32F407)在 168MHz 主频下,CPU 运行速度远高于外部 SRAM 或 SDRAM。为提升性能,Cortex-M4 内核集成了 Cache(通常为 4KB 或 8KB,分为 I-Cache 和 D-Cache)。当 CPU 访问内存时,数据会先被复制到 Cache 中,后续访问直接命中 Cache,避免重复访问慢速内存。 然而,DMA 控制器直接访问物理内存,不经过 CPU 的 Cache。这就导致了一个经典问题: - **发送场景**:CPU 将数据写入内存缓冲区(数据可能被缓存,尚未写回物理内存),然后启动 DMA 传输。DMA 从物理内存读取数据,可能读到旧数据或未初始化的数据。 - **接收场景**:DMA 将接收到的数据写入内存缓冲区(直接写入物理内存),然后 CPU 读取该缓冲区。CPU 可能命中 Cache 中的旧数据,而看不到 DMA 写入的新数据。 在双缓冲串口收发中,这种不一致性会导致数据包错乱、丢包,甚至系统崩溃。因此,必须采取策略维护 Cache 一致性。 ## 2. 策略一:软件清 Cache(简单直接) ### 原理 通过调用 CMSIS 提供的函数,在关键操作前后强制将 Cache 中的数据写回内存(Clean)或使 Cache 失效(Invalidate)。 - **发送前**:Clean 数据缓冲区,确保 DMA 能从物理内存读到最新数据。 - **接收后**:Invalidate 数据缓冲区,使 CPU 重新从物理内存读取 DMA 写入的数据。 ### 配置步骤 1. 开启 D-Cache(默认可能开启,需确认)。 2. 在 DMA 传输前/后调用相应函数。 ### 代码示例 ```c #include "stm32f4xx.h" // 定义双缓冲结构 #define BUF_SIZE 256 uint8_t rx_buf[2][BUF_SIZE] __attribute__((aligned(32))); // 对齐到 Cache 行(32字节) uint8_t tx_buf[2][BUF_SIZE] __attribute__((aligned(32))); // 发送函数 void UART_DMA_Send(uint8_t *data, uint16_t len) { // 1. 将数据拷贝到 tx_buf(假设已在 tx_buf 中) // 2. 清 Cache:确保数据写回物理内存 SCB_CleanDCache_by_Addr((uint32_t*)tx_buf, len); // 3. 启动 DMA 发送 DMA_SetConfig(DMA2_Stream7, data, (uint32_t)&USART1->DR, len); DMA_Cmd(DMA2_Stream7, ENABLE); } // DMA 接收完成回调 void UART_DMA_RxComplete(void) { // 1. 使 Cache 失效:让 CPU 重新从内存读取数据 SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf[active_buf], BUF_SIZE); // 2. 处理接收到的数据 ProcessData(rx_buf[active_buf], BUF_SIZE); // 3. 切换缓冲区,重新启动 DMA 接收 active_buf ^= 1; DMA_SetConfig(DMA2_Stream5, (uint32_t)&USART1->DR, rx_buf[active_buf], BUF_SIZE); DMA_Cmd(DMA2_Stream5, ENABLE); } ``` ### 注意事项 - 缓冲区必须按 32 字节对齐(Cache 行大小),否则清 Cache 操作可能影响相邻数据。 - 频繁调用清 Cache 函数会降低性能,适合数据量小、频率不高的场景。 - 确保在 DMA 启动前完成 Clean,在 DMA 完成后进行 Invalidate。 ## 3. 策略二:配置 MPU 设置内存为非 Cacheable(硬件隔离) ### 原理 通过 MPU(内存保护单元)将 DMA 使用的内存区域配置为“非 Cacheable”属性,这样 CPU 访问该区域时直接读写物理内存,绕过 Cache。虽然会牺牲部分性能,但彻底避免了不一致问题,且无需软件干预。 ### 配置步骤 1. 初始化 MPU,设置一个区域覆盖 DMA 缓冲区。 2. 设置区域属性:非 Cacheable、非 Bufferable(或 Write-Through)。 3. 使能 MPU。 ### 代码示例 ```c void MPU_Config(void) { MPU_Region_InitTypeDef MPU_InitStruct; // 使能 MPU MPU_DeInit(); // 配置区域:基地址为缓冲区地址,大小 4KB(覆盖两个 256B 缓冲区) MPU_InitStruct.Enable = MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress = (uint32_t)rx_buf; MPU_InitStruct.Size = MPU_REGION_SIZE_4KB; MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS; MPU_InitStruct.IsBufferable = MPU_REGION_NOT_BUFFERABLE; MPU_InitStruct.IsCacheable = MPU_REGION_NOT_CACHEABLE; MPU_InitStruct.IsShareable = MPU_REGION_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; MPU_Init(&MPU_InitStruct); // 使能 MPU MPU_Cmd(ENABLE); } // 在主函数中调用 MPU_Config() ``` ### 注意事项 - 非 Cacheable 区域访问速度较慢,但 DMA 缓冲区通常不大,影响有限。 - 确保缓冲区地址和大小符合 MPU 区域对齐要求(通常为 32 字节倍数)。 - 如果同时使用多个缓冲区,可配置多个区域或使用一个大区域覆盖。 ## 4. 策略三:使用 CCM RAM(无 Cache 的专用内存) ### 原理 STM32F4 系列内部有一块 CCM(Core Coupled Memory)RAM,它直接连接到内核,不经过总线矩阵,因此也不受 Cache 影响。将 DMA 缓冲区放置在 CCM RAM 中,可以天然避免一致性问题,且访问速度极快(与内核同频)。 ### 配置步骤 1. 在链接脚本中定义 CCM RAM 段(通常地址为 0x10000000)。 2. 将缓冲区变量指定到该段。 ### 代码示例 ```c // 在链接脚本(如 stm32f407_flash.ld)中添加: // .ccmram : // { // . = ALIGN(4); // *(.ccmram) // . = ALIGN(4); // } > CCMRAM // 定义缓冲区到 CCM RAM __attribute__((section(".ccmram"))) uint8_t rx_buf[2][BUF_SIZE]; __attribute__((section(".ccmram"))) uint8_t tx_buf[2][BUF_SIZE]; // 使用方式与普通变量相同,无需额外 Cache 操作 ``` ### 注意事项 - CCM RAM 容量有限(通常 64KB 或 128KB),需合理分配。 - CCM RAM 不支持 DMA 访问?**注意**:在 STM32F4 中,DMA 控制器无法访问 CCM RAM,因为 CCM 只连接到内核。因此,**此策略仅适用于 CPU 访问缓冲区,不适用于 DMA 直接读写**。但可以用于 CPU 侧缓冲,DMA 使用另一块普通 RAM,通过软件拷贝数据,但这样会引入额外开销。 **修正**:实际上,STM32F4 的 DMA 无法访问 CCM RAM,所以此策略不适合直接用于 DMA 缓冲区。但可以作为辅助:将处理数据的临时缓冲区放在 CCM,DMA 缓冲区放在普通 RAM,通过 memcpy 在两者间拷贝(需配合 Cache 操作)。因此,此策略更适合对性能要求极高且数据量小的场景。 ## 5. 三种策略对比与选型建议 | 策略 | 优点 | 缺点 | 适用场景 | |------|------|------|----------| | 软件清 Cache | 简单、无需硬件配置 | 性能损耗,需注意对齐 | 数据量小、频率低 | | MPU 非 Cacheable | 硬件隔离,无需软件干预 | 访问速度稍慢,配置稍复杂 | 中等数据量,要求实时性 | | CCM RAM | 速度最快,无一致性问题 | 容量小,DMA 无法直接访问 | 需要 CPU 高频处理的小缓冲 | **推荐**:对于大多数串口双缓冲应用,策略一(软件清 Cache)足够且易于实现;若数据吞吐量大,建议使用策略二(MPU 配置);若对延迟极端敏感且数据量小,可结合策略三(CCM RAM)作为辅助。 ## 6. 总结 在 STM32F4 高速主频下,Cache 一致性是 DMA 通信不可忽视的问题。本文介绍了三种实用策略:软件清 Cache 简单直接,MPU 配置硬件隔离,CCM RAM 提供极致速度但需注意 DMA 限制。开发者应根据实际需求选择,并在代码中严格遵循操作顺序,确保数据一致性。希望本文能帮助你在嵌入式开发中少踩坑,提升系统可靠性。