2. DMA控制器硬件结构:通道、描述符、FIFO、总线接口
聊DMA中断延迟和吞吐率,绕不开它底层的硬件骨架。你想想看,一个DMA控制器在芯片里到底长什么样?通道、描述符、FIFO、总线接口,这四个块就是它的四大件。我今天把这四个东西拆开揉碎讲,每个部分都结合我踩过的坑来说。
2.1 通道(Channel)——DMA的并行流水线
通道是DMA控制器的基本工作单元。每个通道独立管理一次数据传输的上下文:源地址、目的地址、传输长度、传输模式、中断配置等。说白了,一个通道就是一条独立的数据搬运动线。
我早期调试一个多通道DMA时遇到过一个怪事:两个通道同时搬数据,结果A通道搬出来的数据里偶尔混着B通道的内容。查到最后,发现是硬件设计没把每个通道的地址寄存器独立锁存,共享总线时发生了竞争。后来我在芯片选型时一定会确认:每个通道是不是有独立的寄存器组,而不是靠软件反复配置。
现代高性能DMA控制器通常支持8~32个通道。通道数越多,你可以挂的外设就越多,带宽调度也越灵活。但要注意,通道多了,中断处理的开销也会翻倍——这就是后面我们会重点讲的“中断风暴”问题。
| 特性 | 单通道 | 多通道 |
|---|---|---|
| 并行传输 | 一次只能搬一路 | 可同时搬多路 |
| 硬件资源 | 寄存器少、面积小 | 寄存器多、面积大 |
| 中断频率 | 低 | 高(需合理调度) |
| 适用场景 | 简单外设(UART、SPI) | 高速外设(GPU、以太网) |
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; // 链表指针
};
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通道分时占用总线,反而把帧率提了上来。想想挺有意思,不是越暴力越好。
2.5 整体结构与硬件的协同
讲完了四个部件,我们把它们拼起来:CPU把描述符链放在内存里,写入DMA通道的基地址寄存器,DMA控制器通过通道读取描述符,然后通过总线接口从源地址读数据,经过FIFO缓冲,再写到目的地址。每搬完一个描述符,DMA自动加载下一个,直到链尾后产生中断。
这就是DMA的典型工作流。下面这张图把四个部件的关系画出来了,你在调试时可以把它贴在桌上当地图。
这张图里我特意加了虚线箭头表示描述符链的链接关系,以及实线箭头表示数据流向。你排查中断延迟或吞吐率问题时,可以沿着这个结构一步步找瓶颈:
- 通道太多导致中断频繁?考虑链式描述符减少中断次数。
- FIFO深度不足丢数据?加深度或调阈值。
- 总线突发太长导致仲裁阻塞?减突发长度或换更宽总线。
- 描述符结构体读写太慢?尝试把描述符放在紧耦合内存(TCM)里。