# 引言 在 STM32F4 系列(如 STM32F429、F407)中,D-Cache 的引入是为了加速 CPU 对外部存储器(如 SDRAM)的访问。然而,D-Cache 的存在使得 CPU 和 DMA 对同一内存区域的访问可能产生数据不一致:CPU 修改的数据可能仍停留在 Cache 中,而 DMA 直接访问物理内存;反之,DMA 写入的新数据可能被 Cache 中的旧数据覆盖。这种问题在嵌入式系统中尤为致命,可能导致串口通信乱码、ADC 采样数据错误或文件系统损坏。 本文将针对三种典型场景,分析其原理,并给出基于 STM32 标准库(或 HAL 库)的修复方案,帮助开发者彻底解决这一隐患。 # 场景一:CPU 写数据,DMA 读数据(外设发送) ## 问题描述 CPU 在内存中准备好发送缓冲区(如串口发送数据),然后启动 DMA 将缓冲区内容传输到外设。如果 CPU 写入的数据还残留在 D-Cache 中,DMA 从物理内存读取时就会拿到旧数据,导致发送内容错误。 ## 原理分析 CPU 写入内存时,数据首先写入 D-Cache(写回策略),只有在 Cache 行被替换或显式清理时才会写回物理内存。DMA 是直接访问物理内存的,因此它看不到 Cache 中的最新数据。 ## 修复方案 在启动 DMA 之前,必须对缓冲区对应的 Cache 行执行清理(Clean)操作,将脏数据写回内存。 ## 代码示例(HAL 库) ```c // 发送缓冲区,需确保对齐到 32 字节(Cache 行大小) __attribute__((aligned(32))) uint8_t tx_buffer[256]; void send_via_dma(uint8_t *data, uint16_t len) { // 1. 将数据复制到 DMA 缓冲区(或直接使用 data,但需保证对齐) memcpy(tx_buffer, data, len); // 2. 清理 D-Cache,将 CPU 写入的数据写回内存 SCB_CleanDCache_by_Addr((uint32_t *)tx_buffer, len); // 3. 启动 DMA 传输 HAL_UART_Transmit_DMA(&huart1, tx_buffer, len); } ``` **注意**:`SCB_CleanDCache_by_Addr` 要求地址和长度都对齐到 32 字节,否则需手动调整。 # 场景二:DMA 写数据,CPU 读数据(外设接收) ## 问题描述 DMA 从外设接收数据并写入内存,然后 CPU 读取这些数据进行处理。如果 CPU 在 DMA 写入之前已经缓存了该内存区域的数据,那么 DMA 写入的新数据不会更新 Cache,CPU 读取时仍会命中旧的 Cache 行,得到陈旧数据。 ## 原理分析 DMA 写入物理内存后,Cache 中对应的行可能仍是之前的数据(如果之前被 CPU 读取过)。CPU 再次读取时,Cache 命中,返回旧值。 ## 修复方案 在 CPU 读取 DMA 写入的数据之前,必须使对应的 Cache 行失效(Invalidate),强制 CPU 从物理内存重新加载。 ## 代码示例(HAL 库) ```c __attribute__((aligned(32))) uint8_t rx_buffer[256]; void on_dma_rx_complete() { // 1. 使 D-Cache 失效,丢弃旧的缓存数据 SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buffer, received_len); // 2. 现在可以安全地读取 rx_buffer 中的数据 process_data(rx_buffer, received_len); } ``` **注意**:失效操作会丢弃该地址范围内的所有 Cache 行,如果该区域有未写回的数据,也会丢失。因此,在 DMA 写入前,若 CPU 曾修改过该区域,需先执行 Clean。 # 场景三:共享缓冲区(双向通信) ## 问题描述 一个缓冲区同时用于 DMA 发送和接收,或者 CPU 和 DMA 交替读写。例如,一个环形缓冲区用于串口收发,CPU 写入待发送数据,DMA 读取;同时 DMA 写入接收数据,CPU 读取。这种场景下,需要同时处理 Clean 和 Invalidate,且顺序至关重要。 ## 原理分析 共享缓冲区中,CPU 和 DMA 都可能修改数据。如果只做 Clean,则 DMA 写入的新数据可能被 Cache 中的旧数据覆盖;如果只做 Invalidate,则 CPU 写入的数据可能丢失。必须根据操作顺序,先 Clean 再 Invalidate,或者使用 CleanAndInvalidate 操作。 ## 修复方案 - 在 CPU 写入数据给 DMA 读之前:执行 Clean(写回 CPU 的修改)。 - 在 DMA 写入数据给 CPU 读之前:执行 Invalidate(丢弃旧缓存)。 - 如果同一缓冲区同时存在两种情况,建议使用 `SCB_CleanInvalidateDCache_by_Addr`,它先清理再失效,确保数据一致性。 ## 代码示例(HAL 库) ```c __attribute__((aligned(32))) uint8_t shared_buf[128]; // CPU 准备发送数据 void prepare_tx_data(uint8_t *data, uint16_t len) { memcpy(shared_buf, data, len); // 清理并失效,确保 DMA 能读到最新数据,且后续 DMA 写入不会被旧缓存干扰 SCB_CleanInvalidateDCache_by_Addr((uint32_t *)shared_buf, len); // 启动 DMA 发送... } // DMA 接收完成回调 void on_rx_done(uint16_t len) { // 清理并失效,确保 CPU 读取到 DMA 写入的新数据,同时保留 CPU 可能写入的未回写数据 SCB_CleanInvalidateDCache_by_Addr((uint32_t *)shared_buf, len); // 处理数据... } ``` # 注意事项与最佳实践 - **对齐要求**:所有涉及 DMA 的缓冲区必须按 32 字节对齐(Cache 行大小),否则 `SCB_*DCache_by_Addr` 可能无法正确操作,导致数据不一致。可使用 `__attribute__((aligned(32)))` 或链接脚本配置。 - **长度处理**:`SCB_*DCache_by_Addr` 的长度参数会向上取整到 32 字节的倍数,因此调用时传入实际长度即可,但需确保缓冲区大小足够。 - **性能权衡**:频繁的 Clean/Invalidate 会降低性能,因为会强制内存访问。建议将 DMA 缓冲区设计为专用区域,避免与普通变量混用,并尽量减少一致性操作的频率。 - **使用 MPU 配置**:对于某些外设(如 LTDC、DMA2D),可以配置 MPU 将相关内存区域设置为非缓存(Device 或 Strongly Ordered),从而避免一致性问题,但会牺牲 CPU 访问性能。 - **调试技巧**:在调试时,可以临时禁用 D-Cache(`SCB_DisableDCache()`)来验证问题是否由缓存引起,但最终必须启用并正确处理。 # 总结 STM32F4 的 D-Cache 是一把双刃剑,它提升了性能,但也引入了数据一致性的复杂性。本文通过三个典型场景,展示了如何通过 Clean 和 Invalidate 操作来保证 CPU 与 DMA 之间的数据同步。在实际开发中,务必遵循以下原则: - CPU 写、DMA 读:先 Clean。 - DMA 写、CPU 读:先 Invalidate。 - 共享缓冲区:使用 CleanInvalidate,并注意操作顺序。 只有深入理解缓存机制,才能写出既高效又可靠的嵌入式代码。希望本文能帮助你规避这一常见陷阱,让你的 STM32 项目更加稳定。