# 引言 STM32F4 系列内置了 Cortex-M4 内核,支持 D-Cache(数据缓存)和 I-Cache。D-Cache 能显著提升 CPU 访问外部存储器(如 SDRAM)的性能,但同时也引入了缓存一致性问题。当 DMA 控制器直接访问内存时,它绕过 CPU 的缓存,直接读写物理内存。如果 CPU 和 DMA 对同一块内存区域进行操作,而缓存未及时同步,就会产生数据不一致。 本文针对三种典型场景,深入分析问题根源,并给出经过验证的规避方案。 # 场景一:CPU 写数据,DMA 读数据(发送场景) ## 问题原理 CPU 将待发送的数据写入内存缓冲区,然后启动 DMA 将缓冲区内容传输到外设(如 UART、SPI)。由于 D-Cache 的存在,CPU 写入的数据可能只停留在缓存中,尚未回写到物理内存。DMA 读取物理内存时,得到的是旧数据,导致发送错误。 ## 规避方案 在启动 DMA 传输前,必须调用 `SCB_CleanDCache()` 或 `SCB_CleanDCache_by_Addr()` 将缓存中对应地址区域的数据回写到物理内存。 ## 代码示例 ```c #include "stm32f4xx.h" #include "core_cm4.h" uint8_t tx_buffer[1024] __attribute__((aligned(32))); void send_via_dma(void) { // 填充数据 for (int i = 0; i < sizeof(tx_buffer); i++) { tx_buffer[i] = i; } // 关键:回写 D-Cache,确保数据到达物理内存 SCB_CleanDCache_by_Addr((uint32_t*)tx_buffer, sizeof(tx_buffer)); // 配置 DMA 并启动传输(此处省略具体外设配置) DMA_Start_Transmit(tx_buffer, sizeof(tx_buffer)); } ``` ## 注意事项 - 缓冲区地址必须按 32 字节对齐(Cache line 大小),否则 `CleanDCache_by_Addr` 可能无法正确回写整个区域。 - 如果缓冲区大小不是 32 的倍数,需向上取整,避免遗漏边界。 # 场景二:DMA 写数据,CPU 读数据(接收场景) ## 问题原理 DMA 从外设接收数据并写入内存缓冲区,然后 CPU 读取该缓冲区进行处理。DMA 写入的是物理内存,但 CPU 读取时,如果缓存中已有该地址的旧数据(例如之前 CPU 访问过该区域),则 CPU 会命中缓存,读到旧数据,而新数据被忽略。 ## 规避方案 在 CPU 读取 DMA 写入的数据之前,必须调用 `SCB_InvalidateDCache()` 或 `SCB_InvalidateDCache_by_Addr()` 使缓存中对应地址区域失效,强制 CPU 从物理内存重新加载。 ## 代码示例 ```c uint8_t rx_buffer[1024] __attribute__((aligned(32))); volatile uint32_t rx_len = 0; void DMA_RX_Complete_Handler(void) { // 关键:使缓存失效,让 CPU 从物理内存读取最新数据 SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buffer, sizeof(rx_buffer)); // 现在可以安全地处理 rx_buffer 中的数据 process_data(rx_buffer, rx_len); } ``` ## 注意事项 - 失效操作必须在 DMA 完全完成之后、CPU 读取之前执行。 - 如果 DMA 采用循环模式,每次传输完成中断中都要执行失效操作,但要注意避免在 DMA 仍在写入时失效,否则可能读到半新半旧的数据。 # 场景三:双缓冲交替访问(乒乓缓冲) ## 问题原理 在高速数据采集或通信中,常使用双缓冲(乒乓缓冲)机制:DMA 正在填充缓冲区 A 时,CPU 处理缓冲区 B;然后交换。这种模式下,CPU 和 DMA 交替访问两个缓冲区,若缓存管理不当,会导致数据错乱。例如,CPU 处理完 B 后,下次 DMA 可能写入 B,但 CPU 的缓存中仍保留 B 的旧数据,导致后续读取错误。 ## 规避方案 在切换缓冲区时,对即将由 DMA 写入的缓冲区执行失效操作,对即将由 CPU 写入的缓冲区执行回写操作。具体流程如下: 1. 当 DMA 完成填充缓冲区 A,CPU 准备处理 A 时,先失效 A 的缓存。 2. 当 CPU 处理完 A,准备让 DMA 再次使用 A 时,先回写 A 的缓存(如果 CPU 修改过 A)。 3. 对缓冲区 B 同理。 ## 代码示例 ```c #define BUFFER_SIZE 1024 uint8_t buf_A[BUFFER_SIZE] __attribute__((aligned(32))); uint8_t buf_B[BUFFER_SIZE] __attribute__((aligned(32))); volatile uint8_t current_buf = 0; // 0: A, 1: B void DMA_Transfer_Complete_Handler(void) { uint8_t *processed_buf; uint8_t *next_buf; if (current_buf == 0) { processed_buf = buf_A; next_buf = buf_B; } else { processed_buf = buf_B; next_buf = buf_A; } // 1. 使已填充缓冲区的缓存失效,以便 CPU 读取最新数据 SCB_InvalidateDCache_by_Addr((uint32_t*)processed_buf, BUFFER_SIZE); // 2. 处理数据(假设处理过程中可能修改缓冲区) process_data(processed_buf, BUFFER_SIZE); // 3. 回写已处理缓冲区,确保修改写回物理内存(若未修改可省略) SCB_CleanDCache_by_Addr((uint32_t*)processed_buf, BUFFER_SIZE); // 4. 切换缓冲区,并准备下一次 DMA 传输 current_buf ^= 1; DMA_Start_Receive(next_buf, BUFFER_SIZE); } ``` ## 注意事项 - 失效和回写操作必须成对出现,且顺序不能颠倒。 - 如果 CPU 在步骤 2 中只读不写,则步骤 3 可以省略,但为了通用性建议保留。 - 缓冲区切换时,要确保 DMA 不会在缓存操作完成前启动,否则可能仍会出错。 # 总结 D-Cache 与 DMA 的数据一致性是 STM32F4 开发中的关键问题。本文总结了三种典型场景及对应的规避方案: - **CPU 写,DMA 读**:启动 DMA 前执行 `SCB_CleanDCache_by_Addr()`。 - **DMA 写,CPU 读**:CPU 读取前执行 `SCB_InvalidateDCache_by_Addr()`。 - **双缓冲交替**:切换时对两个缓冲区分别执行失效和回写操作。 此外,务必注意缓冲区地址对齐(32 字节)和大小处理,避免边界问题。在实际项目中,建议封装统一的缓存管理函数,减少出错概率。掌握这些技巧,你将能充分发挥 STM32F4 的高性能,同时避免数据陷阱。