1. 为什么把Corundum移植到Bittware VV4:先把动机想清楚
这块Bittware VV4在我的工位上放了快两个星期。刚拆箱的时候,它也就是一块普通的FPGA加速板,厂商例程能点灯、能跑PCIe枚举,但说到底还是在demo层面打转。我心里其实很清楚,这种级别的板卡如果只是跑厂商自带的参考设计,那它和一块开发板的差别就只剩下价格了。真正让这种板卡产生价值的方式,是把它变成一件有明确用途的工具。而对我手上这块板卡来说,最合适的用途就是:把它改造成一张开源的100G NIC。于是就有了这个系列——把Corundum移植到Bittware VV4。
先说说Corundum是什么。它是目前FPGA开源网卡领域里完成度最高的项目之一,用Verilog写了一套完整的网卡数据通路,覆盖了以太网MAC/PCS、收发队列、DMA引擎、PCIe主机接口,并且配套提供Linux内核驱动。也就是说,你拿到这块软核代码,把它综合到一块有高速SerDes和PCIe硬核的FPGA上,再配上驱动,这块FPGA板卡就能变成服务器里一个真正可用的高带宽网口。和市面上动辄几千上万的商用100G网卡相比,Corundum最大的优势不是便宜,而是开放——从DMA描述符的组织方式到接收队列的调度策略,每一行逻辑你都能看、能改、能重写。
那VV4为什么适合做这个移植?这张板卡搭载了Xilinx Virtex UltraScale+ VU4P FPGA,板上有PCIe Gen3 x16硬核,还有QSFP28这类四通道高速光模块接口,物理上完全具备构成100G网卡的条件。很多FPGA板卡其实都具备这种潜质,但厂商默认只会给你一套演示用的bitstream,不会给你网卡固件,更不会提供完整的数据通路设计。这时候Corundum的价值就出来了:你不需要从零写一个网卡,而是把开源方案适配到自己的板卡上。
这个系列我计划分两篇来写。第一篇主要解决“能不能跑起来”的问题:包括Corundum工程结构拆解、VV4板卡资源盘点、板级工程生成、引脚与时钟适配、综合布局布线,最后做到bitstream能下载、PCIe能被主机枚举。第二篇再聊Linux驱动加载、网络接口配置、以及100G双向吞吐的实际表现。之所以要把硬件适配和软件验证分成两篇,是因为这两个阶段的工作完全是两个维度,混在一起讲很容易两端都没讲透。
这篇内容适合谁看?两类人比较对口。一类是FPGA工程师,手里恰好有类似的Xilinx高端板卡,想用开源方案快速验证网卡功能;另一类是做数据中心网络或智能网卡方向的同学,想在网卡数据面里加入自定义逻辑,需要先有一个完整可控的基准设计。就算你手头不是VV4,这篇文章里的移植思路对绝大部分Xilinx UltraScale+的板卡都适用,无非是引脚、时钟和IP配置不同。
2. 移植之前,先看透Corundum的工程骨架
很多人拿到开源FPGA项目,第一反应是直接打开Vivado把工程跑起来。但Corundum这种项目规模不算小,如果连它的模块边界都不清楚,一旦综合报错或者bitstream跑不出预期效果,你连从哪开始查都不知道。所以先把工程骨架看明白,后面每一步都能少走弯路。
2.1 从物理信号到Linux驱动,整条链路是怎么组织的
一个FPGA网卡的数据通路可以粗略分成四层。最底层是高速SerDes和以太网物理层,在Xilinx平台上通常体现为GTH/GTY高速收发器,以及以太网IP核里的PCS/PMA部分。往上一层是以太网MAC,负责组帧、校验、流量控制这些数据链路层的活。再往上是网卡自己的核心逻辑:收发队列、描述符表、DMA引擎,这一层决定了网卡怎么把网络数据搬到主机内存,以及怎么把主机要发送的数据取回来。最后是PCIe硬核和Linux内核驱动,负责把整个设备暴露给操作系统。
Corundum最让我欣赏的一点就是,这四层之间的边界非常干净。里面有大量自研逻辑,尤其是队列管理和DMA引擎,完全不依赖Xilinx或者Intel的闭源DMA IP。这跟很多人习惯的“用XDMA IP做搬运”是两回事。自研DMA的优势不仅是代码可控,更重要的是队列模型是开放的,你可以按自己的需求调整队列数量、描述符深度、中断聚合策略,这些在商用网卡上基本都是被固件锁死的。
2.2 移植的时候,真正要动的其实是一个薄适配层
理解了整条链路之后你会发现,Corundum的核心逻辑其实和具体板卡没多大关系。它的DMA、队列、MAC逻辑都是通用的,芯片厂商的差异被隔离在一个比较薄的适配层里。所谓移植,本质上就是把这个适配层替换成目标板卡需要的形态。
具体来说,板级工程里需要动的东西集中在三个地方。第一是RTL顶层,顶层模块里实例化了哪些高速收发器、哪些IP核,这是和板卡强相关的;第二是IP配置,PCIe硬核的lane数、参考时钟频率,以太网子系统的线速率和SerDes配置,这些参数必须和板卡物理设计一致;第三是约束文件,也就是XDC,里面定义了所有引脚位置、时钟约束、以及高速串行信号的物理约束。
很多移植文章喜欢直接给你列操作步骤,但我觉得把边界想清楚比步骤更重要。你只要知道哪些代码是通用的、哪些是要动的,遇到问题的时候脑子里的排查路径就是清晰的:先查适配层,再查IP配置,最后查约束。不会一上来就在核心逻辑里乱找。
2.3 软件侧:驱动和硬件工程之间的匹配点
Corundum的Linux内核驱动和硬件工程之间是有明确匹配约定的。驱动通过PCIe的厂商ID、设备ID、BAR空间布局、中断配置来识别和操作硬件。移植板卡时只要FPGA工程里这几个参数保持一致,驱动基本上不用改就能认出设备。
但这里有个容易被忽略的坑:板卡厂商给的PCIe设备ID、子系统厂商ID,有时候和Corundum官方参考板卡的默认值不一样。我第一次调试的时候就遇到过类似的情况,设备能被lspci看到,但驱动就是绑不上。后来才发现是设备ID不匹配,需要通过驱动参数或修改配置空间来对齐。这种事在官方参考板卡上不会出现,但一换到自己的板卡,立马就成了第一个拦路虎。
所以在动手改工程之前,我强烈建议你先去Corundum的官方代码仓库里,把板级工程的目录结构和驱动源码的目录结构都浏览一遍,心里有个地图。尤其是板级工程的构建脚本,它会告诉你整个工程是怎么从零拼出来的。
3. VV4板卡资源盘点:上电前先把这些信息整理成一张表
移植工作正式开始前的第一件事,不是写代码,也不是跑综合,而是拿着一张原理图把板卡的物理资源彻底摸一遍。这一步做得到不到位,直接影响后面Debug要花多少时间。我第一次做类似的移植时,因为跳过了这一步,结果综合跑到一半才发现PCIe参考时钟引脚选错了bank,整整浪费了两天。
3.1 必须确认的板级硬件信息清单
以下这些信息,每一项都直接影响工程的搭建。我建议你拿着板卡原理图和用户手册,把它们整理成一张表,后面写XDC的时候直接对照着抄就行:
| 需要确认的信息 | 常见取值/查找位置 | 对移植的影响 |
|---|---|---|
| 主FPGA型号、封装、速度等级 | 板卡丝印/Vivado device list | 决定Part号,影响工程建立 |
| PCIe硬核位置和lane数 | 原理图/UG手册 | 决定PCIe IP的lane配置和参考时钟位置 |
| QSFP28高速收发器引脚 | 原理图,QSFPDD到FPGA的net命名 | 决定GT引脚约束,必须精确到通道 |
| 板载参考时钟源频率和引脚 | 原理图时钟树:系统时钟、PCIe refclk、GT refclk | 决定时钟约束和MMCM/PLL输入频率 |
| 复位信号类型和极性 | 上电复位芯片、FPGA全局复位脚 | 决定复位同步逻辑的方向 |
| 配置模式 | JTAG/QSPI/PCIe配置 | 决定下载bitstream的方式 |
| DDR控制器引脚 | 原理图DDR部分 | 工程若带DDR就必须对应约束 |
这里面的细节太多了,我只挑几个最容易出问题的讲。首先是FPGA型号的确认,看起来像是废话,但同一块板卡在不同批次可能搭配不同速度等级的FPGA,速度等级选高了会导致时序收敛难度增大,选低了又浪费硬件能力。原则上以板卡上实际丝印为准。
其次是PCIe参考时钟的位置。Xilinx FPGA上PCIe硬核的refclk引脚不是随便一个普通IO都能接的,它必须接到专用的refclk引脚上,而且要和PCIe IP在同一个Quad/bank区域。到时候你打开Vivado的device view如果发现reflck连到了普通IO,综合阶段大概率会报错或者生成一个无法布线的设计。
3.2 对照参考板卡评估移植工作量
除了整理VV4自己的资源,我还会花一点时间去Corundum仓库里看官方支持了哪些板卡工程,尤其是同厂商、接口形态相近的参考板卡。这一步的目的是评估移植工作量:如果你的目标板卡和某个参考板卡在高速串行接口数量、PCIe lane数、系统时钟设计上都比较接近,那你基本可以确定,移植工作主要集中在引脚约束和IP重配置上,RTL顶层几乎不用大改。
以VV4为例,它用的是Xilinx UltraScale+器件,如果官方仓库里已经有一块同样是UltraScale+、带QSFP28和PCIe Gen3 x16的参考板卡,那么整个移植的思路就非常清晰了:以这块参考板卡为基础,把它的XDC替换成VV4的引脚约束,把IP按VV4的实际硬件重配一遍,然后重新综合实现。这比从零新建一个工程要稳妥得多,因为Corundum的核心逻辑在参考板卡上已经被验证过了,你要动的只是外围绑定。
4. 生成板级工程:从参考板卡起步,而不是从零开始
准备工作做完以后,终于到了动手建工程的环节。在这个阶段我的原则是:能复用就复用,能不手写就不手写。Corundum官方仓库里的构建脚本本身就是为了省去手工搭建工程的重复劳动设计的,你要是老老实实遵循它的目录结构和脚本约定,生成工程就是一条命令的事。
4.1 找到最接近的参考板卡工程并复制
第一步,在仓库的板级工程目录里找到最接近VV4的参考板卡。怎么判断“最接近”?主要看三个维度:FPGA厂商(都是Xilinx),高速收发器类型(是不是都有足够的GTY/GTH通道跑QSFP28),以及PCIe硬核规格(是不是Gen3 x16)。找到一个合适的模板后,把它整体复制一份,改成你的板卡名称,比如board_vv4。
这里有个很多人会犯的错误:直接在原模板上改。虽然前期省事,但改到后面你会分不清哪些是模板自带的、哪些是你改的,出了问题不好回退。复制一份新目录,把原模板保持干净,任何时候都能做一个干净的对照实验,这个习惯能帮你省下很多Debug时间。
4.2 用脚本方式生成工程,不要迷信GUI向导
接下来就是修改工程参数并重新生成。这里我强烈建议用Vivado的Tcl脚本方式,而不是在GUI里点点点。原因很简单:脚本可重复、可版本管理、且方便以后在别的机器上重建工程。
工程生成脚本涉及的参数无非是这几项:目标器件Part号、源文件路径列表、XDC约束路径列表、IP列表。下面是一段非常简化的示意,真实工程里的脚本会比这个复杂,但核心逻辑是相通的:
# 示意:实际以仓库board脚本为准 set part "xcvu4p-flga2104-2L-e" create_project -name corundum_vv4 -force set_property part $part [current_project] add_files -norecurse [glob ./rtl/src/*.v] add_files -fileset constrs_1 [glob ./xdc/*.xdc] update_compile_order -fileset sources_1需要注意的是,Part号千万不要从别的板卡工程里复制过来用,务必对照你自己板卡上的丝印确认。同一个FPGA系列,封装不同、速度等级不同,综合出来的结果是两回事。
4.3 IP核重配:PCIe、100G以太网、时钟管理
工程生成以后,重头戏是IP核的重新配置。Corundum的板级工程里通常至少要包含以下几类IP:
第一个是PCIe硬核IP。你要确认lane数、参考时钟频率、接口位宽这些参数和VV4一致。PCIe Gen3 x16的配置下,IP核的AXI接口位宽、地址转换逻辑都会影响最终和驱动的配合,这里一定要仔细对照原理图。
第二个是高速以太网IP。在Xilinx平台上,100G以太网通常采用4路25G的线速率,所以对应的以太网IP要按4x25G来配置。这里面的参考时钟非常关键,100G的SerDes参考时钟通常不是普通的100MHz,而是和线速率相关的专用频率,比如25G通道常见的156.25MHz。如果这个频率配错,IP核综合能过,但上板后GT的TX/RX完全没法锁定。
第三个是时钟管理模块。板卡上的晶振频率和参考板卡经常不一样,比如可能是125MHz系统时钟、33MHz PCIe时钟等。所有和MMCM/PLL相关的输入频率参数都要按VV4的实际情况修改。
IP重配是个细致活,改完每一个IP都要重新生成输出产物,然后回到工程里更新一下。不要攒到最后一起改,否则一旦工程更新,报错会铺天盖地,你就分不清是IP的问题还是引脚约束的问题了。
5. 综合和实现阶段最容易卡住的三个地方
工程生成完,开始跑综合。这里我先给你打一个预防针:第一次跑的时候,报错几乎是必然的。这不代表你前面的工作有问题,反而说明引脚、IP、复位这些细节开始真正接受Vivado的规则检查了。
5.1 参考时钟引脚和GT bank的绑定关系
我遇到的第一类报错,是高速串行信号的物理约束问题。Xilinx对GT引脚的约束要求非常严格:一个GT reference clock必须落在对应的专用时钟引脚上,一组GT通道也必须工作在指定的bank范围内。如果你的XDC里把QSFP28的TX/RX信号约束到了错误的GT channel,Vivado在实现阶段会直接给出“physically incapable”之类的错误,告诉你这个物理连接根本走不通。
解决这类问题没有什么捷径,就是回到原理图,一个一个通道地核对:QSFP28的每个lane到底接到了FPGA的哪个GT引脚,然后把这些引脚号老老实实写进XDC。别嫌繁琐,这一步的细致程度决定了后边能否布通。
有个小经验:在写GT约束的时候,我会把原理图里的net名和FPGA引脚名做成一张对照表,每查一个就勾掉一个,避免漏掉或搞混。尤其是那些看起来像TX/RX的差分对,极性一接反,上板后就是千兆网卡都认不出来的诡异现象。
5.2 100G以太网IP的复位序列
第二类容易让人抓狂的问题,不是综合报错,而是综合实现都能过、bitstream也能烧,但上板后IP核状态就是不对。这种情况里,100G以太网IP的复位时序是头号嫌疑。
Xilinx的以太网IP对复位都是有要求的:核心复位信号必须维持足够长的时间,用户逻辑必须在IP核内部的PCS复位完成后才能开始发送数据。很多移植设计里,这个问题会被“参考板卡的复位逻辑”掩盖掉,因为参考板卡的复位时序恰好满足要求。但你一旦把XDC换成VV4、把复位源换到另一个引脚,复位释放时间可能就会发生变化,IP核的lane就开始进入“假死”状态。
排查思路是:在工程里加一个ILA,抓取IP核的状态信号和reset_done信号,对比IP手册里的时序要求。如果发现复位释放太早,就调整复位逻辑里的计数器阈值,所以RTL顶层那些看起来不起眼的参数,实际上每个都需要和板卡时序对齐。
5.3 时序收敛:第一次跑到100G频率没那么简单
第三类问题是时序收敛。100G网卡设计里,以太网和DMA路径的工作频率都不低,Vivado的时序报告里到处是failing paths其实是常态,但只要在合理范围内修起来并不难。
我的习惯是先区分是真实路径问题还是约束缺失问题。如果failing path集中在跨时钟域的关键路径上,先检查有没有给异步FIFO或者同步器加合适的set_clock_groups约束;如果是正常的逻辑路径时序不够,则考虑调整综合策略(例如global retiming)、优化RTL里的关键组合逻辑、或者换用更高级别的优化选项。
有一点要特别提醒:不要为了时序好看,把所有跨时钟域路径都用set_false_path一刀切。Corundum的数据通路里很多CDC是有实际功能要求的,乱加false_path虽然能让时序报告变绿,但上板后数据会随机出错,那种问题比时序不过还难查。
6. 第一次把bitstream跑起来:验证PCIe枚举和GT状态
综合实现都过了,bitstream也生成出来了,接下来进入最让人兴奋也最容易失望的环节:下载验证。这个阶段你期望的结果只有一个——打开Hardware Manager下载bitstream后,主机能通过PCIe枚举到设备,GT通道的状态信号能给出正确的ready电平。
6.1 VV4板卡上的JTAG下载与首次上电
Vivado Hardware Manager里选择对应的JTAG链目标,加载bitstream,观察下载是否成功。如果弹出了错误,先别急着怀疑硬件,先确认JTAG链路是否正常、板卡是否处于正确配置状态。VV4这类板卡的JTAG链路一般直连FPGA,只要电源正常、下载器驱动没问题,这一步通常都比较顺利。
下载成功以后,马上在主机上执行lspci -vvv,看看PCIe设备有没有被总线枚举出来。如果能看到一个未知设备,描述里出现类似“Ethernet controller”或“Network controller”的字样,说明bitstream基本正常,PCIe硬核的配置空间已经被顺利读取了。这一步绿灯亮起,意味着你前面折腾的PCIe IP配置、参考时钟、引脚约束都是对的。
6.2 用ILA确认GT状态,而不是直接期待网口能通
很多人在这个阶段容易有一厢情愿的期待:bitstream烧进去以后,插根网线连上交换机,网口是不是就能通了?答案是否定的,因为这时候Linux驱动还没加载,网卡的DMA引擎和队列管理逻辑根本没有被初始化,网络接口还静默着。所以这个阶段不要急着测网络,先做两件事。
第一件,在工程里预先插入ILA核,抓取GT的TX/RX ready状态和以太网IP核的lane状态。当你下载完bitstream后,去ILA里观察这些信号,确认GT的PLL已经锁定、RX端没有在疯狂报错误。第二件,如果条件允许,做一个GT loopback测试,也就是在FPGA内部把TX短接到RX,观察数据通过SerDes后能不能正确回收。这一步能验证高速收发器链路是否通路,是后面跑真实流量前最靠谱的预检手段。
这里我踩过一个坑:第一次上板时看了GT状态全绿,但QSFP28的光模块就是不发光。纠结很久才发现,光模块的使能信号是FPGA的一个GPIO控制的,而我在XDC里忘了约束这个管脚,导致它一直处于disable状态。这种问题在原理图面前其实一眼就能看穿,所以每次上板前我都会把原理图里所有和光模块相关的GPIO过一遍,确认它们在工程里都有对应的IO约束。
6.3 审慎记录,为第二篇驱动调试做准备
首轮验证完成后,我一般会把以下信息记录下来:设备枚举的完整lspci输出、ILA抓到的GT状态、以及所有在适配过程中修改过的参数。这些信息看起来零散,但它们组成了下一步驱动调试的基线。
驱动加载这一步,涉及的内容已经不少了:驱动模块怎么编译、设备ID怎么匹配、BAR空间大小是否和硬件一致、网络接口命名、以及最关键的——跑真实的iperf3 100G吞吐测试。这正好是第二篇的主题。现在你手上已经有一块能被PCIe枚举的“硬件雏形”,但它还不是一张完整的网卡。剩下的事情,就是把软件喂进去,让这些队列和DMA引擎真正运转起来。
到时候我们会用一个最简单的方式验证成果:给两个网口配好IP,启动iperf3,然后看那两行Throughput数据出来的时候,你会觉得前面所有和时序、引脚、IP核纠缠的日子都值了。