ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于UltraScale+的XDMA方案:PCIe高速DMA传输全流程实践

基于UltraScale+的XDMA方案:PCIe高速DMA传输全流程实践

这个项目是某块Ultrascale+板卡,目标是让服务器和FPGA之间的PCIE通道彻底打通,传输性能还不能拉胯。调研完方案以后直接选了XDMA这条路,从Vivado建工程、配IP、连线、出bitstream,到Linux下驱动加载、DMA读写验证、带宽测试,整个流程前后花了差不多两周。中间踩了一堆坑,官方文档写得绕,网上帖子又经常只讲一半,尤其是配置项含义和排查思路这块,能一次说清楚的不多。所以我把这段经历完整整理出来,给后面走这条路的同学一个能直接照着做的参考。

这套内容适合对FPGA和Linux驱动都有点基础、但第一次做PCIe DMA的开发者,也适合急着评估XDMA方案的板卡试玩党。硬件上只需要一块Ultrascale+开发板,插到服务器的PCIe槽里,配上Vivado开发环境,剩下的就是耐心。

1. 整体方案与架构拆解

1.1 三条路线对比:为什么最终选XDMA

先说方案选型。PCIe通信在FPGA端常见的有三条路,我一开始都认真评估过。

第一条路是用普通的AXI Memory Mapped桥接,也就是不引入DMA引擎,Host端直接通过BAR空间读写FPGA侧的寄存器或内存。这个方案最简单,IP配置少,代码量也小,但问题非常明显:每次读写都要CPU发起,数据量一大CPU占用率直接爆表,单次事务的效率也上不去,实际带宽往往只有几百MB/s,根本撑不起高速采集类应用。

第二条路是自研DMA引擎。理论上可控性最高,想要什么策略自己写就行,但现实很骨感:你得自己处理TLP构造、描述符环管理、MSI-X中断、异常恢复,这些东西单独拿出来每一个都能写上几周,更别说验证和调试的成本。除非你是专门做PCIe IP的团队,否则项目周期完全不可控。

第三条路就是Xilinx官方提供的XDMA IP核。它把PCIe事务层、DMA引擎、描述符管理、中断控制全部封装好了,用户只需要在FPGA侧接上AXI接口,Host侧加载配套驱动,就能实现高速DMA传输。选择这条路的核心原因就两个字:可控。时间可控,风险也可控,官方IP在很多板卡上都有验证案例,踩坑成本远低于自研。

方案开发成本性能上限风险适用场景
AXI MM桥接低低低低速控制、寄存器读写
自研DMA高高高定制协议、长期产品
XDMA IP中高中低快速交付、标准DMA

1.2 UltraScale+平台与PCIe硬核的优势

方案定了以后,平台选型就相对简单了。UltraScale+系列集成了PCIe硬核,也就是Integrated Block for PCIe,物理层、数据链路层、事务层的大部分逻辑已经在硅片上做好了,用户不用从零写RTL,只需通过IP配置和胶合逻辑把它用起来。

相比7系列,Ultrascale+的PCIe硬核对PCIe Gen3的支持更完整,每条lane跑8GT/s,x4配置下总带宽就是32GT/s,128b/130b编码折算后净速率大约3.9GB/s,实际DMA负载跑到2.5GB/s以上没有问题。7系列虽然也有硬核,但Gen3的时序收敛更痛苦,信号质量要求也高,做起来心累。

另外XDMA IP本身要占不少资源,大致是几万LUT加几十个BRAM,放在Ultrascale+这个级别的片子上面毫无压力,用户逻辑还能留出大量空间。如果选容量太小的7系列,光IP占资源就够你头疼了。

1.3 系统架构与数据通路总览

整个系统的结构用文字描述大概是这样的:Host端是CPU加内存,通过PCIe总线连接到FPGA板卡。FPGA端以XDMA IP为核心,对外有四组AXI接口,分别承担不同角色。

第一组是AXI4-MM Master接口,DMA引擎搬运数据时用它访问FPGA端的存储或外设。第二组是AXI4-Lite接口,Host通过BAR空间映射过来直接读写用户寄存器。第三组是AXI4-MM Slave接口,个别模式下Host也能主动访问FPGA端较窄的地址空间。第四组是可选AXI4-Stream接口,适合连续流式数据,比如ADC采样直传。

数据搬运的基本流程是这样的:Host端驱动初始化描述符环,把源地址、目的地址、传输长度等信息写入描述符,然后往XDMA控制寄存器里触发一次搬运。XDMA从描述符环里按顺序取出任务,向主机内存发起DMA读或写,期间数据以TLP包的形式在PCIe总线上传输。搬运完成后写回描述符状态,再通过MSI-X中断通知Host。这个流程听起来复杂,但用户可以完全不用关心TLP细节,只管AXI侧的读写就行。

2. XDMA核心机制与IP配置实操

2.1 看懂DMA背后的四个关键词:BAR、TLP、描述符、通道

想配置好XDMA,至少得把四个基础概念搞明白,否则配出来的工程出了问题都不知道去哪查。

第一个是BAR空间。PCIe设备上电后,会向系统申请一段地址空间,通过BAR寄存器映射到系统物理地址。对CPU来说,访问这段地址就像访问普通内存一样,实际上触发的是PCIe总线上的一次地址映射访问。XDMA的BAR0通常放它的配置寄存器,包括控制、状态、描述符地址、中断状态等。BAR1、BAR2、BAR3可以映射到FPGA侧的AXI4-Lite域,这样Host就能直接读写用户自定义的寄存器。

第二个是TLP。PCIe总线上的一切数据交换都封装成事务层数据包,也就是TLP。你不需要深挖每种TLP的字段结构,但要清楚DMA引擎的本质就是源源不断地产生存储器读TLP和存储器写TLP,把数据从一端搬到另一端。

第三个是描述符环。这是DMA性能的关键所在。一批DMA任务描述以环形队列的形式放在Host内存里,每个描述符包含源地址、目的地址、传输字节数和标志位。XDMA从环里按顺序取描述符,执行一个就写回一个状态。这个设计有点像餐厅的出菜口:厨师把订单贴在窗口,传菜员拿一张送一桌,做完就打钩,老板随时能往窗口补新单。订单越多、队列越深,传菜员空闲时间越少,整体吞吐自然越高。

第四个是通道。XDMA的DMA通道分为Host到Card和Card到Host两类,方向不同但结构相近。每个方向的通道都有一组独立的描述符环和控制状态寄存器。项目里最多会用到一组H2C和一组C2H,基本能满足绝大多数场景。

2.2 Vivado工程与XDMA参数配置详解

建工程的时候,先确认好目标器件,选具体的Ultrascale+型号。新建Block Design,添加XDMA IP,然后逐项过参数。下面这些配置项是我这次实际用过的,直接可以抄作业。

PCIe/XDMA Mode选Advanced。Standard模式下IP只是一个裸的PCIe端点,不暴露AXI接口,没法直接用DMA功能,所以必须选Advanced才能拿到AXI MM接口。

PCIe Lane Width按板卡实际能力来,比如板卡是x4的PCIe金手指,这里就选x4。PCIe Link Speed直接选Gen3,也就是每条lane 8GT/s。如果板卡信号质量不太好,PCIe协议本身会支持自动降速协商到Gen2甚至Gen1,所以即使配成Gen3,链路起不来也能降级工作,不会直接挂死。

DMA Interface选AXI Memory Mapped,这是最通用的模式。DMA引擎以AXI MM方式读写FPGA侧的存储,后面无论是挂BRAM还是DDR控制器都方便。如果做ADC流式采集这类应用,可以选AXI Stream模式,但第一次调通建议还是MM模式起步。

AXI Data Width选128位,AxI Address Width选64位。这里有个计算关系值得说清楚:PCIe Gen3 x4的总带宽是32GT/s,如果AXI数据宽度是128位,那么AXI时钟就是32G除以128,正好250MHz,非常规整。如果选512位,AXI时钟会降到62.5MHz,虽然总线位宽更大,但对大多数用户逻辑来说并不一定更好,128位是性能和易用性的折中。

BAR空间配置上,BAR0留4KB给XDMA控制寄存器,BAR1、BAR2再分配一块区域给用户AXI4-Lite接口,这样Host可以直接读写板卡上的状态寄存器。BAR的地址不用手动分配,Xilinx工具会按照配置自动生成。中断选择MSI-X,性能和灵活性都最好,Linux驱动对它的支持也成熟。

还有一组容易被忽略的选项是Descriptor Depth,也就是描述符环深度。默认值可能偏小,我建议直接配置到1024或者更大。描述符环太浅,DMA引擎容易空等,吞吐会明显下降,这是性能优化里最需要关注的地方之一。

2.3 Block Design接线与地址规划

所有关键参数配好后,剩下的工作就是连线。XDMA的ref_clk_p和ref_clk_n接板载100MHz差分时钟,sys_rst_n是系统复位,要保证在参考时钟稳定后释放。user_lnk_up信号可以引出来观察链路training状态,调试时非常有用。

XDMA的AXI MM Master接口通过一个AXI Interconnect或AXI SmartConnect接到用户侧的从设备。最简单的起步方案是接AXI BRAM Controller加BRAM,这样不需要写任何用户RTL就能把DMA通路跑起来。再挂一个AXI4-Lite Slave寄存器模块,给Host提供板卡状态读取入口。

地址规划是新手很容易翻车的地方。打开Address Editor,把BRAM的地址段分配到位,比如放在0x00000000_10000000,然后把XDMA的控制寄存器段和用户寄存器段都分好。关键是保证这些地址段互不重叠,否则DMA写数据有可能直接覆盖到控制寄存器上,表现出的症状非常难查。

时钟域也要提前打好招呼。XDMA输出的axi_aclk通常是250MHz,AXI互联和BRAM控制器都用这个时钟,跨时钟域问题就少了。如果用户的逻辑工作在另外的频率,需要在AXI接口上做异步处理,不要想当然地直接接上去。

3. FPGA端用户逻辑与Host端软件联动

3.1 FPGA端搭建BRAM与寄存器

FPGA侧的逻辑,第一步建议只做一件事:验证通路。所以最稳的组合就是BRAM加AXI BRAM Controller。

在Block Design里把XDMA的AXI MM Master接到AXI Interconnect的Slave侧,Interconnect的Master侧挂AXI BRAM Controller,再给它分配一段地址。BRAM实例的大小可以先设1MB,足够一轮测试用了。这里有个重要提醒:BRAM容量决定了DMA传输长度的上限。如果一次传输超过容量,地址就会越过边界,要么回绕要么写到不存在的地址,表现是传输卡死或者数据错位。所以测试代码里要对长度做限制。

同时加一组用户寄存器,挂在XDMA的AXI4-Lite接口上。寄存器里放板卡版本号、设备ID、最近一次DMA传输的状态字,Host端这边能直接读出来,方便快速判断是DMA通路的问题还是用户逻辑的问题。我在实际项目里就靠这组寄存器快速缩小排查范围,省了很多时间。

生成bitstream之前,检查一下约束文件里的引脚分配。PCIe的引脚位置、参考时钟的差分对、system reset的引脚,全部要和原理图严格对上。综合实现跑完后,导出bitstream,烧到板子里,进入软件环节。

3.2 Linux驱动加载与DMA验证

Host端我用的是Linux,XDMA的驱动源码可以从Vivado安装目录里提取,也可以从开源社区找对应版本。不管哪种途径,核心步骤都一样:进入linux-kernel目录,直接make,生成xdma.ko内核模块。

加载驱动的命令很简单:

sudo insmod xdma.ko

然后检查设备是否被识别:

lspci -d 10ee:

厂商ID 0x10EE是设备默认值,看到这个基本代表PCIe链路已经起来了。再看dmesg输出,确认驱动和硬件完成绑定。此时/dev目录下会多出一组设备节点,通常是xdma0_h2c_0、xdma0_c2h_0、xdma0_user。h2c是Host到Card的DMA通道,c2h是Card到Host的DMA通道,user是BAR空间映射的用户寄存器节点。

验证DMA最简单的方式是用dd命令,比如把BRAM内容读到文件:

dd if=/dev/xdma0_c2h_0 of=/tmp/bram_dump.bin bs=4096 count=256

当然要确保count乘bs不超过BRAM容量。拿到文件后和bitstream里BRAM的初始值比对,一致就说明链路完全通了。第一次项目做到这里,基本就是最让人兴奋的时刻。

3.3 性能测试与调优要点

通路通了以后,就该关心性能了。先算理论上限:Gen3 x4,32GT/s总线速率,128b/130b编码效率大概0.9846,净带宽约3.9GB/s。实际DMA吞吐还要扣掉TLP包头的开销、链路的flow control、描述符管理的开销和中断延迟,所以一般在2.5GB/s到3.2GB/s之间属于正常。

如果实测远远低于这个区间,优先检查下面几件事。

第一,Host内存是否页对齐。驱动如果需要用bounce buffer来做对齐,转发会明显拖慢速度。建议用大页内存做测试缓冲区,性能提升非常直观。

第二,描述符环深度是否足够。前面配置里我建议把描述符深度调大,如果默认值太小,DMA引擎会频繁空等。

第三,块大小。单次DMA传输的块太小,TLP开销占比就高,带宽上不去。测试时bs至少给4KB,最好用更大块。

第四,中断频率。如果每传输一笔就触发一次中断,CPU会忙不过来。XDMA支持中断合并,把多个完成事件合并成一个中断,能有效降低CPU开销。我后来在这个项目里就是用中断合并加多描述符批处理,吞吐比默认配置高了不少。

4. 常见问题与排查技巧实录

4.1 链路训练失败,lspci看不见设备

这个现象是所有PCIe调试里最让人头大的,因为一上来就不通,后面什么都做不了。按以下顺序排查基本能定位。

先看参考时钟。PCIe的ref_clk必须是100MHz差分信号,用示波器直接量P端和N端的波形,频率和幅度都要确认。参考时钟不稳或者根本没起振,链路一定训练不起来。再看复位信号sys_rst_n,PCIe规范要求复位信号拉低时间至少100ms,而且要在参考时钟稳定之后释放。板级设计如果忽略了这个时序,就会出现上电后设备时通时不通的现象。

然后检查引脚约束。PCIe引脚在FPGA封装里是有固定位置的,必须和板卡原理图的连接一致,约束错了百分之百起不来。最后查电源,PCIe硬核对应的bank电压要满足要求,供电不稳也可能导致训练失败。这几项都正常,再用lspci -vv看链路协商到的速度,如果明明是Gen3配置却只协商到Gen1,基本是信号完整性问题,主因通常是链路走线过长或者连接器质量不过关。

4.2 DMA传输卡死或数据全零

链路通了以后,下一类问题集中在DMA行为本身。

如果DMA传输一直阻塞不返回,先确认FPGA侧的AXI地址映射是否正确。地址段重叠会把数据写到控制寄存器上,造成的症状非常诡异。然后把传输长度和BRAM容量核对一遍,超容量访问极容易把事务卡死在AXI总线上。

还有一类隐蔽问题出在AXI从设备的响应能力上。如果用户自己的AXI Slave逻辑写得不完整,不响应或者响应时序不对,XDMA会一直等下去,表现就是驱动超时。所以第一次验证一定要用AXI BRAM Controller这种成熟IP,不要拿未验证的模块去调DMA通路,否则很难分清是协议问题还是用户逻辑问题。

数据全零的情况,优先考虑是不是BRAM压根没初始化,或者地址高位被强制当成0处理。先用BAR空间直接读FPGA寄存器,再通过DMA写一个固定pattern进去,然后DMA读回来比对,两步下来就能确定问题在写入侧还是读取侧。

4.3 中断不生效与复位时序收尾

DMA数据都对但read接口一直阻塞等待,通常是中断链路出了问题。第一次调试时建议先在驱动里关掉中断模式,改用轮询方式验证数据通路,数据通了以后再回头查MSI-X配置。检查驱动注册的中断处理函数是否绑定成功,确认设备树或模块参数里中断相关配置没有被屏蔽。

复位时序这个问题我最后单拎出来说,是因为它最容易被忽略也最坑。板卡上电时如果sys_rst_n由FPGA自己拉高,而不是由主板PCIe槽位上的复位信号经过合理延时后驱动,可能会产生随机性的识别失败。稳妥的做法是把PCIe槽位的复位信号通过阻容滤波或专用复位芯片,延时足够时间后再接入FPGA。这个细节不处理好,可能会让你在链路训练和驱动加载之间来回怀疑人生。

实际操作中我把排查顺序固定成三板斧:先用lspci看链路,再用BAR空间读写寄存器,最后才开DMA。每一步都确认OK再进入下一步,大部分问题都能在这一层一层筛选中现出原形。整套流程走下来,XDMA这套东西并没有想象中那么神秘,其实就是把PCIe协议栈的复杂度藏进了官方IP,真正需要你花心思的,反而是工程接线、参数配置和软件驱动这些看起来不起眼的细节。

我个人最大的心得是:遇到黑盒问题不要硬猜,把系统拆成链路、寄存器、DMA三层去验证,每一层都搞到十拿九稳再进下一层。按照这个方法重来一次,我大概三天就能把一条高速PCIe通路从零跑到测试验收,后面再扩展串行数据采集功能也就有了无比扎实的基础。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进