3. 深入DMA控制器内部结构

做DMA优化的人,要是连控制器内部长啥样都不清楚,那后续的描述符链循环优化基本就是在「盲开」。我个人习惯先看数据手册里的框图,再拿着逻辑分析仪抓实际波形。今天咱们就把DMA控制器的内部结构拆开揉碎了讲。

3.1 核心组成模块

一个典型的DMA控制器,说白了就是几个关键部件的组合。我总结为四大块:通道引擎、描述符管理器、地址生成器和总线接口。

模块 功能简述 我遇到过的坑
通道引擎 管理多个DMA通道,处理请求仲裁 某次通道优先级配错,导致摄像头数据丢帧
描述符管理器 解析描述符、维护链表、控制传输 描述符地址未对齐,直接卡死整个DMA
地址生成器 根据描述符配置生成源/目的地址及增量 地址回绕没处理好,数据写到了错误的内存区域
总线接口 读写AHB/AXI总线,处理握手与突发传输 突发长度超限,总线直接回复错误响应

嗯,这里要注意:有些厂商把描述符管理器嵌入通道引擎里,有些则是独立模块。你得先搞清楚具体芯片的设计。

3.2 通道引擎的仲裁机制

通道多了,谁先谁后?仲裁器决定。常见的有固定优先级和轮询(Round-Robin)两种。

  • 固定优先级:通道0永远最高。适合实时性要求严格的外设,比如音频I2S。
  • 轮询:每个通道轮流获得总线控制权。适合多路并发但都不紧急的场景,比如后台数据搬移。

我记得有一次调试一个四路SPI采集系统,用了轮询模式,结果每路采样率都能稳定达到2Msps。换成固定优先级后,低优先级通道的数据滞后越来越严重。所以,仲裁方式不能拍脑袋选。

个人建议:如果系统里只有一路高优先级(比如显示刷新),其他全是低优先级后台任务,用固定优先级反而简单高效。反之一堆同等级任务,轮询更公平。

3.3 描述符管理器的内部状态机

描述符管理器是个有限状态机,典型的状态流转如下:

  1. 空闲:等待软件写使能位触发传输。
  2. 获取描述符:从链表中读取下一个描述符到内部缓存。
  3. 解析配置:提取源、目的地址、传输长度、突发配置等参数。
  4. 执行传输:驱动地址生成器和总线接口搬数据。
  5. 完成处理:更新状态位、触发中断、准备下一个描述符。
  6. 循环检查:若描述符设置了链循环标志,则回到状态2;否则进入空闲。

你想想看,为什么状态机里没有「错误处理」?因为大多数DMA会在状态2和状态3里检测非法地址或描述符格式错误,然后直接跑到一个异常状态。我曾在项目里看到描述符的next指针指向未初始化的内存,结果管理器死循环在那里。后来加了超时检测才解决问题。

避坑指南:描述符管理器的内部缓存很小(通常只缓存当前描述符),如果你在中断里修改链表,必须保证修改发生在管理器读取下一个描述符之前。我曾经在中断里改链表,却被管理器提前读走了旧数据——那个bug找了整整两天。

3.4 地址生成器与总线接口的配合

地址生成器不简单。它要根据描述符里的地址增量配置决定每次传输后地址加多少。常见的有:固定地址(一维数组)、递增地址(内存)、或根据二维/三维传输计算偏移。

总线接口则负责把地址和命令发送到系统总线。这里有个关键指标——突发能力(Burst)。拿AXI总线举例,DMA可以发出INCR4、WRAP8等突发命令。一次突发能完成多个数据节拍,大大减少总线握手开销。

突发类型 典型应用场景 效率提升
单次传输 寄存器访问 基准
INCR突发 内存到内存搬移 3~5倍
WRAP突发 Cache Line填充、FIFO回绕 更高(取决于对齐)

但要注意:突发长度受总线位宽和硬件限制。比如32位总线下,INCR16一次传输16个32位字,那就是64字节。如果源地址或目的地址没对齐到边界,总线可能自动降级为INCR1,效率瞬间暴跌。我之前做过一个视频处理项目,就因为源地址没按16字节对齐,DMA吞吐率直接砍半。

3.5 内部结构逻辑关系(SVG)

下面我把DMA控制器的核心结构画成一张框图。你看一眼就能明白模块之间怎么绑在一起。

DMA控制器内部结构框图 DMA控制器内部结构图 外设请求输入 通道引擎 仲裁·状态机·通道控制 描述符管理器 解析·链表维护·完成处理 地址生成器 增量·回绕·二维偏移 总线接口 AHB/AXI·突发控制 内存 / 外设 系统总线上的目标 描述符链 存储在内存中

看图不难发现:通道引擎是调度中心,描述符管理器从内存读取描述符链,把参数喂给地址生成器,最后通过总线接口去读写实际的目标。整个流程就像一条流水线。如果你能把这个流水线每一站的延迟都搞清楚,优化起来就有的放矢了。

3.6 内部寄存器的分类与访问

DMA控制器内部还有一堆寄存器,我习惯把它们分成三类:

  • 全局配置寄存器:使能、优先级模式、中断汇总等。读写频率极低,初始化一次就行。
  • 通道控制寄存器:通道使能、传输类型、描述符地址指针等。每个通道独立,软件在启动前配置。
  • 状态寄存器:完成的传输计数、错误标志、是否忙等等。中断处理时读取,用来判断下一个动作。

很多人忽略了一点——寄存器的访问时序。比如某些DMA要求写描述符地址寄存器之前,必须先使能通道,否则写入无效。我踩过这个坑,后来干脆写了一个严格的初始化顺序模板。

小技巧:在调试阶段,每次启动DMA后轮询状态寄存器,看EXPECTED_BUSY位是否按预期翻转。如果状态一直卡死在IDLE,八成是描述符地址写错或者总线访问没通过。

3.7 不同架构DMA的差异

最后说一句:不同厂商的DMA内部结构差异挺大。比如STM32的DMA支持双缓冲和循环模式,但描述符链需要手动维护。而Xilinx的AXI DMA有完整的描述符链管理硬件,你只需要写好链表就能自动循环。我个人建议,拿到一个新芯片,先看它的描述符管理器是否支持硬件预取下一个描述符。如果不支持,软件必须在当前传输结束前就把下一个描述符地址写进去,否则就会出现「断流」。