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 模式下的开销:

  1. CPU 接收中断(入栈、保护现场):约 500ns
  2. CPU 读取数据寄存器(从 SPI FIFO 读取):约 10ns
  3. CPU 写入内存(将数据存到指定 Buffer):约 10ns
  4. CPU 退出中断(恢复现场、出栈):约 300ns
  5. 总共每次中断开销:约 820ns
  6. 总共需要中断次数:1024 次
  7. CPU 总占用时间:820ns * 1024 ≈ 840μs

DMA 模式下的开销:

  1. CPU 初始化 DMA 描述符(配置源地址、目的地址、长度):约 2μs
  2. DMA 控制器自动搬运数据(硬件流控,不占用 CPU):0s
  3. DMA 传输完成,发送中断:约 500ns
  4. CPU 处理完成中断:约 300ns
  5. 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 在传输流程上的直观对比:

PIO 与 DMA 传输流程对比 PIO 传输模式 DMA 传输模式 CPU 发起读请求 CPU 等待中断/轮询标志 CPU 从 FIFO 拷数据到内存 数据 传完? 未完成,继续等待 完成 CPU 配置 DMA 描述符 DMA 控制器自动搬运数据 CPU 并行处理其他任务 DMA 发送完成中断 CPU 处理完成数据 说明:DMA 模式下,数据搬运期间 CPU 可以释放出来执行其他任务,这是两者效率差异的根本来源。

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 占用,适合大数据流。理解这些本质差异,你才能在项目中做出最优的选择。