# 引言 STM32F4 系列搭载 Cortex-M4 内核,支持 D-Cache(数据缓存)和 I-Cache。D-Cache 能显著提升 CPU 访问外部 RAM 的性能,但同时也引入了缓存与主存之间的数据一致性问题。当 DMA 控制器直接访问内存时,它绕过 CPU 和缓存,导致 CPU 看到的缓存数据与 DMA 读写的主存数据可能不一致。本文针对五种典型场景,提供系统性的解决方案。 ## 场景一:CPU 写数据,DMA 读数据(外设发送) **问题描述**:CPU 在内存中构造发送缓冲区,然后启动 DMA 将数据发送到外设(如 UART、SPI)。由于 D-Cache 的存在,CPU 写入的数据可能仍留在缓存中,尚未写回主存,DMA 从主存读取时得到旧数据。 **解法**:在启动 DMA 前,对缓冲区执行 Cache Clean 操作,将缓存数据写回主存。 ```c // 假设缓冲区为 uint8_t tx_buf[256]; 长度为 len SCB_CleanDCache_by_Addr((uint32_t*)tx_buf, len); // 启动 DMA 传输 HAL_UART_Transmit_DMA(&huart, tx_buf, len); ``` **注意事项**: - Clean 操作必须在 DMA 启动之前完成。 - 地址需 32 字节对齐(Cortex-M4 缓存行大小),若不对齐,可整体 Clean 或使用 `SCB_CleanDCache()` 全清。 - 若频繁操作,建议使用 DMA 双缓冲或乒乓缓冲,避免每次 Clean 全缓冲区。 ## 场景二:DMA 写数据,CPU 读数据(外设接收) **问题描述**:DMA 将外设接收的数据写入内存,CPU 随后读取。由于 D-Cache 可能包含旧数据,CPU 读到的不是 DMA 刚写入的新数据。 **解法**:在 CPU 读取前,对缓冲区执行 Cache Invalidate 操作,使缓存行失效,强制从主存重新加载。 ```c // 假设接收缓冲区为 uint8_t rx_buf[256]; 长度为 len SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, len); // 现在 CPU 可以安全读取 rx_buf ``` **注意事项**: - Invalidate 操作必须在 CPU 读取之前执行。 - 如果缓冲区同时被 CPU 和 DMA 访问(如半字中断),需小心处理,避免覆盖未写回的数据。 - 建议在 DMA 传输完成中断中执行 Invalidate。 ## 场景三:双缓冲(Ping-Pong)模式 **问题描述**:双缓冲常用于高速数据流(如 ADC、以太网),两个缓冲区交替使用。一个缓冲区被 DMA 填充时,另一个被 CPU 处理。若不加处理,切换时可能发生数据错乱。 **解法**:在切换缓冲区时,对即将使用的缓冲区执行 Invalidate,对即将交给 DMA 的缓冲区执行 Clean。 ```c // 假设两个缓冲区 buf0, buf1,大小均为 BUF_SIZE volatile uint8_t *active_buf; void DMA_Complete_IRQHandler(void) { // 当前 DMA 完成,切换缓冲区 if (active_buf == buf0) { // 处理 buf0 前,先 Invalidate SCB_InvalidateDCache_by_Addr((uint32_t*)buf0, BUF_SIZE); process(buf0); // 准备 buf1 给下一次 DMA,先 Clean SCB_CleanDCache_by_Addr((uint32_t*)buf1, BUF_SIZE); start_dma(buf1); active_buf = buf1; } else { SCB_InvalidateDCache_by_Addr((uint32_t*)buf1, BUF_SIZE); process(buf1); SCB_CleanDCache_by_Addr((uint32_t*)buf0, BUF_SIZE); start_dma(buf0); active_buf = buf0; } } ``` **注意事项**: - 确保 Clean 和 Invalidate 的顺序正确:先 Clean 再启动 DMA,先 Invalidate 再读取。 - 缓冲区大小最好为缓存行(32 字节)的整数倍,避免部分失效。 - 使用 `__DMB()` 内存屏障确保操作顺序。 ## 场景四:DMA 描述符链(Scatter-Gather) **问题描述**:DMA 描述符链中,每个描述符包含缓冲区地址和长度。CPU 更新描述符后,DMA 读取描述符时可能因缓存不一致而使用旧描述符。 **解法**:在修改描述符后,对描述符区域执行 Clean;在读取 DMA 状态(如传输完成标志)前,执行 Invalidate。 ```c // 描述符结构体(假设 32 字节对齐) typedef struct { uint32_t addr; uint32_t len; uint32_t ctrl; uint32_t status; } DMA_Desc; DMA_Desc desc[2]; // 更新描述符 void update_desc(int idx, uint32_t addr, uint32_t len) { desc[idx].addr = addr; desc[idx].len = len; desc[idx].ctrl |= DMA_CTRL_VALID; // 确保描述符写回主存 SCB_CleanDCache_by_Addr((uint32_t*)&desc[idx], sizeof(DMA_Desc)); __DMB(); // 内存屏障 } // 在中断中检查状态 void DMA_IRQHandler(void) { SCB_InvalidateDCache_by_Addr((uint32_t*)desc, sizeof(desc)); if (desc[0].status & DMA_STATUS_DONE) { ... } } ``` **注意事项**: - 描述符必须对齐到缓存行边界,否则 Clean/Invalidate 可能影响相邻数据。 - 使用 `__DMB()` 确保 Clean 完成后才启动 DMA。 - 若描述符频繁更新,考虑将描述符放在非缓存区域(如 TCM RAM 或配置 MPU 为 non-cacheable)。 ## 场景五:共享内存(CPU 与 DMA 并发访问) **问题描述**:在多任务或中断中,CPU 和 DMA 同时访问同一内存区域(如共享状态标志、环形缓冲区),导致数据竞争。 **解法**:使用 MPU 将共享内存区域配置为 non-cacheable(或 write-through),彻底避免缓存一致性问题。 ```c // 配置 MPU 区域,使 0x20010000 开始的 4KB 为 non-cacheable void MPU_Config(void) { MPU_Region_InitTypeDef MPU_InitStruct; HAL_MPU_Disable(); MPU_InitStruct.Enable = MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress = 0x20010000; 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.SubRegionDisable = 0x00; MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0; MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE; HAL_MPU_ConfigRegion(&MPU_InitStruct); HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT); } ``` **注意事项**: - 非缓存区域访问速度较慢,仅对关键共享数据使用。 - 若使用 write-through 策略,CPU 写操作会直接更新主存,但读操作仍可能缓存,需结合 Invalidate。 - MPU 配置需在系统初始化时完成,并确保区域不重叠。 # 总结 D-Cache 与 DMA 的一致性问题是 STM32F4 高性能应用的常见挑战。核心原则是: - **CPU 写、DMA 读**:Clean 缓存。 - **DMA 写、CPU 读**:Invalidate 缓存。 - **双缓冲和描述符链**:在切换时执行相应操作,并配合内存屏障。 - **共享内存**:使用 MPU 配置为 non-cacheable,简化设计。 实际开发中,建议结合具体外设和性能要求,选择最合适的方案。对于极端实时性要求,可考虑将关键缓冲区放置于 TCM RAM(如 ITCM/DTCM),它们不经过 D-Cache,天然一致。希望本文能帮助你规避这些陷阱,写出更健壮的嵌入式代码。