STM32F4 系列 D-Cache 与 DMA 数据一致性:五种典型场景及解法
👁 1 阅读 · 2026-08-27 · 嵌入式
在 STM32F4 系列(如 F429、F407)中,当启用 D-Cache 后,DMA 与 CPU 之间的数据一致性成为嵌入式开发中的常见痛点。本文深入剖析五种典型场景(CPU 写 DMA 读、DMA 写 CPU 读、双缓冲、描述符链、共享内存),并给出基于 Cortex-M4 内核的 Clean/Invalidate 操作、内存屏障及 MPU 配置的实用解法,帮助开发者避免数据错乱、性能下降等隐患。
# 引言
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,天然一致。希望本文能帮助你规避这些陷阱,写出更健壮的嵌入式代码。