STM32F4 D-Cache 与 DMA 数据一致性:五种失效场景及修复方案
👁 2 阅读 · 2026-08-27 · 嵌入式
在 STM32F4 系列(如 STM32F407、STM32F429)中,D-Cache 虽能提升 CPU 访问速度,却常与 DMA 产生数据一致性问题,导致外设数据错乱、系统崩溃。本文深入剖析五种典型失效场景——DMA 写入后 CPU 读到旧数据、CPU 修改后 DMA 发出旧数据、双缓冲交替、描述符链更新、以及外设寄存器访问,并给出基于 `SCB_CleanDCache`、`SCB_InvalidateDCache` 及内存屏障的修复方案,附完整代码示例与配置要点,助你规避嵌入式开发中的经典陷阱。
# STM32F4 D-Cache 与 DMA 数据一致性:五种失效场景及修复方案
## 1. 背景与原理
STM32F4 系列(Cortex-M4)内置 D-Cache(数据缓存),用于加速 CPU 对 SRAM 的访问。但当 DMA 直接访问内存时,DMA 不经过 Cache,直接读写物理 RAM。这导致 Cache 中的数据与 RAM 中的数据可能不一致,即“数据一致性问题”。
- **DMA 写入 RAM**:DMA 将外设数据写入 RAM,但 Cache 中仍保留旧数据,CPU 读取时命中 Cache,得到过期数据。
- **CPU 写入 RAM**:CPU 修改数据后,数据可能仍留在 Cache(写回策略),DMA 读取 RAM 时得到旧值。
STM32F4 的 D-Cache 默认是写回(write-back)模式,因此必须手动维护一致性。
## 2. 五种失效场景与修复
### 场景 1:DMA 接收数据后,CPU 读取旧数据
**现象**:UART 或 SPI 通过 DMA 接收数据到缓冲区,CPU 在 DMA 完成中断中读取缓冲区,却得到旧数据。
**原因**:DMA 写入 RAM,但 Cache 中对应行仍为旧值,CPU 读 Cache 命中。
**修复**:在 CPU 读取前,使 Cache 失效(Invalidate)。
```c
// 假设 DMA 接收完成中断
void DMA_RX_IRQHandler(void) {
// 使缓冲区对应的 Cache 行失效
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buffer, sizeof(rx_buffer));
// 现在 CPU 读取 rx_buffer 将获得 DMA 写入的新数据
process_data(rx_buffer);
}
```
### 场景 2:CPU 修改数据后,DMA 发送旧数据
**现象**:CPU 填充发送缓冲区,启动 DMA 发送,但 DMA 发送的是旧数据。
**原因**:CPU 写入的数据还在 Cache 中,尚未写回 RAM,DMA 直接读 RAM 得到旧值。
**修复**:在启动 DMA 前,将缓冲区 Cache 行写回(Clean)。
```c
void send_data(uint8_t* buf, uint32_t len) {
// 填充 buf
fill_buffer(buf, len);
// 将 buf 写回 RAM,确保 DMA 可见
SCB_CleanDCache_by_Addr((uint32_t*)buf, len);
// 启动 DMA 发送
DMA_Start_TX(buf, len);
}
```
### 场景 3:双缓冲交替使用,未同步维护
**现象**:使用双缓冲(Ping-Pong)时,一个缓冲区被 DMA 写入,另一个被 CPU 处理,切换时数据错乱。
**原因**:两个缓冲区可能映射到同一 Cache 行(若地址相邻),导致失效操作影响彼此。
**修复**:确保缓冲区按 Cache 行大小(32 字节)对齐,并分别维护。
```c
// 使用 __ALIGNED(32) 对齐缓冲区
__ALIGNED(32) uint8_t buf_ping[1024];
__ALIGNED(32) uint8_t buf_pong[1024];
void DMA_Complete_IRQ(uint8_t active_buf) {
if (active_buf == 0) {
SCB_InvalidateDCache_by_Addr((uint32_t*)buf_ping, sizeof(buf_ping));
process(buf_ping);
SCB_CleanDCache_by_Addr((uint32_t*)buf_ping, sizeof(buf_ping));
} else {
SCB_InvalidateDCache_by_Addr((uint32_t*)buf_pong, sizeof(buf_pong));
process(buf_pong);
SCB_CleanDCache_by_Addr((uint32_t*)buf_pong, sizeof(buf_pong));
}
}
```
### 场景 4:DMA 描述符链更新,CPU 修改描述符后未写回
**现象**:使用 DMA 描述符(如以太网 DMA)时,CPU 更新描述符(如设置长度、状态),但 DMA 仍读取旧描述符。
**原因**:描述符结构体被 CPU 修改,但数据在 Cache 中,DMA 读 RAM 得到旧值。
**修复**:更新描述符后,必须 Clean 描述符所在内存区域,并添加内存屏障确保顺序。
```c
typedef struct {
uint32_t addr;
uint32_t len;
uint32_t ctrl;
} DMA_Desc;
void update_desc(DMA_Desc* desc, uint32_t addr, uint32_t len) {
desc->addr = addr;
desc->len = len;
desc->ctrl |= DESC_CTRL_VALID;
// 写回描述符
SCB_CleanDCache_by_Addr((uint32_t*)desc, sizeof(DMA_Desc));
// 内存屏障,确保写回完成
__DSB();
// 启动 DMA
DMA_Start(desc);
}
```
### 场景 5:外设寄存器映射到内存区域,被 Cache 缓存
**现象**:某些外设(如 FSMC 控制的 LCD、外部 SRAM)映射到内存地址,CPU 访问时被 Cache 缓存,导致外设状态更新不及时。
**原因**:外设寄存器或帧缓冲被标记为可缓存,CPU 写操作被缓存,外设看不到。
**修复**:将外设映射区域配置为不可缓存(使用 MPU),或手动 Clean/Invalidate。
```c
// 使用 MPU 配置区域为不可缓存
void MPU_Config(void) {
MPU_Region_InitTypeDef MPU_InitStruct;
// 配置区域基地址为外设地址,大小 1MB,属性为 Device
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0x60000000; // FSMC 区域
MPU_InitStruct.Size = MPU_REGION_SIZE_1MB;
MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL_0;
MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE;
MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
__DSB();
}
```
## 3. 完整代码示例:UART DMA 接收 + 发送
以下示例展示如何安全使用 D-Cache 与 DMA 进行 UART 通信。
```c
#include "stm32f4xx_hal.h"
__ALIGNED(32) uint8_t rx_buf[256];
__ALIGNED(32) uint8_t tx_buf[256];
void UART_DMA_Init(void) {
// 使能 D-Cache(若未使能)
SCB_EnableDCache();
// 配置 UART DMA 等(省略)
}
void UART_RX_Complete_IRQ(void) {
// 使 rx_buf 的 Cache 失效
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, sizeof(rx_buf));
// 处理接收数据
process_rx(rx_buf);
}
void UART_TX_Start(uint8_t* data, uint32_t len) {
memcpy(tx_buf, data, len);
// 写回 tx_buf
SCB_CleanDCache_by_Addr((uint32_t*)tx_buf, len);
// 启动 DMA 发送
HAL_UART_Transmit_DMA(&huart, tx_buf, len);
}
```
## 4. 注意事项
- **对齐要求**:缓冲区必须按 32 字节(Cache 行大小)对齐,否则 `SCB_InvalidateDCache_by_Addr` 可能破坏相邻数据。
- **长度处理**:操作长度最好为 32 的倍数,否则需手动处理边界。
- **内存屏障**:在 Clean/Invalidate 后,使用 `__DSB()` 确保操作完成。
- **性能权衡**:频繁 Clean/Invalidate 会降低性能,建议使用 DMA 双缓冲并批量处理。
- **MPU 配置**:对于外设映射区域,优先使用 MPU 设置为不可缓存,避免手动操作。
## 5. 总结
D-Cache 与 DMA 的一致性问题在 STM32F4 开发中极为常见,理解失效场景并正确使用 `SCB_CleanDCache` 和 `SCB_InvalidateDCache` 是解决问题的关键。记住:**DMA 写入后要 Invalidate,DMA 读取前要 Clean**,并配合对齐和内存屏障,即可确保数据一致。希望本文能帮助你避开这些坑,写出更稳定的嵌入式代码。