3. DMA控制器基础:DMA工作原理、SG列表、循环模式、中断与轮询
DMA,全称 Direct Memory Access,直接存储器访问。说白了,就是让数据绕过 CPU,直接在内存和外设之间搬砖。CPU 只需要告诉 DMA:「从哪搬,搬到哪,搬多少」,然后就可以去干别的事了。
我刚开始做嵌入式时,总觉得 DMA 是个黑盒子。后来调一个高速 ADC 采集的项目,CPU 被中断淹没了,系统几乎卡死。嗯,从那以后我才真正重视 DMA。这一章,我就把 DMA 的核心机制掰开揉碎讲给你听。
3.1 DMA 工作原理:一次完整的搬运
一个典型的 DMA 传输,包含三个要素:
- 源地址:数据从哪来(比如 UART 接收寄存器)
- 目的地址:数据搬到哪(比如内存缓冲区)
- 传输长度:搬多少字节
DMA 控制器内部有专门的地址寄存器和长度计数器。启动后,每个时钟周期搬一个单元,直到计数器归零。整个过程 CPU 零参与。
关键点:DMA 传输的单位可以是字节、半字(2字节)或字(4字节)。具体用哪种,要看外设寄存器的位宽。UART 通常按字节传输。
我画了一张 DMA 传输的简化流程图,帮你理解数据流向:
3.2 SG 列表:散列-收集的艺术
SG 列表,全称 Scatter-Gather List,散列-收集列表。为什么需要它?
你想想看,如果一次 DMA 传输只能搬一整块连续内存,那太死板了。实际项目中,数据可能分散在多个缓冲区里。比如网络协议栈,一个数据包的头和负载可能在不同的内存位置。
SG 列表就是用来解决这个问题的。它是一个描述符数组,每个描述符包含:
- 源地址指针
- 目的地址指针
- 传输长度
- 下一个描述符指针(链表结构)
DMA 控制器自动遍历这个链表,逐个完成每个描述符的传输。全部完成后,再通知 CPU。
我的经验:在 Linux 内核里,SG 列表用 struct scatterlist 表示。驱动开发中,申请 DMA 缓冲区时尽量用 dma_alloc_coherent 或 dma_map_sg,保证物理地址连续。我曾经踩过一个坑:用 kmalloc 分配的内存做 DMA,结果在高内存压力下物理页不连续,DMA 传着传着就崩了。
3.3 循环模式:永不停歇的数据流
循环模式,也叫环形缓冲区模式。DMA 传输完一轮后,自动回到起始地址重新开始。适合音频采集、UART 连续接收这类场景。
我举个例子:UART 接收 1024 字节的环形缓冲区。DMA 配置好源地址(UART 数据寄存器)、目的地址(缓冲区)、长度(1024)。启动后,DMA 从 0 开始写,写到 1023 后自动跳回 0。CPU 只需要定期检查缓冲区,取走数据。
| 特性 | 普通模式 | 循环模式 |
|---|---|---|
| 传输完成后地址 | 停在末尾 | 自动回到起始 |
| CPU 干预频率 | 每次传输完都要处理 | 只需处理缓冲区半满/全满 |
| 适用场景 | 单次数据块传输 | 持续数据流 |
| 中断开销 | 高 | 低 |
注意:循环模式下,CPU 和 DMA 同时访问同一个缓冲区,必须处理好读写指针的同步。我曾经见过一个产品,DMA 写速度比 CPU 读速度快,导致数据覆盖,音频出现爆音。解决方案是使用「半传输中断」和「全传输中断」双指针机制。
3.4 中断与轮询:两种通知方式
DMA 传输完成后,怎么通知 CPU?两种方式:中断和轮询。
3.4.1 中断方式
DMA 控制器在传输完成时,向 CPU 触发一个中断。CPU 暂停当前任务,进入中断服务程序处理数据。
- 优点:响应及时,CPU 利用率高
- 缺点:频繁中断会拖慢系统(中断风暴)
3.4.2 轮询方式
CPU 定期检查 DMA 的状态寄存器,看传输是否完成。
- 优点:实现简单,没有中断上下文切换开销
- 缺点:CPU 空转,浪费算力
我个人习惯:数据量小、频率低时用中断;数据量大、频率高时用轮询或混合模式。比如 UART 115200 波特率,每秒约 11.5KB,用中断完全没问题。但如果是高速 SPI 或摄像头数据流,我建议用轮询加 DMA 循环模式。
避坑指南:我曾经在一个项目里,UART 中断频率太高,导致系统响应变慢。后来我把中断改成「DMA 半满中断 + 轮询读取剩余数据」,问题解决了。具体做法是:DMA 配置为循环模式,设置半传输中断。每次中断处理一半缓冲区,另一半留给 DMA 继续写。这样中断频率降低了一半,CPU 有更多时间处理其他任务。