2. DMA原理与分类:DMA基本概念、DMA传输模式、DMA与CPU交互机制
各位同学,咱们今天聊聊DMA。说实话,DMA这个概念在嵌入式开发里太重要了。我刚开始做PCIe驱动那会儿,对DMA的理解也就停留在“能搬数据”这个层面。直到有一次项目里遇到了严重的性能瓶颈,我才真正去深挖它的原理。
DMA,全称Direct Memory Access,直接存储器访问。说白了,就是让外设和内存之间能直接交换数据,不需要CPU一字节一字节地去搬。你想想看,如果没有DMA,CPU得亲自下场做搬运工,那它还有精力去处理更重要的任务吗?
2.1 DMA基本概念
DMA的核心思想就一句话:解放CPU。CPU只需要告诉DMA控制器“从哪搬、搬到哪、搬多少”,然后就可以去干别的事了。搬数据这种脏活累活,交给DMA去干。
我给大家拆解一下DMA的几个关键要素:
- DMA控制器:负责管理数据传输的硬件模块。在PCIe系统中,它通常集成在Endpoint设备里,或者位于Root Complex中。
- 源地址:数据从哪来。可能是外设的FIFO,也可能是系统内存的某个地址。
- 目的地址:数据要去哪。同样,可能是内存,也可能是外设。
- 传输长度:要搬多少数据。单位通常是字节。
- 传输方向:读还是写?从外设到内存叫DMA读,从内存到外设叫DMA写。
重要概念:在PCIe语境下,DMA传输实际上是通过Memory Read/Write TLP(事务层包)来完成的。DMA控制器会生成这些TLP,然后通过PCIe链路发送出去。这一点和普通的CPU发起的MMIO访问有本质区别。
嗯,这里要注意一个容易混淆的点:DMA传输和PIO传输的区别。PIO是CPU亲自读写IO端口或MMIO寄存器来传输数据,而DMA是硬件自动完成。我见过不少新手工程师,在需要高速传输的场景下还在用PIO,结果CPU占用率直接飙到100%。
2.2 DMA传输模式
DMA的传输模式,我个人习惯把它分成两大类:块传输和描述符链传输。这两种模式各有各的适用场景。
2.2.1 块传输(Block Transfer)
块传输是最简单的DMA模式。你给它一个源地址、一个目的地址、一个长度,它就一次性把数据搬完。搬完之后,它会发一个中断告诉CPU:“活干完了”。
这种模式的优点是实现简单,控制逻辑清晰。缺点也很明显:一次只能处理一块连续的数据。如果数据在内存里是分散的,那就麻烦了。
我记得在做一个数据采集卡的项目时,一开始用的就是块传输。每次采集完一批数据,DMA就中断一次CPU。数据量小的时候还好,一旦数据量大了,CPU频繁被中断,系统性能直线下降。后来我换成了描述符链传输,问题才解决。
小技巧:块传输适合数据量不大、传输频率不高的场景。比如配置寄存器、读取少量状态信息等。如果你需要高速、大批量传输数据,建议直接上描述符链。
2.2.2 描述符链传输(Descriptor Chain Transfer)
描述符链传输,也叫Scatter-Gather DMA。它的核心思想是:用一组描述符(Descriptor)来定义多个传输任务,这些描述符通过链表的形式串联起来。DMA控制器会依次处理链表上的每个描述符,处理完一个自动跳到下一个。
每个描述符通常包含以下信息:
| 字段 | 说明 |
|---|---|
| 源地址 | 数据源的内存地址或外设地址 |
| 目的地址 | 数据目标的内存地址 |
| 传输长度 | 本次传输的字节数 |
| 下一个描述符指针 | 指向链表中下一个描述符的地址 |
| 控制标志 | 如中断使能、链结束标志等 |
描述符链传输的好处是显而易见的:
- 支持非连续内存:你可以把一块物理上不连续的内存,通过描述符链组织成逻辑上连续的传输。
- 减少CPU干预:CPU只需要设置好描述符链,然后启动DMA,剩下的就交给硬件了。
- 提高传输效率:避免了频繁中断,DMA可以连续工作。
我曾经在一个项目中遇到过一个问题:系统内存碎片化严重,无法分配大块连续的DMA缓冲区。当时我用了描述符链传输,把多个小块内存通过描述符串起来,完美解决了问题。嗯,这就是Scatter-Gather的魅力所在。
避坑指南:描述符链传输虽然强大,但实现起来比块传输复杂。你需要管理描述符的内存分配、链表的维护、以及处理各种边界情况。我曾经因为描述符链的最后一个描述符没有正确设置链结束标志,导致DMA一直在空转,查了整整两天才找到原因。
2.3 DMA与CPU交互机制
DMA和CPU之间怎么配合?说白了,就是一套“你干活,我休息;你干完,叫我”的机制。主要有以下几种交互方式:
2.3.1 中断机制
这是最常用的方式。DMA传输完成后,会向CPU发送一个中断信号。CPU收到中断后,在中断处理函数里检查传输状态、处理数据。
中断机制的优点是响应及时,缺点是频繁中断会消耗CPU资源。对于高速DMA,我建议使用中断合并(Interrupt Coalescing)技术,也就是攒够一定数量的传输完成后再发一次中断。
2.3.2 轮询机制
CPU不去睡觉,而是不停地检查DMA的状态寄存器,看看传输是否完成。这种方式在低延迟场景下有用,但CPU会被完全占住。
我个人很少用纯轮询,除非是调试阶段或者对延迟要求极其苛刻的场景。你想想看,CPU一直在那转圈等,多浪费啊。
2.3.3 硬件信号量
有些高级的DMA控制器支持硬件信号量机制。CPU和DMA通过共享的信号量来同步。比如,CPU写一个描述符到链表后,通过写信号量来通知DMA“有新任务了”。DMA处理完一个描述符后,也会更新信号量来告诉CPU“这个任务完成了”。
这种机制减少了中断次数,适合高性能场景。我在做NVMe驱动时就用过这种机制,效果很不错。
核心要点:DMA与CPU的交互,本质上是一个生产者-消费者模型。CPU是生产者(生成描述符),DMA是消费者(执行传输),或者反过来。理解了这个模型,你就能更好地设计驱动中的同步逻辑。
好了,关于DMA的原理和分类,我就讲这么多。下一节我们会深入PCIe DMA的具体实现,包括如何配置DMA引擎、如何处理描述符环等。到时候我会结合实际的代码示例来讲解,敬请期待。