2. DMA 与 PIO 对比:谁才是真正的效率担当?
好,我们直接切入正题。中断驱动 I/O(也就是 PIO 模式)和 DMA,很多工程师做了好几年项目,可能对它们的认知还停留在“DMA 比较快”这种模糊印象上。
但实际情况要复杂得多。
我个人习惯是,在做系统架构选型时,先把这两者的底层逻辑彻底理清楚。你想想看,如果一个方案选错了,轻则 CPU 被无端浪费,重则导致整个系统响应延迟失控。我在项目中遇到过不止一次这样的案例,最后追查下来,都是对 PIO 和 DMA 的本质差异理解不到位。
这一章,我们就把这个问题彻底讲透。
2.1 核心差异:CPU 是主力还是监工?
先说 PIO。Programmed I/O,说白了就是 CPU 亲力亲为。每一次数据从外设到内存的搬移,都需要 CPU 一条指令一条指令地去执行。CPU 要么轮询状态寄存器(Polling),要么等待中断通知,然后手动把数据从 FIFO 拷到内存。
DMA 就不一样了。Direct Memory Access,相当于你雇了个专门的搬运工。CPU 只需要告诉这个搬运工:“喂,帮我把这 1KB 数据从 SPI 接口搬到内存 0x8000_0000 地址去。”然后 CPU 就可以去干别的了。搬运工干完活,再给你打个中断说“搞定了”。
嗯,这里要注意,中断在 DMA 里只是辅助角色,不再是数据传输的主力了。
一句话总结:
- PIO: CPU 搬运数据,中断通知 CPU 有数据需要搬运。
- DMA: DMA 控制器搬运数据,中断通知 CPU 搬完了。
我曾经在调试一个高速 ADC 采集卡时,就遇到过这种场景。采样率 100Msps,12-bit 分辨率,数据量巨大。如果用 PIO,CPU 会被中断完全淹没。那不是我想要的系统,那是中断风暴。
2.2 适用场景分析
是不是 DMA 一定就比 PIO 好?不一定。
DMA 是有开销的。你想想看,配置 DMA 描述符、初始化通道、处理链表的开销,有时候比数据传输本身还要大。对于那种小数据量、低频次的操作,比如配置寄存器、读取设备状态,用 DMA 反而会引入不必要的延迟。
看下面的表格,这是我自己的经验总结:
| 特性 | PIO (中断驱动) | DMA |
|---|---|---|
| 单次传输量 | 适合小数据量(通常 < 64 Bytes) | 适合大数据量(通常 > 256 Bytes) |
| CPU 占用率 | 高,整个传输过程 CPU 被绑定 | 低,只在初始化配置和完成处理时占用少量 CPU |
| 响应延迟 | 较低,中断响应后立即处理 | 相对较高,需要先等待 DMA 描述符配置完成 |
| 系统实时性 | 容易被高频率中断破坏实时性 | 批量处理,减少中断次数,有利于实时性 |
| 硬件复杂度 | 低,几乎不需要额外硬件支持 | 高,需要专门的 DMA 控制器 |
我的建议:
如果你发现 CPU 在数据传输这件事上花费了超过 30% 的时间,那就该考虑用 DMA 了。反之,如果只是偶尔传输几个字节的控制信息,别折腾 DMA 了,PIO 就足够了。
2.3 性能量化对比
光说不练假把式。我们来做一些量化对比。假设我们有一个 SPI 接口,时钟频率 50MHz,每次传输 8 位数据,需要连续传输 1KB 的数据。
PIO 模式下的开销:
- CPU 接收中断(入栈、保护现场):约 500ns
- CPU 读取数据寄存器(从 SPI FIFO 读取):约 10ns
- CPU 写入内存(将数据存到指定 Buffer):约 10ns
- CPU 退出中断(恢复现场、出栈):约 300ns
- 总共每次中断开销:约 820ns
- 总共需要中断次数:1024 次
- CPU 总占用时间:820ns * 1024 ≈ 840μs
DMA 模式下的开销:
- CPU 初始化 DMA 描述符(配置源地址、目的地址、长度):约 2μs
- DMA 控制器自动搬运数据(硬件流控,不占用 CPU):0s
- DMA 传输完成,发送中断:约 500ns
- CPU 处理完成中断:约 300ns
- CPU 总占用时间:2μs + 500ns + 300ns ≈ 2.8μs
关键结论:
在这个例子中,PIO 占用了 CPU 约 840μs,而 DMA 只占用了约 2.8μs。相差了将近 300 倍。这就是为什么在高吞吐场景下,DMA 是不可替代的。
你想想看,如果系统里同时有多个高速外设(比如 Ethernet、USB、Display),全部用 PIO,CPU 就干脆不要干别的了。
下面是 PIO 和 DMA 在传输流程上的直观对比:
2.4 避坑指南:看似简单的 DMA,其实有陷阱
DMA 虽好,但用起来要小心。我在一些项目里见过不少因为 DMA 使用不当导致的诡异 bug。
避坑 1:Cache Coherence(缓存一致性)
我曾经在调试一个 Video 采集驱动时,发现图像数据花屏。查了三天,最后发现是 DMA 直接写入内存,但 CPU 的 Cache 里还存着旧数据。CPU 以为自己读取的是新数据,实际上是从 Cache 里读的垃圾。
解决办法:在 DMA 传输完成后,对 DMA Buffer 所在的地址范围做 Cache Flush 或 Cache Invalidate 操作。
避坑 2:Descriptor 的 Ownership(所有权)
DMA 控制器和 CPU 之间通过描述符链表通信。你得确保,当 CPU 在修改描述符时,DMA 控制器不会去读它。反之亦然。我见过有人忘记等待 DMA 完成就释放了描述符内存,结果 DMA 写了一个已经释放的地址,直接导致系统 Panic。
我的建议是,使用 Desc 中的 Done 位或者 Owner 位来做硬件握手。
避坑 3:Scatter-Gather 列表的边界条件
如果你的 DMA 支持 Scatter-Gather(分散-聚集),记得处理最后一个链表的 Next Pointer 一定是 NULL 或者指向一个 Terminate 节点。否则 DMA 会一直跑下去,把你内存里的数据全部写穿。
嗯,这个问题我在 Zynq 平台上碰到过,至今记忆犹新。
说白了,PIO 和 DMA 没有的优劣,只有适用场景的不同。PIO 简单、直接、低延迟,适合控制类消息;DMA 高效、批量、低 CPU 占用,适合大数据流。理解这些本质差异,你才能在项目中做出最优的选择。