引言
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() 将缓存中对应地址区域的数据回写到物理内存。
代码示例
#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 从物理内存重新加载。
代码示例
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 写入的缓冲区执行回写操作。具体流程如下:
- 当 DMA 完成填充缓冲区 A,CPU 准备处理 A 时,先失效 A 的缓存。
- 当 CPU 处理完 A,准备让 DMA 再次使用 A 时,先回写 A 的缓存(如果 CPU 修改过 A)。
- 对缓冲区 B 同理。
代码示例
#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 的高性能,同时避免数据陷阱。