volatile仅防止编译器优化,不能解决DMA与CPU访问同一变量的硬件竞态;必须配合双缓冲、原子索引、内存屏障、Cache控制或dma_alloc_coherent等同步机制才能确保数据一致性。

DMA与CPU同时改同一变量,不是“可能出错”,而是“必然出错”——只要没做同步,就一定会在某个优化等级、某个编译器版本、某次上电后暴露。
volatile 不能解决竞态,只能防止编译器乱优化
很多人第一反应是加 volatile,比如:volatile uint32_t buffer_ready = 0;。这确实能保证每次读写都落到内存,避免编译器把变量缓存在寄存器里,但它完全不阻止硬件级竞态:CPU执行 buffer_ready = 1 的瞬间,DMA可能正在往 rx_buffer[0] 写第100个字节;而CPU紧接着读 buffer_ready 判断完成,却还没等DMA把最后一个字节写完。现象就是“标志已置位,但数据不全”。volatile 只管“读写地址没错”,不管“读写时序对不对”。
真正要防的是 CPU 读写和 DMA 读写的物理重叠
问题核心不在变量本身,而在内存区域的访问权限和时序控制。必须让 CPU 和 DMA 对同一块缓冲区的访问“错开”或“可见”。常见做法有:
- 用双缓冲(ping-pong buffer):DMA始终写 buffer A,CPU处理 buffer B;DMA完成中断里交换指针,而不是改共享标志
- 用环形缓冲区 + 原子索引:把
head和tail声明为std::atomic<uint16_t></uint16_t>(需确认MCU支持原子指令),DMA只动tail,CPU只动head,两者不碰同一变量 - 禁用该缓冲区所在内存段的 D-Cache(如 Cortex-M7 的 MPU 配置为
Memory Attribute: Device或Non-cacheable),彻底消除 Cache 层带来的视图不一致
别信“DMA完成中断一进来数据就一定全了”
很多裸机代码在 DMA 中断服务函数(ISR)里直接设标志、然后主循环去读,这是高危操作。原因有三:
- DMA传输完成中断触发时,最后几个字节可能还在总线缓冲区(AXI write buffer)里,尚未真正写入 SRAM/DDR
- CPU如果开了 D-Cache,即使你刚在 ISR 里读过
rx_buffer[0],下次主循环读它时仍可能命中旧 Cache 行 - 某些 MCU(如 STM32H7)的 DMA 通道有“传输完成”和“更新完成”两种中断源,前者只表示 DMA 控制器任务结束,后者才表示所有写操作已落地
实操建议:在 ISR 末尾加 __DSB(); __ISB();(数据同步+指令同步屏障),再配合 __DMB(); 强制刷写写缓冲区;若用 Cache,则必须跟 SCB_CleanInvalidateDCache_by_Addr() 配合使用。
Linux 下别手写 cache 操作,优先用 dma_alloc_coherent
如果你在 Linux 驱动里写 DMA 代码,dma_alloc_coherent() 是首选。它返回的内存天然满足:CPU 写完立即对 DMA 可见,DMA 写完立即对 CPU 可见。背后机制是内核帮你做了三件事:
- 分配 non-cacheable 物理页(或通过 CMA + cache maintenance 指令模拟一致性)
- 禁止该页被 swap 或迁移
- 自动插入必要的内存屏障(
dma_wmb()/dma_rmb())
反例是用 kmalloc() 分配普通内存再调 dma_map_single() —— 这种方式需要你手动调 dma_sync_single_for_cpu() / dma_sync_single_for_device(),漏一次就可能丢数据。
最常被忽略的一点:DMA 缓冲区地址必须按 Cache Line 对齐(通常是 32 字节或 64 字节),否则 clean/invalidate 操作会误刷相邻行。哪怕你只用 128 字节缓冲区,也得从 aligned_alloc(64, 128) 开始申请,而不是 malloc(128)。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











