最近手上在做一个 100G NIC 的项目,核心是把开源网卡 IP Corundum 从它默认的 Xilinx 参考板卡搬到 Bittware VV4 上。这块板卡用的是 Intel Arria 10 系 FPGA,和 Corundum 官方支持的 VCU118 完全是两个世界,所以整个移植过程就是一个“读懂开源代码 + 适配硬件差异 + 解决时序与接口问题”的历程。这篇文章是系列的第一篇,先聊整体思路、硬件识别、工具链切换和基础移植流程,适合手里有 VV4 或者其他 Arria 10 板卡、想玩开源 NIC 的 FPGA 工程师参考。
Corundum 这个词在 FPGA 网络圈子里不算陌生。它是一套基于 Verilog 的、开源的高性能网卡实现,支持 10G/25G/100G 以太网、PCIe DMA、多队列、硬件时间戳和 PTP,整体架构非常完整,不是那种只能跑个回环测试的 demo IP。而 Bittware VV4 是一块商用板卡,板载资源丰富,QSFP 笼子、DDR4、PCIe x16 都有,等于说硬件底子完全够用,缺的就是把 Corundum 从“官方板卡的工程”变成“VV4 自己的工程”这一步。这一步牵扯到的东西不少,我从零开始踩了一遍,把里面最关键的内容整理出来。
1. 项目概述:为什么要做这次移植
1.1 Corundum 到底是什么级别的开源网卡
很多朋友一听到“开源 NIC”,下意识觉得就是“一个以太网 MAC”。其实 Corundum 的定位比这高得多。它是一套完整的智能网卡实现,核心是mqnic这个顶层模块,内部集成了 PCIe DMA 引擎、AXI 互联、MAC 控制器、多队列调度器、MSI-X 中断控制器,甚至还有硬件时间戳和 PTP 1588 的支持。
我最初看中 Corundum,主要是因为它不是“只发不收”的玩具,而是有自己的驱动框架。主机端通过 PCIe 枚举出网卡后,可以使用自带 Linux 驱动进行 DMA 收发和中断处理,配合 iperf3 就能跑真实流量测试。这样从 FPGA 逻辑到软件协议栈,整条链路都是自己可控的,特别适合做定制协议卸载和网络测量。
1.2 Bittware VV4 板卡的特点和选型理由
Bittware VV4 这块板子在国内玩的人相对少一些,但它是一块很标准的、适合做网络加速的 FPGA 板卡。它用的是 Intel Arria 10 系列器件,板上带了 QSFP+ 或 QSFP28 光口,PCIe 接口(x8 或 x16 视具体版本而定),还有 DDR4 和独立的时钟缓冲芯片。
选择它来做 100G NIC 移植,有几个明确的好处。第一,Arria 10 内置硬核 PCIe IP 和高速收发器,100G 的实现不需要外挂 PHY,直接用 Transceiver Native PHY 就能把 4 路 25G NRZ 拉起来。第二,板卡的电源和时钟设计比较规整,稳定性和可复现性比自研板卡强。第三,Bittware 提供了完整的原理图和 QSF 工程模板,引脚约束可以直接修改,不需要自己从零推断。
1.3 移植的整体思路和预期目标
移植这件事,说白了就是“让同一份 RTL 跑在不同的 FPGA 环境里”。Corundum 官方仓库的 fpga 目录下给了好几个参考工程,覆盖 Xilinx VCU118、VCU1525 以及 Intel Arria 10 的某些板卡。理论上如果直接有对应的 A10 工程,照葫芦画瓢就行,但 VV4 不在官方列表里,所以我需要做的是:
- 理解 Corundum 的工程结构和文件清单。
- 在 Quartus 中新建 VV4 工程,把 RTL 和约束导进去。
- 生成 Arria 10 的 PCIe 硬核 IP 和收发器 IP。
- 根据 VV4 原理图,改引脚约束和时钟约束。
- 完成综合实现,修时序,跑到 100G 线速。
这一篇文章先把 1 到 4 讲清楚,时序收敛和驱动的部分放到后续部分展开。
2. 移植前的准备:工具链和硬件识别
2.1 工具链选型和版本匹配
Corundum 官方文档里明确提到,FPGA 工程基于 Vivado 和 Quartus 两套环境都能构建,但版本不能随便换。Xilinx 方向通常用 Vivado 2020.2 或 2021.2,Intel 方向则建议使用 Quartus Prime Pro 20.3 或更新版本。我这次用的是 Quartus Prime Pro 20.4,配合对应版本的 Platform Designer 和 Quartus IP Catalog。
为什么版本匹配很重要?因为 Corundum 在生成 PCIe IP 时用的 Tcl 脚本和 IP 配置界面,在不同版本里字段叫法会有差异。比如 A10 的 PCIe IP 配置里,PHY type、Link speed、RX buffer这些选项在不同版本间可能会变化,导致官方脚本跑不过。如果不想折腾脚本,也可以手动添加 IP 核,然后在 Platform Designer 里手动连线,但那样更容易出错。
2.2 通过 JTAG 识别板卡器件型号
第一步不是写代码,而是确认你手里板卡的实际 FPGA 型号。Bittware VV4 有多个版本,有的用 Arria 10 GX 1150,有的可能是 GX 2200。同一个工程不能直接通吃,因为资源数量和封装引脚不同。
拿到一块全新的 VV4,先用 USB-Blaster 连接到板上的 JTAG 口,打开 Quartus Programmer,点击 Hardware Setup,通常能看到类似5AGXFB3H4F35C5ES这样的器件 ID。然后对照 Bittware 官网的用户手册确认板卡版本,再到 Quartus 里选择对应的 device。
这一步千万别偷懒。我就见过有人拿 A10 GX 1150 的工程强行跑到 2200 的板子上,结果引脚分配全部错位,逻辑分析仪都救不回来。
2.3 获取 Corundum 源码和目录结构
Corundum 源码在 GitHub 上,仓库名就叫corundum。克隆下来之后,重点关注rtl、fpga、modules、linux这几个目录。
rtl目录是核心 RTL 源码,按模块划分,比如mqnic、dma、eth、lib等。fpga目录放的是各板卡的工程文件,以及platform相关的约束和脚本。modules目录是可选的扩展模块,比如 RoCE、RDMA 相关的原型代码。linux目录是 Linux 驱动源码。
我的做法是先把rtl目录整体拷贝到自己的工程目录下,不要直接改仓库里的源码,因为后续想升级官方版本时可以快速 diff。然后在自己的工程里建立ip、scripts、constr几个文件夹,分别放生成的 IP 核、Tcl 脚本和约束文件。
3. 核心移植过程:从 RTL 到 VV4 工程
3.1 先搭一个最小的 Quartus 工程框架
在 Quartus Prime Pro 里新建工程后,选择对应的 Arria 10 器件,然后做几件基础配置:
- 设定顶层模块为
mqnic_pcie或其封装,跟随官方参考顶层变动。 - 添加
rtl目录下所有.v和.sv文件(Corundum 大部分使用 Verilog,也有少量 SystemVerilog)。 - 添加板卡约束文件
.qsf(后续需要修改引脚)。
Corundum 的 RTL 文件比较多,手动一个个加容易漏。官方在fpga目录下有个Makefile或者 Tcl 脚本,能自动收集 RTL 文件列表。如果没有现成脚本,可以在 Quartus 中用file glob方式批量加入,例如在.tcl中执行:
set rtl_files [glob -nocomplain ../rtl/*/*.v ../rtl/*/*.sv] foreach f $rtl_files { puts "Adding $f" }注意 A10 的 Quartus 工程对.sv文件比较敏感,个别文件用了typedef、interface的语法,编译时指定 SystemVerilog 标准为IEEE 1800-2012可以避免低级报错。
3.2 生成 Arria 10 PCIe Hard IP
Corundum 的 DMA 和主机通信全走 PCIe,所以 PCIe 硬核 IP 是第一步。在 Quartus 的 IP Catalog 里搜索 "Arria 10 PCIe",生成一个 PCIe IP,通常是altera_pcie_a10。
几个关键配置项,需要和 VV4 的 PCIe 物理接口对应:
- Lane rate 设为 Gen3(8 GT/s),链路宽度 x8 或 x16,取决于板卡背板连接和 RTL 里
M_AXI_*宽度的设定。 - 参考时钟可以选 100MHz,通常板卡从 PCIe 插槽引入 100MHz。
- 接口模式选
Native AXI-Stream,因为 Corundum 的dma_if模块直接面向 AXI-Stream。
这里有个容易掉的坑:Corundum 默认的dma_if需要一定数量的描述符缓存,而 Arria 10 PCIe IP 的 AXI-Stream 用户接口数据宽度默认可能只有 256 bit, 100G 数据路径要用 512 bit 总线更合理。具体位宽需要看mqnic里的DMA_AXIS_DATA_WIDTH参数,建议设成 512。
生成 IP 后,Quartus 会生成一个altpcie_*例化模板,你需要手动把它和 Corundum 的mqnic_pcie对接。对接的端口主要包括:
rx_st_*、tx_st_*数据总线rx_st_ready、tx_st_valid等握手信号app_msi_*中断信号cra_*CSR 接口(AXI-Lite)
3.3 以太网 MAC 和高速收发器适配
100G MAC 在 Corundum 里是自带的逻辑,不需要额外的以太网 IP 核。它通过一个内部 AXI-Stream 接口接到外部 PHY 或者直接接收发器。在 VV4 板上,100G 通常走 QSFP28 光模块,物理层由 FPGA 内部的高速收发器实现。
为此,需要在 Quartus 里生成一个 Transceiver Native PHY IP,或者用 Arria 10 的altera_xcvr_native_a10IP,配置为 4 通道,每通道 25.78125 Gbps,参考时钟从板上 156.25MHz 或者 100MHz 提供。
这个 IP 的时钟配置如果和板卡实际输入不一致,链路完全起不来。所以这一步必须回到 VV4 原理图,确认 QSFP 参考时钟引脚接的哪一对差分时钟,频率是多少。有的板子设计成可切换时钟源,可能要用 IODELAY 或者 SMB 开关切换,但 VV4 具体如何,要查丝印和手册。
3.4 时钟、复位和引脚约束的关键配置
一旦 RTL 加好,IP 生成完,最“无趣”也最容易出问题的一步就是写约束。A10 的工程里,约束集中在.qsf文件中。
首先是时钟约束。Corundum 通常需要一个user_clk(比如 250MHz 或 300MHz)和一个clk_125(125MHz)。前者由 PCIe IP 输出或者 MMCM/PLL 倍频生成,后者可以来自板卡固定晶振,也可以从参考时钟生成。我在 VV4 上用了 PCIe IP 的app_clk作为 user_clk,因为 A10 PCIe IP 会自动输出一个和链路速率匹配的时钟。
然后是引脚分配。VV4 的 QSF 通常网上有 Bittware 提供的模板,但不可能直接复用,因为官方模板是针对他们自己的参考设计的。需要根据原理图找到:
pcie_refclk引脚pcie_rx/tx引脚数组qsfp_tx/rx引脚数组- 复位按键和 LED 引脚
一个示意的 QSF 片段长这样:
set_location_assignment PIN_AK26 -to pcie_tx[0] set_location_assignment PIN_AL26 -to pcie_tx[1] ... set_location_assignment PIN_U22 -to cdr_refclk_p set_instance_assignment -name IO_STANDARD "DIFFERENTIAL" -to cdr_refclk_p除了引脚位置,时钟约束还要写create_clock。例如:
create_clock -name pcie_refclk -period 10.000 -waveform {0 5} [get_ports {pcie_ref_p}] create_clock -name qsfp_refclk -period 6.400 -waveform {0 3.2} [get_ports {qsfp_ref_p}]如果漏掉时钟约束,Quartus 会报出大量时序分析失败,因为所有路径都变成 virtual clock。很多人以为是逻辑跑不快,其实是没做约束。
3.5 综合实现中的路径优化和资源观察
Arria 10 的资源对 Corundum 来说相对充裕,但 100G 的 AXI 数据通路如果位宽配置不合理,容易在布线阶段出现拥塞。我建议一开始就把DMA_AXIS_DATA_WIDTH设为 512,MAC_AXIS_DATA_WIDTH设为 512,避免内部需要降位宽而产生额外的 shifter 逻辑。
在综合之后,先去Compilation Report看资源占用。如果 LUT 占用超过 60%,布线时序风险会明显上升。Corundum 本身的逻辑并不太吃 LUT,多出来的资源主要是 PCIe IP 的描述符 RAM 和队列管理逻辑。
时序方面,最高频路径通常在dma_if里的描述符指针更新逻辑。如果跑不出 250MHz 的 user_clk,可以尝试在 AXI 总线上插入寄存器级(register slice),Corundum 的 AXI 互联本身支持AXI_*_REG_EN参数,打开之后会在跨时钟域处插入流水级,对时序收敛有很大帮助。
4. 常见问题与排查技巧实录
4.1 PCIe 枚举不识别怎么办
这是移植之后第一个会遇到的问题。插上板卡,主机 lspci 看不到设备,或者看到的是 unknown device。排查顺序我建议如下:
- 检查 PCIe 链路是否进入 L0。看板卡上的电源灯和 PCIe 指示灯是否正常,如果灯都不亮,先查供电。
- 检查
PERST#复位信号时序。Corundum 的pcie_*复位逻辑依赖 PCIe IP 输出的app_rstn。如果板卡的PERST#没有正确接到 FPGA 对应引脚,PCIe IP 根本无法训练链路。 - 检查参考时钟。用示波器可以看
pcie_refclk_p/n是否有稳定的 100MHz 差分信号。很多时候是时钟选择拨码开关没拨对。 - 检查 RTL 顶层里
pcie_rx/tx引脚是否进出反了。收发器方向接反不会烧东西,但链路训练不起来。
4.2 以太网 MAC 回环模式测试数据不对
Corundum 自带回环模式,可以在寄存器层面把 TX 数据直接环回 RX。这种做法常用于在还没有 PHY 时验证逻辑链路。
我在 VV4 上试过,发现回环模式下所有包都发了出去,但 RX 方向收到的字节顺序和发送端不一致。问题出在 AXI-Stream 数据总线的字节使能tkeep处理上。100G 的 AXI 总线是 512 bit,也就是 64 字节,但最后一个包往往不满 64 字节,需要依赖tkeep截断。如果tkeep逻辑没对齐,就会把 padding 当成有效数据。核查eth_mac中tkeep生成逻辑时,注意它是否基于TDEST或者是固定长度,Corundum 里是动态使能,不能改成固定 0xFF。
4.3 高速收发器链路起不来,眼图和 BER 都难看
如果 QSFP 光模块插上后,对端能看到光但收不到数据,多半是发射端极性或者均衡参数没配好。Arria 10 收发器的TX_EMP、TX_DIFF_CTRL等参数一般不需要改,最容易忽略的是协议类型设置。
在 Transceiver Native PHY IP 配置里,Protocol选项如果选了Basic无协议模式,很多时候不会自动做 64B/66B 编码。而 Corundum 的 100G MAC 输出的是编码前的数据,这意味着 100G 物理层必须使用10GBASE-R的物理编码子层。我在配置时把协议选成了10GBASE-R(或者对应的25GBASE-R),内部PCS使能后,整个链路才正常工作。
另外要注意:A10 的收发器通道参考时钟输入在 QSF 里要用不同标准的引脚,REFCLK引脚要设成DIFFENTIAL,并且参数input_io_standard也要对应。这点非常容易忽略,因为 Quartus 不会报明显错误,只会让参考时钟检测异常。
4.4 调试时用 ILA 还是 SignalTap
有了 SignalTap II,排查 FPGA 内部信号就方便多了。我的建议是,在起步阶段不要着急拉一堆信号,信号太多反而占资源、影响时序。
先拉这几个关键信号:
user_clk和clk_125,确认时钟真实存在且频率正确。pcie_rstn,确认 PCIe 复位释放。dma_if的rx_axis_*和tx_axis_*,确认有数据流进入。- 100G MAC 的同步头
ctrl_rx_valid或者rx_header,确认 PCS 真正锁定。
如果user_clk存在但dma_if没有输出,要看描述符状态寄存器的地址是否能通过 AXI-Lite 读到正确值。这一步可以顺手验证 PCIe 到 FPGA 的寄存器读写通路是否正常。
5. 一阶段总结和第二篇预告
这一阶段做下来,最大的体会是:开源 IP 的移植工作,80% 的时间其实不在写 RTL,而是在跟器件 IP 配置、引脚约束和时序报表较劲。Corundum 本身的代码质量相当高,在 A10 上几乎不需要改动逻辑功能,只要把“外壳”换掉,也就是 PCIe IP、收发器 IP、时钟约束这层东西,它就能在 VV4 上跑起来。
如果跳过这些准备工作直接上板,遇到问题再回来查看原理图,那是最浪费时间的操作。Vivado 和 Quartus 的报错信息有时候很抽象,但原理图上的引脚名和电源树永远是最可靠的依据。
第二篇我会接着写 Linux 驱动加载、dma 初始化、iperf3 测速以及 100G 大文件传输测试的具体过程,包括怎么配置光模块、怎么排查丢包率、怎么用网卡自带统计寄存器验证收发字节数。这个系列会保持同样的风格,只讲实际操作,不做纸面功夫。有正在移植 Corundum 或者其他开源 NIC 的朋友,可以在评论区聊聊你踩过的坑,一个板子一个坑,但方法和思路是共通的。