引言
在 STM32F4 系列(如 STM32F407、STM32F429)中,Cortex-M4 内核集成了可配置的 D-Cache(数据缓存),用于加速 CPU 对内存的访问。然而,当 DMA 控制器直接访问内存时,D-Cache 的存在会导致 CPU 与 DMA 看到的数据不一致,从而引发难以排查的 bug。本文将从原理出发,结合实际案例,给出系统性的排查与修复方案。
D-Cache 与 DMA 的工作原理
为什么需要 D-Cache?
D-Cache 是 CPU 与主存(如 SRAM)之间的小容量高速缓存。CPU 读取数据时,优先从 Cache 中获取;写入时,数据可能暂存在 Cache 中(写回策略),而非立即更新主存。这显著提升了 CPU 访问速度,但引入了数据一致性问题。
DMA 的“旁路”特性
DMA 控制器直接访问主存,不经过 CPU 的 Cache。因此,当 DMA 向内存写入数据时,CPU 可能从 Cache 中读到旧值;反之,CPU 修改数据后,DMA 可能从主存中读到旧值。
一致性问题的两种场景
-
场景 A:DMA 写入,CPU 读取(如 ADC 采样数据由 DMA 搬运到内存)
- DMA 更新主存,但 Cache 中保留旧数据,CPU 读 Cache 得到陈旧值。
-
场景 B:CPU 写入,DMA 读取(如 CPU 准备数据包,DMA 发送)
- CPU 写 Cache,但主存未更新,DMA 读主存得到旧数据。
典型问题现象
- 数据包发送时,偶尔出现首字节错误或丢包。
- ADC 采样数据时,读取的数组始终是旧值,除非禁用 Cache。
- 调试时,观察变量显示正确,但实际运行结果异常(因为调试器可能强制刷新 Cache)。
解决方案:Clean 与 Invalidate
核心操作
- Clean(清干净):将 Cache 中的脏数据(修改过但未写回主存)写回主存,确保主存最新。
- Invalidate(使无效):将 Cache 中的行标记为无效,下次访问时重新从主存读取。
CMSIS 提供的 API
在 STM32F4 的 CMSIS 设备头文件中,提供了以下函数(定义于 core_cm4.h):
void SCB_CleanDCache(void); // 清理整个 D-Cache
void SCB_InvalidateDCache(void); // 使整个 D-Cache 无效
void SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize);
void SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize);
注意:dsize 参数是字节数,且地址需 32 字节对齐(Cache line 大小)。
实战案例:DMA 接收数据
硬件配置
- MCU:STM32F407ZGT6
- 外设:USART2,DMA1 通道 6 接收数据
- 缓冲区:
uint8_t rx_buf[256] __attribute__((aligned(32)));
错误代码(未处理一致性)
uint8_t rx_buf[256];
void DMA1_Stream5_IRQHandler(void) {
if (DMA_GetITStatus(DMA1_Stream5, DMA_IT_TCIF5)) {
DMA_ClearITPendingBit(DMA1_Stream5, DMA_IT_TCIF5);
// 直接处理 rx_buf,但可能读到旧数据
process_data(rx_buf);
}
}
修复后的代码
// 使用 __attribute__((aligned(32))) 确保对齐
uint8_t rx_buf[256] __attribute__((aligned(32)));
void DMA1_Stream5_IRQHandler(void) {
if (DMA_GetITStatus(DMA1_Stream5, DMA_IT_TCIF5)) {
DMA_ClearITPendingBit(DMA1_Stream5, DMA_IT_TCIF5);
// 关键:使 Cache 无效,强制从主存重新读取
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, sizeof(rx_buf));
process_data(rx_buf);
}
}
发送场景修复
// CPU 填充数据后,发送前 Clean
uint8_t tx_buf[128] __attribute__((aligned(32)));
void send_data(uint8_t *data, uint16_t len) {
memcpy(tx_buf, data, len);
// 将 Cache 中的数据写回主存,确保 DMA 能读到最新值
SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, len);
// 启动 DMA 发送
DMA_SetCurrDataCounter(DMA1_Stream4, len);
DMA_Cmd(DMA1_Stream4, ENABLE);
}
注意事项
-
对齐要求:
SCB_CleanDCache_by_Addr和SCB_InvalidateDCache_by_Addr要求地址按 32 字节对齐,否则可能导致断言失败或操作无效。建议使用__attribute__((aligned(32)))定义缓冲区。 -
长度处理:
dsize参数是字节数,但内部会按 Cache line 对齐处理,建议传入实际长度,但确保缓冲区大小是 32 的倍数,避免越界。 - 性能开销:频繁 Clean/Invalidate 会降低性能,建议仅在 DMA 传输完成或开始前调用,避免在中断中频繁操作。
- 多缓冲区策略:使用双缓冲(Ping-Pong)时,需分别对每个缓冲区进行一致性操作。
-
调试技巧:如果问题难以复现,可临时禁用 D-Cache(
SCB_DisableDCache())对比测试,确认是否由 Cache 引起。
总结
D-Cache 与 DMA 的一致性问题是 STM32F4 开发中的经典陷阱。通过理解 Cache 的写回机制和 DMA 的旁路特性,并合理使用 Clean 和 Invalidate 操作,可以彻底解决此类问题。记住:DMA 写入后,CPU 读取前需 Invalidate;CPU 写入后,DMA 读取前需 Clean。希望本文能帮助你少走弯路,让嵌入式开发更加顺畅。