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发个中断说“搞定了”。

核心思想: DMA的本质是“数据搬运的硬件加速器”。它不处理数据,只负责把数据从一个地址搬到另一个地址。

4.2 DMA传输类型——三种最常见的场景

在实际项目中,我遇到过三种最常见的DMA传输场景。每种场景的硬件设计侧重点都不一样。

4.2.1 内存到内存(Memory-to-Memory)

这种场景最简单。比如你要把一个大数组从地址A拷贝到地址B。用CPU的memcpy当然可以,但数据量一大,CPU就卡住了。

DMA做这件事,只需要配置源地址、目的地址、传输长度,然后启动。DMA会自己产生读总线和写总线的时序,把数据搬过去。

我的经验: 内存到内存传输,带宽利用率是关键。我曾经遇到一个设计,DMA每次只搬4字节,结果总线利用率不到10%。后来改成每次搬64字节(一个cache line大小),带宽直接拉满。

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才停下来发中断。

描述符链的好处: CPU可以一次性准备好几十个甚至几百个传输任务,然后告诉DMA“开工”。DMA自己一个一个执行,CPU完全不用管。这就是“批处理”的思想。

我在一个视频处理项目中,用描述符链实现了“乒乓缓冲”。描述符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的灵魂。我一般设计成这几个状态:

  1. IDLE: 等待CPU启动或外设请求。
  2. READ_DESC: 从内存读取描述符。如果描述符链为空,回到IDLE。
  3. READ_DATA: 从源地址读取数据。如果是外设到内存,等待外设请求信号。
  4. WRITE_DATA: 将数据写入目的地址。
  5. UPDATE_DESC: 更新描述符状态(比如已传输字节数),然后跳转到READ_DESC读取下一个。
  6. DONE: 发送中断,回到IDLE。
注意: 状态机设计时,一定要考虑“总线死锁”的情况。比如DMA正在读内存,但内存控制器正在等DMA释放总线。这种循环依赖会导致系统卡死。我的做法是:在状态机里加一个超时计数器,如果总线请求超过一定时间没响应,就主动放弃并报错。

4.4.3 架构框图

下面我用一张SVG图,把DMA控制器的整体架构画出来。你可以看到数据流和控制流的走向。

DMA控制器架构框图 CPU 系统总线(AHB/AXI) DMA控制器 控制寄存器组 状态机(FSM) 总线接口单元 描述符读取单元 中断控制器 外设 内存 图例 控制流 数据流 总线连接

4.5 设计中的几个关键考量

最后,分享几个我在实际项目中总结的经验:

  • 描述符放在哪里? 我建议放在专用的SRAM里,而不是系统主存。因为DMA读描述符时,如果和主存的其他访问冲突,会影响性能。专用SRAM可以保证描述符读取的确定性延迟。
  • 传输粒度怎么选? 对于外设到内存,传输粒度最好和外设的数据宽度一致。比如UART一次发1字节,DMA就设成8位传输。如果设成32位,会浪费带宽。
  • 中断频率控制: 描述符链的最后一个描述符才发中断,中间的不发。这样可以减少CPU的负担。我见过有人每个描述符都发中断,结果CPU又被淹没了——这不又回到中断风暴了吗?
  • 错误处理: 总线错误、地址对齐错误、描述符格式错误,这些都要有对应的处理机制。我的做法是:错误发生时,DMA立即停止,把错误状态写入寄存器,然后发一个“错误中断”给CPU。CPU读取错误寄存器,决定是重试还是放弃。
一个小技巧: 调试DMA时,可以在描述符里加一个“调试标志位”。DMA处理完每个描述符后,把当前状态写入一个环形缓冲区。这样即使出错了,也能回放DMA的执行轨迹,快速定位问题。

好了,这一章的内容就到这里。DMA的基本原理、传输类型、描述符链和控制器架构,都是后续章节的基础。下一章我们会深入DMA的调度算法和性能优化,到时候再聊。


公众号:蓝海资料掘金营,微信deep3321