第3章 硬件DMA控制器概述:常见厂商方案归纳

做DMA驱动,你不能只盯着Linux内核的dmaengine框架。硬件长什么样,决定了你的代码怎么写。这两年我带过的不少新人,上来就撸代码,结果配寄存器配出了奇葩现象——数据错位、中断丢失……说到底,是对DMA控制器的硬件行为没吃透。

这一章,我带你过一遍三家主流DMA控制器:Intel、ARM、Xilinx。都是我亲手调过的。你看完就知道它们各自的门道在哪。

3.1 先搭个认知框架

DMA控制器的作用说白了就四个字:搬数据,不占CPU。但不同厂商的玩法差异挺大:

  • Intel:偏重PC平台,走的PCIe总线,支持多通道、大描述符环、硬件排序。
  • ARM:嵌入式的命脉,挂载在AMBA总线上,强调低延迟、多通道、微码可编程。
  • Xilinx:可编程逻辑的宠儿,AXI接口,灵活配置,适合在FPGA里定制数据通路。

我个人习惯:拿到新板子第一件事,先把Datasheet里的DMA寄存器表打印出来,一边读一边画状态机。比你对着example code瞎调快三倍。

核心心法:所谓“多队列DMA驱动设计”,第一步就是理解硬件到底支持几个独立的描述符链、每个链能不能单独配置传输属性和中断。

3.2 Intel DMA控制器——从I/OAT到DMA Engine

Intel的DMA方案最早用在芯片组里的I/O加速技术(I/OAT),后来集成进CPU的Data Direct I/O。你如果写过x86平台的网卡驱动,肯定跟它的描述符环打过交道。

关键特性:

  • 支持多个逻辑通道(甚至可达几十个),每个通道有自己的描述符队列。
  • 描述符结构固定,硬件负责自动回读完成状态。
  • 内置硬件排序引擎,可以保证同一通道内的DMA操作按提交顺序完成。

我记得调试Intel I/OAT时遇到过一个大坑:描述符环地址没有64字节对齐,结果硬件一直报“描述符非法”。查了三天,最后发现是kmalloc分配的内存只对齐了32字节……从那以后,我对DMA内存对齐这事儿变得异常敏感。

3.3 ARM DMA控制器——PL330与DMA-330

ARM系DMA最常见的两块:PL330(低速外设用)和DMA-330(高性能系统用)。它们都挂在AMBA总线上,编程接口类似。

我建议你关注这点:ARM DMA的微码(Microcode)是可以替换的。不同SoC厂商可能会塞自己的自定义微码,导致寄存器偏移都不一样。你看树莓派和全志的DMA驱动,一个用PL330默认微码,另一个用了魔改版——同一个IP核,驱动代码能差出几百行。

避坑指南:我曾经在全志A64的产品上用过ARM PL330,跑memcpy测试一切正常。结果接上SPI外设后,DMA居然每次只传4字节就停了。后来查到是微码里的burst size配置跟外设FIFO深度不匹配。记住:ARM DMA的微码默认值未必适合你的应用,一定要对照SoC的Scatter/Gather表逐项核对。

3.4 Xilinx DMA控制器——AXI DMA与AXI CDMA

Xilinx的DMA属于FPGA里的软核IP。你用Vivado把它例化进来,可以随意调整通道数量、数据位宽、是否带描述符管理等。这种灵活性是一把双刃剑。

  • AXI DMA:适合简单的存储器到外设/外设到存储器传输,支持Scatter-Gather。
  • AXI CDMA:专为存储器到存储器设计,带内部缓存一致性控制。
  • AXI Multichannel DMA(MCDMA):真正支持多队列多通道,每个通道有独立描述符链。

你想想看:在FPGA里,DMA的时钟域可以和CPU不一样。我遇到过一个案例,AXI DMA工作在200MHz,而CPU总线只有150MHz,结果DMA每次完成中断传给CPU时,跨时钟域同步丢了几个脉冲,导致中断丢失。最后不得不在DMA驱动里加了个超时轮询——说起来都是泪。

3.5 各厂商DMA对比总览

下面这个表总结了我挑选DMA时最在意的几项指标。建议你收藏,做方案选型时直接翻出来对照。

维度 Intel I/OAT ARM PL330/DMA-330 Xilinx AXI DMA/MCDMA
总线 PCIe AMBA AHB/AXI AXI4
典型通道数 8~32 2~8 1~16(可配置)
描述符管理 硬件环形队列 微码+链表或环形 硬件链表(SG模式)
中断机制 每通道单独中断 全局中断+通道状态寄存器 每通道独立中断(可配置)
多队列原生支持 强(多通道独立描述符环) 弱(多数SoC只暴露单队列) 强(MCDMA有独立队列)
编程复杂度 中(需要了解PCIe BAR) 高(微码定制困难) 中(IP配置灵活,但寄存器组合多)

3.6 一张图看懂三大家DMA核心架构

下面这张SVG画的是这三类DMA控制器的核心数据通路与描述符管理方式。你对照着看,能快速理解它们的共性与差异。

常见DMA控制器核心架构对比 Intel I/OAT CPU侧:PCIe BAR访问 描述符环:硬件环形队列 多通道:每通道独立链 中断:MSI-X每通道 数据搬运:硬件排序 ARM PL330/DMA-330 CPU侧:AMBA寄存器 描述符:微码+SRAM链表 多通道:依赖于SoC实现 中断:全局INTR+通道状态 数据搬运:微码流水线 Xilinx AXI DMA/MCDMA CPU侧:AXI-Lite控制 描述符:硬件链表(SG) 多通道:MCDMA支持 中断:每通道可配置 数据搬运:AXI交叉互联 说明 三者共性:都支持硬件描述符链、DMA完成中断、多通道(程度不同)。 关键差异:Intel用PCIe + 硬件环形队列(适合PC生态);ARM用AMBA + 微码(适合SoC定制);Xilinx用AXI + 可配置IP核(适合FPGA场景)。 多队列能力:Intel原生强 > Xilinx MCDMA选配强 > ARM弱(取决于SoC封装)。 实践建议:先读对应厂商的DMA驱动源码(drivers/dma/),再回来看这张图

3.7 几个让你少走弯路的实战心得

  1. 先做memcpy,再做外设:不管什么DMA,先让它在内存间搬运数据跑通,排除中断、描述符、地址映射问题,再挂外设。我见过有人上来就调SPI DMA,结果折腾一个星期发现是memcpy模式就没配置对。
  2. 关注Cache一致性:ARM和Xilinx的DMA默认不会帮你做Cache同步(除非用IOMMU/SMMU)。Intel的PCIe DMA带有硬件一致性协议(如DMA Remapping)。你写驱动时一定要在描述符区域使用dma_alloc_coherent或手动flush。这个坑我至少填过三次。
  3. 多队列不等于性能线性提升:通道多了,中断合并、描述符预取、Cache线争夺都会影响实际吞吐。我曾经在Xilinx MCDMA上开了8个通道,结果总带宽反而不如4通道——每个通道都在抢AXI总线仲裁。你需要根据外设带宽需求做减法。
最后送一句话:读Datasheet的速度决定了你调DMA的速度。别省看寄存器描述的时间,那几分钟换来的可能是几天的不解之谜。