# STM32F4 D-Cache 与 DMA 数据一致性:三大典型场景及解决方案 ## 引言 STM32F4 系列(如 STM32F407/427)内置了 Cortex-M4 内核,支持可选的 D-Cache(数据缓存)。启用 D-Cache 后,CPU 访问内存数据会先经过缓存,而 DMA 外设则直接访问物理内存(SRAM)。这种架构差异导致 CPU 与 DMA 之间可能看到不一致的数据,尤其在高速数据传输(如 ADC、UART、SPI)中,若未正确处理,轻则数据错误,重则系统崩溃。本文针对三种典型场景,给出基于 CMSIS 的解决方案。 ## 背景知识:D-Cache 的工作原理 D-Cache 是 CPU 与主存之间的高速缓存,以行(通常 32 字节)为单位。当 CPU 写数据时,数据可能只更新到 Cache 中(写回策略),尚未同步到 SRAM;当 CPU 读数据时,可能直接从 Cache 读取,而忽略 SRAM 中已被 DMA 更新的内容。因此,需要软件显式地执行 Cache 维护操作: - **Clean**:将 Cache 中的脏数据写回 SRAM(`SCB_CleanDCache`)。 - **Invalidate**:使 Cache 行失效,强制 CPU 下次从 SRAM 重新读取(`SCB_InvalidateDCache`)。 - **Clean & Invalidate**:先写回再失效(`SCB_CleanInvalidateDCache`)。 ## 场景一:CPU 写数据,DMA 读取(外设发送) **问题描述**:CPU 准备一个数据缓冲区,然后启动 DMA 将数据发送到外设(如 UART、SPI)。若 CPU 写入的数据仍停留在 D-Cache 中,DMA 从 SRAM 读取时可能拿到旧数据。 **解决方案**:在启动 DMA 前,对缓冲区执行 Clean 操作,确保数据已写回 SRAM。 **代码示例**: ```c // 缓冲区定义(建议 32 字节对齐) uint8_t tx_buffer[256] __attribute__((aligned(32))); void send_via_dma(uint8_t *buf, uint32_t len) { // 1. 确保 CPU 写入的数据已同步到 SRAM SCB_CleanDCache_by_Addr((uint32_t *)buf, (int32_t)len); // 2. 启动 DMA(以 UART 为例) // 配置 DMA 通道,源地址指向 buf,长度 len,然后使能 // ... DMA 配置代码 ... } ``` **注意事项**: - 缓冲区地址和长度需对齐到 32 字节,否则 Clean 操作可能不完整(CMSIS 函数内部会处理非对齐,但性能下降)。 - 若 DMA 传输完成后,CPU 需要修改缓冲区,应先执行 Invalidate 再读取,避免读到 Cache 中的旧数据。 ## 场景二:DMA 写数据,CPU 读取(外设接收) **问题描述**:DMA 将外设接收的数据写入 SRAM 缓冲区,然后 CPU 读取处理。若 CPU 之前已访问过该缓冲区,Cache 中可能存有旧数据,导致 CPU 读到过时内容。 **解决方案**:在 DMA 传输完成后,对缓冲区执行 Invalidate 操作,强制 CPU 从 SRAM 重新读取。 **代码示例**: ```c uint8_t rx_buffer[128] __attribute__((aligned(32))); volatile uint8_t rx_complete = 0; void DMA_IRQHandler(void) { if (/* DMA 传输完成中断 */) { // 1. 使缓冲区对应的 Cache 行失效 SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buffer, sizeof(rx_buffer)); rx_complete = 1; } } void process_rx_data(void) { if (rx_complete) { // 此时读取 rx_buffer 中的数据是新鲜的 // 处理数据... rx_complete = 0; } } ``` **注意事项**: - Invalidate 操作必须在 CPU 读取缓冲区之前完成,且应在 DMA 完全结束后执行(在中断中处理)。 - 若缓冲区被多个 DMA 通道复用,需确保每次传输后都执行 Invalidate,避免残留。 - 对于连续 DMA 传输(如双缓冲),建议使用 Clean+Invalidate 组合,以防数据覆盖。 ## 场景三:共享缓冲区(CPU 与 DMA 同时读写) **问题描述**:在音频或图像处理中,CPU 和 DMA 可能同时操作同一缓冲区(例如,DMA 持续采集数据,CPU 实时处理)。此时,简单的 Clean 或 Invalidate 无法保证一致性,需要更精细的控制。 **解决方案**:采用双缓冲(Ping-Pong)机制,并配合 Clean/Invalidate 操作,确保 CPU 和 DMA 不会同时访问同一块内存。 **代码示例**: ```c #define BUF_SIZE 1024 uint8_t buf_ping[BUF_SIZE] __attribute__((aligned(32))); uint8_t buf_pong[BUF_SIZE] __attribute__((aligned(32))); volatile uint8_t current_buf = 0; // 0: ping, 1: pong void DMA_IRQHandler(void) { // DMA 完成当前缓冲区传输 if (current_buf == 0) { // 使 pong 缓冲区失效,准备 CPU 读取 SCB_InvalidateDCache_by_Addr((uint32_t *)buf_pong, BUF_SIZE); // 切换 DMA 到 ping 缓冲区(并 Clean ping,确保 CPU 写入的数据被 DMA 看到) SCB_CleanDCache_by_Addr((uint32_t *)buf_ping, BUF_SIZE); // 配置 DMA 使用 ping 缓冲区... current_buf = 1; } else { SCB_InvalidateDCache_by_Addr((uint32_t *)buf_ping, BUF_SIZE); SCB_CleanDCache_by_Addr((uint32_t *)buf_pong, BUF_SIZE); // 配置 DMA 使用 pong 缓冲区... current_buf = 0; } } void cpu_process(void) { uint8_t *buf_to_process = (current_buf == 0) ? buf_pong : buf_ping; // 处理数据(此时该缓冲区已失效,CPU 从 SRAM 读取) } ``` **注意事项**: - 双缓冲避免了 CPU 和 DMA 对同一地址的竞争,但需确保切换逻辑正确,防止数据覆盖。 - 每次切换时,对即将被 CPU 使用的缓冲区执行 Invalidate,对即将被 DMA 使用的缓冲区执行 Clean。 - 若缓冲区较大,可考虑使用 MPU 将区域配置为非缓存(`MPU_REGION_NO_CACHE`),从根源避免一致性问题,但会牺牲性能。 ## 总结与最佳实践 - **缓存对齐**:所有 DMA 缓冲区建议使用 `__attribute__((aligned(32)))` 对齐,并确保长度是 32 的倍数,以发挥 Cache 操作的最大效率。 - **操作顺序**:Clean 必须在 DMA 启动前;Invalidate 必须在 DMA 完成后、CPU 读取前。 - **中断安全**:Cache 操作在中断中执行时,注意优先级和原子性,避免被其他中断打断。 - **性能权衡**:频繁的 Clean/Invalidate 会降低性能,可考虑使用 MPU 将特定区域配置为缓存关闭(`MPU_REGION_NO_CACHE`),适用于高频 DMA 场景。 - **调试技巧**:使用调试器观察 Cache 和 SRAM 内容,可快速定位一致性问题。 掌握以上三种场景的解决方案,你就能在 STM32F4 上安全地使用 D-Cache 与 DMA,实现高性能且可靠的数据传输。