4、DMA引擎设计(上):DMA基本原理、DMA传输类型、DMA描述符链、DMA控制器架构设计
4.1 为什么需要DMA?——从一次“中断风暴”说起
我记得刚入行那会儿,接手了一个高速数据采集项目。CPU主频1GHz,外设ADC每秒产生100万个采样点。我一开始用中断方式,每个采样点来一次中断,CPU进去搬数据。结果呢?CPU 90%的时间都在响应中断和搬数据,主程序几乎跑不动。
这就是典型的“中断风暴”。说白了,CPU被外设牵着鼻子走,根本没时间干正事。
DMA(Direct Memory Access)就是来解决这个问题的。它让数据搬运这件事,从CPU手里解放出来,交给专门的硬件去做。CPU只需要告诉DMA:“从外设A搬1000个字节到内存B”,然后就可以去忙别的了。DMA搬完了,再给CPU发个中断说“搞定了”。
4.2 DMA传输类型——三种最常见的场景
在实际项目中,我遇到过三种最常见的DMA传输场景。每种场景的硬件设计侧重点都不一样。
4.2.1 内存到内存(Memory-to-Memory)
这种场景最简单。比如你要把一个大数组从地址A拷贝到地址B。用CPU的memcpy当然可以,但数据量一大,CPU就卡住了。
DMA做这件事,只需要配置源地址、目的地址、传输长度,然后启动。DMA会自己产生读总线和写总线的时序,把数据搬过去。
4.2.2 外设到内存(Peripheral-to-Memory)
这是最常用的场景。比如UART接收数据、SPI读取传感器、ADC采集波形。外设每产生一个数据,DMA就自动把它搬到内存缓冲区里。
这里有个关键点:外设的“请求信号”。外设准备好数据后,会拉高一个硬件信号(比如DMA_REQ),DMA看到这个信号,就执行一次传输。传输完,信号自动拉低。
我曾经踩过一个坑:外设的请求信号是电平触发,结果DMA一次传输完成后,信号还没拉低,DMA又触发了一次,导致数据重复。后来改成边沿触发,问题解决。
4.2.3 内存到外设(Memory-to-Peripheral)
反过来,CPU要往外设发数据,比如通过SPI发送波形数据、通过DAC输出音频。CPU只需要把数据准备好,DMA会自动从内存取数据,送到外设的发送寄存器。
这种场景下,外设的“就绪信号”同样重要。外设说“我准备好了,可以发下一个数据”,DMA才去取数据。否则数据会丢失。
| 传输类型 | 源端 | 目的端 | 触发方式 | 典型应用 |
|---|---|---|---|---|
| 内存到内存 | 内存 | 内存 | 软件启动 | 数据拷贝、图像处理 |
| 外设到内存 | 外设寄存器 | 内存 | 外设请求 | UART接收、ADC采集 |
| 内存到外设 | 内存 | 外设寄存器 | 外设请求 | SPI发送、DAC输出 |
4.3 DMA描述符链——让DMA自己“找活干”
你想想看,如果每次DMA传输完一个数据块,CPU都要重新配置一次DMA寄存器,那跟中断方式有什么区别?
所以,硬件工程师发明了“描述符链”(Descriptor Chain)。
描述符本质上是一个数据结构,放在内存里。它包含了:
- 源地址指针
- 目的地址指针
- 传输长度
- 控制标志(比如传输完是否发中断)
- 下一个描述符的指针(关键!)
DMA控制器只需要知道第一个描述符的地址。它处理完第一个描述符后,自动读取“下一个描述符指针”,找到第二个任务,继续执行。直到最后一个描述符的“下一个指针”为NULL,DMA才停下来发中断。
我在一个视频处理项目中,用描述符链实现了“乒乓缓冲”。描述符A指向缓冲区1,描述符B指向缓冲区2。DMA先处理A,把数据搬到缓冲区1;同时CPU处理缓冲区2的数据。处理完A后,DMA自动跳到B,把数据搬到缓冲区2;CPU处理缓冲区1。如此循环,数据流从不间断。
4.4 DMA控制器架构设计——从寄存器到状态机
好了,理论讲完了。咱们来看看DMA控制器内部到底长什么样。
4.4.1 核心模块
一个典型的DMA控制器,包含以下几个关键模块:
- 控制寄存器组: 存放源地址、目的地址、传输长度、控制字等。CPU通过总线访问这些寄存器。
- 状态机(FSM): DMA的大脑。负责解析描述符、发起总线请求、处理传输完成等。
- 总线接口单元: 负责与系统总线(如AHB、AXI)交互,发起读/写传输。
- 描述符读取单元: 专门从内存读取描述符数据,解析后加载到控制寄存器。
- 中断控制器: 传输完成或出错时,向CPU发送中断。
4.4.2 状态机设计要点
状态机是DMA的灵魂。我一般设计成这几个状态:
- IDLE: 等待CPU启动或外设请求。
- READ_DESC: 从内存读取描述符。如果描述符链为空,回到IDLE。
- READ_DATA: 从源地址读取数据。如果是外设到内存,等待外设请求信号。
- WRITE_DATA: 将数据写入目的地址。
- UPDATE_DESC: 更新描述符状态(比如已传输字节数),然后跳转到READ_DESC读取下一个。
- DONE: 发送中断,回到IDLE。
4.4.3 架构框图
下面我用一张SVG图,把DMA控制器的整体架构画出来。你可以看到数据流和控制流的走向。
4.5 设计中的几个关键考量
最后,分享几个我在实际项目中总结的经验:
- 描述符放在哪里? 我建议放在专用的SRAM里,而不是系统主存。因为DMA读描述符时,如果和主存的其他访问冲突,会影响性能。专用SRAM可以保证描述符读取的确定性延迟。
- 传输粒度怎么选? 对于外设到内存,传输粒度最好和外设的数据宽度一致。比如UART一次发1字节,DMA就设成8位传输。如果设成32位,会浪费带宽。
- 中断频率控制: 描述符链的最后一个描述符才发中断,中间的不发。这样可以减少CPU的负担。我见过有人每个描述符都发中断,结果CPU又被淹没了——这不又回到中断风暴了吗?
- 错误处理: 总线错误、地址对齐错误、描述符格式错误,这些都要有对应的处理机制。我的做法是:错误发生时,DMA立即停止,把错误状态写入寄存器,然后发一个“错误中断”给CPU。CPU读取错误寄存器,决定是重试还是放弃。
好了,这一章的内容就到这里。DMA的基本原理、传输类型、描述符链和控制器架构,都是后续章节的基础。下一章我们会深入DMA的调度算法和性能优化,到时候再聊。
公众号:蓝海资料掘金营,微信deep3321