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)。
// 假设 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)。
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 字节)对齐,并分别维护。
// 使用 __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 描述符所在内存区域,并添加内存屏障确保顺序。
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。
// 使用 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 通信。
#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,并配合对齐和内存屏障,即可确保数据一致。希望本文能帮助你避开这些坑,写出更稳定的嵌入式代码。