2. DMA控制器硬件结构:通道、描述符、FIFO、总线接口

聊DMA中断延迟和吞吐率,绕不开它底层的硬件骨架。你想想看,一个DMA控制器在芯片里到底长什么样?通道、描述符、FIFO、总线接口,这四个块就是它的四大件。我今天把这四个东西拆开揉碎讲,每个部分都结合我踩过的坑来说。

2.1 通道(Channel)——DMA的并行流水线

通道是DMA控制器的基本工作单元。每个通道独立管理一次数据传输的上下文:源地址、目的地址、传输长度、传输模式、中断配置等。说白了,一个通道就是一条独立的数据搬运动线。

我早期调试一个多通道DMA时遇到过一个怪事:两个通道同时搬数据,结果A通道搬出来的数据里偶尔混着B通道的内容。查到最后,发现是硬件设计没把每个通道的地址寄存器独立锁存,共享总线时发生了竞争。后来我在芯片选型时一定会确认:每个通道是不是有独立的寄存器组,而不是靠软件反复配置。

现代高性能DMA控制器通常支持8~32个通道。通道数越多,你可以挂的外设就越多,带宽调度也越灵活。但要注意,通道多了,中断处理的开销也会翻倍——这就是后面我们会重点讲的“中断风暴”问题。

特性 单通道 多通道
并行传输一次只能搬一路可同时搬多路
硬件资源寄存器少、面积小寄存器多、面积大
中断频率低高(需合理调度)
适用场景简单外设(UART、SPI)高速外设(GPU、以太网)
经验之谈:嵌入式项目中,建议把频繁中断的通道(比如高吞吐SPI)分配到高优先级中断线,把批量搬数据的通道(比如从DDR搬数据到SRAM)用轮询或软件触发,避免它们抢CPU。

2.2 描述符(Descriptor)——传输的“剧本”

描述符是DMA传输的控制结构体。它是一个存放在内存里的数据结构,告诉DMA“从哪搬、搬多少、搬到哪、搬完后干什么”。

常见的有两种组织形式:链表式和数组式。链表式用next指针把多个描述符串起来,DMA搬完一个自动加载下一个,不用CPU干预。数组式则固定一段连续内存,CPU按顺序填充,DMA按顺序执行。

我最喜欢用链表式描述符,尤其是在做动态数据流的时候。举个例子:摄像头每秒30帧,每帧数据到DDR里不同的缓冲区。我可以用一个链把所有的描述符串起来,最后一帧搬完再触发中断。这样一帧中断一次,而不是每次碎片中断一次,吞吐率能提不少。

// 典型的dma描述符结构体(以某款cortex-m dma为例)
struct dma_descriptor {
    uint32_t src_addr;        // 源地址
    uint32_t dst_addr;        // 目的地址
    uint32_t transfer_size;   // 本次传输字节数
    uint32_t control;         // 控制标志(中断使能、下一个描述符是否有效等)
    struct dma_descriptor *next; // 链表指针
};
曾经踩过的坑:描述符链的最后一环,next指针一定要设为NULL或者硬件定义的终止值,否则DMA会死循环。我之前有一次忘了清空next,结果芯片跑起来之后DMA一直在搬同一块数据,把系统直接卡死了,排查了整整半天。

2.3 FIFO(先进先出缓冲区)——数据缓冲的“蓄水池”

FIFO是DMA内部的一个小容量SRAM缓冲,用来平滑外设速度和总线速度之间的差异。外设写入FIFO,DMA再从FIFO读出并搬到内存,或者反过来。

为什么需要FIFO?举个例子:一个UART波特率只有115200bps,但DMA总线时钟可能高达几百MHz。没有FIFO,UART每次只能发一个字节,DMA却要频繁响应总线请求,效率极低。有了FIFO,UART可以等FIFO满了再触发一次DMA,大幅减少中断次数。

FIFO深度很重要。深度16字节够UART用,但如果是高速外设比如摄像头,至少256字节起步。深度太浅容易导致“欠载”——外设写太快,FIFO满了又没及时被DMA读走,丢数据。

我记得有次调试一个ADC高速采集,FIFO深度只有8字节,采样率一高就丢数。后来我做了两件事:一是把DMA通道优先级提高,二是把FIFO阈值从“半满”改为“全满”才触发DMA请求。丢数问题解决了,但DMA延迟却变大了。这就是典型的trade-off——延迟和吞吐率,你总得做个平衡。

2.4 总线接口(Bus Interface)——数据流动的“高速路”

DMA通过总线接口与系统总线(如AXI、AHB、OCP)相连。总线接口负责仲裁、地址译码、握手协议。性能瓶颈往往就在这里——总线宽度、突发传输长度、时钟频率,每一项都直接影响吞吐率。

总线宽度最简单粗暴:64位总线比32位总线一次能多搬一倍数据。突发传输则更聪明:DMA可以连续发出多个请求,总线一次性返回多个数据节拍,不用每拍都重新仲裁。现代DMA控制器普遍支持AXI 64/128位总线,突发长度可达到16个节拍甚至更高。

我在一个视频采集项目里吃过亏:板子用的是AHB总线,DMA突发长度只能设4。我一开始没注意,直接设了最大突发,结果总线仲裁器忙不过来,DMA请求经常被阻塞,导致帧率不足。后来我把突发长度降为2,同时让DMA通道分时占用总线,反而把帧率提了上来。想想挺有意思,不是越暴力越好。

核心指标:总线吞吐率 = 总线宽度 × 时钟频率 × 总线利用率。利用率受仲裁、等待、协议开销等影响,实际能做到理论值的60%~80%已经不错了。

2.5 整体结构与硬件的协同

讲完了四个部件,我们把它们拼起来:CPU把描述符链放在内存里,写入DMA通道的基地址寄存器,DMA控制器通过通道读取描述符,然后通过总线接口从源地址读数据,经过FIFO缓冲,再写到目的地址。每搬完一个描述符,DMA自动加载下一个,直到链尾后产生中断。

这就是DMA的典型工作流。下面这张图把四个部件的关系画出来了,你在调试时可以把它贴在桌上当地图。

DMA控制器硬件结构(四大组件关系) CPU / 内存(描述符) 通道(Channel) FIFO 缓冲 总线接口(Bus I/F) 描述符链 (src/dst/len/next) 外设(Peripheral) 加载描述符 链表链接 (内存→通道) 数据交换 说明: CPU写入描述符链到内存; 通道自动取描述符,经FIFO缓冲, 由总线接口与外设交换数据。

这张图里我特意加了虚线箭头表示描述符链的链接关系,以及实线箭头表示数据流向。你排查中断延迟或吞吐率问题时,可以沿着这个结构一步步找瓶颈:

  • 通道太多导致中断频繁?考虑链式描述符减少中断次数。
  • FIFO深度不足丢数据?加深度或调阈值。
  • 总线突发太长导致仲裁阻塞?减突发长度或换更宽总线。
  • 描述符结构体读写太慢?尝试把描述符放在紧耦合内存(TCM)里。
核心一句话:DMA性能调优,本质就是调和通道、描述符、FIFO、总线接口四者之间的矛盾。你理解透了这四块,后面的中断延迟和吞吐率优化才有抓手。