1. 为什么是Corundum?——从“能跑通”到“真可用”的硬核分水岭
在FPGA高速网络接口开发圈子里,提到100G以太网,绕不开两个名字:一个是商业IP核厂商的闭源黑盒方案,另一个就是Corundum——这个由社区驱动、完全开源、MIT许可证授权的100G NIC实现。但很多人第一次听说它时,下意识反应是:“开源的?能跑吗?能稳定收发吗?延迟多少?吞吐打满没?”——这些不是质疑,而是真实工程落地前必须跨过的门槛。我2021年第一次把Corundum跑在Xilinx VC709上时,用的是官方提供的Vivado工程,loopback测试通过,ping通了,就以为“成了”。结果一接入真实流量,TCP重传率飙升,UDP丢包肉眼可见,抓包发现大量FCS错误和RX FIFO overflow。折腾两周才发现,问题根本不在Corundum本身,而在于它默认适配的是特定参考板的PHY层时序约束、时钟域划分和PCB走线特性。就像给一辆赛车装上民用轮胎——引擎再强,抓地力不够照样打滑。
Bittware VV4这块卡,不是普通FPGA开发板。它是一块面向数据中心加速场景的商用PCIe卡,核心是Intel Agilex FPGA,板载双100G QSFP28光口,支持PCIe Gen4 x16,关键还有Bittware自家的BIOS固件、硬件管理控制器(HMC)和经过信号完整性验证的高速背板连接器。它的价值不在于“能烧进FPGA”,而在于“能插进服务器机架里7×24小时跑”。所以,把Corundum移植到VV4,绝不是简单替换一下top.v文件里的引脚定义。它是一次完整的系统级适配工程:从FPGA内部的时钟树重构,到外部PHY芯片(通常是Marvell或Intel的100G PHY)的寄存器配置序列;从PCIe Root Complex与Endpoint之间的链路训练协商,到Linux内核驱动对DMA描述符环、中断向量、MSI-X多消息的支持;甚至包括Bittware HMC如何与FPGA逻辑协同完成热插拔检测和温度监控。这背后涉及的,是数字电路设计、高速SerDes协议、PCIe体系结构、Linux内核网络子系统、以及商用硬件平台特有的工程规范。开源不等于免维护,恰恰相反——它把所有黑盒打开,让你直面每一个比特的来龙去脉。这也是为什么标题里特意强调“(一)”:这不是一个周末就能搞定的Demo,而是一个需要拆解成至少五个关键模块、逐个击破的系统工程。
提示:Corundum不是“开箱即用”的软件包,它是一个RTL级的硬件设计项目。它的“编译”是综合与布局布线,“安装”是bitstream烧录,“驱动”是Linux内核模块。理解这一点,是避免后续踩坑的第一道心理防线。
2. VV4硬件拓扑解剖——看清你的战场在哪里
在动代码之前,必须把VV4这张卡的物理和逻辑结构彻底摸透。Bittware官方文档(尤其是《VV4 Hardware User Guide》和《VV4 FPGA Design Guide》)是唯一权威来源,但它们往往写得像法律条文一样严谨枯燥。我花了三天时间,把文档里所有关于FPGA I/O Bank分配、时钟资源、PCIe Hard IP配置、QSFP28控制信号、以及HMC通信接口的部分,全部手绘成一张逻辑拓扑图。这张图后来成了整个移植项目的“作战地图”。下面我把关键信息提炼出来,用工程师之间交流的口吻讲清楚:
VV4的核心是Intel Agilex AGF014R24B2E2V device,它被划分为几个功能区域:
- PCIe Hard IP Block:位于FPGA左上角,原生支持PCIe Gen4 x16。注意,它不是软核,是硬宏,这意味着你不能随意修改其内部状态机,只能通过Avalon-MM或AXI-Lite接口配置其寄存器。Corundum的PCIe wrapper必须严格匹配这个Hard IP的时序要求和地址映射。
- QSFP28 PHY Interface:VV4提供两组独立的QSFP28接口,每组包含8条100G PAM4 SerDes通道(实际使用中,100G通常采用4x25G NRZ模式)。关键信号包括:
tx_data_i[31:0]/rx_data_o[31:0](并行数据总线)、tx_clk_i/rx_clk_o(源同步时钟)、tx_reset_n/rx_reset_n(复位)、以及mod_prs_n/los_n(模块存在/信号丢失检测)。这里最容易出错的是时钟域交叉:QSFP28的RX clock来自光纤,是异步于FPGA主时钟的,Corundum的MAC层必须通过异步FIFO做跨时钟域处理,否则必然出现亚稳态导致数据错乱。 - HMC (Hardware Management Controller):这是VV4区别于普通开发板的灵魂。它是一颗ARM Cortex-M系列MCU,通过SPI总线与FPGA通信,负责监控板卡温度、电压、风扇转速,并在异常时触发FPGA的
hmc_alert_n信号。Corundum本身不关心HMC,但你的顶层设计必须预留这个接口,并在FPGA侧实现一个简单的SPI slave逻辑,至少能响应HMC的读取请求(比如返回一个固定的“FPGA ready”状态码),否则Bittware的管理软件会报错,甚至拒绝加载bitstream。 - JTAG & UART Debug Header:别小看这个4-pin排针。它直接连到FPGA的JTAG TAP和UART RX/TX。在早期bring-up阶段,你几乎90%的调试信息都靠它输出。我建议在Corundum的顶层模块里,集成一个极简的UART TX-only core(用Verilog写几十行就行),把关键状态机跳转、PCIe link up事件、PHY初始化完成等信息打印出来。这比用ChipScope抓波形快十倍。
下面这张表格,是我根据VV4原理图和Corundum v2.0 release的pinout整理出的关键信号映射对照表,也是我第一次烧录失败后,逐条核对发现的三处致命错误来源:
| VV4 FPGA Pin | Signal Name | Corundum Expected Role | 实际物理连接 | 常见错误 |
|---|---|---|---|---|
AF15 | pcie_rx_p[0] | PCIe RX differential pair | 连接PCIe插槽第1对差分线 | 错误:接成pcie_tx_p[0],导致link无法训练 |
Y12 | qsfp28_0_tx_reset_n | QSFP28 Port0 TX复位 | 连接Marvell PHY的RESET_N引脚 | 错误:未加拉电阻,默认高电平,PHY不工作 |
AB10 | hmc_spi_miso | HMC SPI MISO | 连接HMC的MISO引脚 | 错误:FPGA侧未配置为高阻输入,导致SPI总线冲突 |
注意:Bittware的原理图PDF里,有些信号名用了缩写(如
qsfp28_0_mod_prs_n),而Corundum代码里用的是全称(qsfp_mod_present_n)。这种命名不一致是初期编译报错的高频原因。我的做法是,在Corundum的top.v里,用assign语句做一层信号名重映射,而不是直接改原始代码,方便后续升级。
3. Corundum RTL层改造——从“通用模板”到“VV4专属”
Corundum的代码仓库结构非常清晰:rtl/目录下是所有硬件逻辑,lib/是通用IP库(AXI, PCIe, Ethernet MAC等),example/里是针对不同开发板的参考工程。VV4没有现成的example,所以我们必须自己建一个。这个过程不是“复制粘贴”,而是一场精密的外科手术:切掉不适用的部分,缝合新的接口,加固薄弱环节。
第一步,创建example/vv4/目录。里面放四个核心文件:top.v(顶层模块)、vv4_defines.vh(板级参数定义)、vv4_constraints.xdc(约束文件)、build.sh(构建脚本)。其中,vv4_defines.vh是灵魂。它定义了所有VV4特有的参数,比如:
// vv4_defines.vh `define VV4_PCIE_GEN 4 `define VV4_PCIE_LANE_COUNT 16 `define VV4_QSFP28_COUNT 2 `define VV4_QSFP28_DATA_WIDTH 32 // 4x25G = 100G, 并行总线宽度32bit `define VV4_HMC_SPI_FREQ 10_000_000 // HMC SPI时钟频率10MHz这些宏定义会被top.v和下游模块引用,确保整个设计的参数一致性。如果某天你要把设计迁移到另一块支持PCIe Gen5的卡上,只需改这里,不用动任何逻辑代码。
第二步,重写top.v。Corundum官方的top.v(比如vc709_top.v)是一个巨大的拼图,把PCIe wrapper、MAC、DMA engine、AXI interconnect、DDR controller等模块用assign和wire连起来。对VV4,我们必须做三处关键手术:
- 替换PCIe Wrapper:删掉Xilinx的
pcie_axiwrapper,换成Intel Agilex的pcie_hard_ipwrapper。这个wrapper不是Corundum自带的,需要从Intel Quartus Prime的IP Catalog里生成,然后导出为Verilog。关键是要正确配置它的BAR大小、MSI-X能力、以及Completion Timeout值。我最初设的Completion Timeout是10ms,结果在高负载下频繁出现Transaction Layer Packet (TLP) timeout,改成100ms后问题消失。这是因为VV4的PCIe链路可能经历更长的路由延迟。 - 重构QSFP28 PHY Interface:Corundum默认假设PHY是“透明”的,即它只管收发并行数据。但VV4上的Marvell 88X3310 PHY需要一系列寄存器配置才能进入100G KR4模式。这部分逻辑不能写在Corundum的MAC里,而要单独做一个
phy_init_fsm.v状态机,在FPGA上电后,通过MDIO总线(由FPGA GPIO模拟)向PHY发送约20条配置命令。这个FSM必须有超时机制和错误回滚,否则PHY初始化失败,整个NIC就瘫痪了。 - 集成HMC接口:在
top.v里实例化一个hmc_spi_slave.v模块。它非常简单:一个SPI时钟分频器、一个8-bit移位寄存器、一个状态机。当HMC发起读操作时,它返回一个8-bit的状态字,bit0表示FPGA是否ready,bit1表示是否检测到QSFP28模块,bit2-7保留。这个模块的输出必须连接到hmc_alert_n信号,当状态字表明异常时,拉低此信号通知HMC。
第三步,编写vv4_constraints.xdc。这是让Quartus知道“哪个pin对应哪个信号”的宪法文件。它比Vivado的XDC更严格。一个典型的约束是:
# 设置QSFP28 RX clock为全局时钟 create_clock -name qsfp28_rx_clk -period 4.0 [get_ports {qsfp28_0_rx_clk_i}] set_clock_groups -asynchronous -group [get_clocks {qsfp28_rx_clk}] -group [get_clocks {fpga_main_clk}] # 约束PCIe TX/RX differential pair set_instance_assignment -name IO_STANDARD "DIFFERENTIAL 1.2V SSTL" -to pcie_tx_p[0] set_instance_assignment -name CURRENT_STRENGTH_ONE_DRIVE 8MA -to pcie_tx_p[0] set_instance_assignment -name OUTPUT_DATA_RATE 16G -to pcie_tx_p[0]这里的关键是set_clock_groups命令。它告诉工具:qsfp28_rx_clk和fpga_main_clk是异步的,不要尝试做跨时钟域的时序分析,否则会报出海量的false path。而OUTPUT_DATA_RATE 16G则强制工具按PCIe Gen4的速率进行布线优化。
实测心得:在Quartus中,Synthesis阶段耗时最长,但真正决定成败的是Place & Route(P&R)。我第一次P&R失败,报错是“Failed to meet timing on critical path”。查了半天,发现是
qsfp28_rx_clk的create_clock命令漏写了-waveform参数,导致工具误判了时钟的占空比。加上-waveform {0 2}(表示0ns上升,2ns下降)后,P&R一次通过。这种细节,只有亲手调过三次以上才会记住。
4. Linux驱动适配与Bring-up实战——让内核认出你的NIC
Bitstream烧录成功,只是万里长征第一步。接下来,要让Linux内核不仅识别出这块PCIe设备,还要把它当作一个真正的网络接口(eth0),能ifconfig up,能ping,能跑iperf3。这个过程,是RTL工程师和Linux驱动工程师的交界地带,也是最容易卡住的地方。
首先,确认设备被PCIe枚举出来。插上VV4卡,启动服务器,运行lspci -vv -s $(lspci | grep -i "corundum" | awk '{print $1}')。你应该看到类似这样的输出:
04:00.0 Ethernet controller: Device 1172:0001 Subsystem: Device 1172:0001 Control: I/O+ Mem+ BusMaster+ SpecCycle- MemWINV- VGASnoop- ParErr- Stepping- SERR+ FastBk- DisINTx+ Status: Cap+ 66MHz- UDF- FastBk+ ParErr- DEVSEL=fast >TAbort- <TAbort- <MAbort- >SERR- <PERR- INTx- Latency: 0, Cache Line Size: 64 bytes Interrupt: pin A routed to IRQ 45 Region 0: Memory at a0000000 (64-bit, prefetchable) Region 2: Memory at a0010000 (64-bit, prefetchable) Capabilities: [40] Power Management version 3 Capabilities: [50] MSI: Enable+ Count=1/1 Maskable- 64bit+ Capabilities: [70] Express (v2) Endpoint, MSI 00注意几个关键点:Device 1172:0001是Corundum的Vendor ID和Device ID,必须和你在RTL中设置的pcie_id一致;Region 0和Region 2是两个BAR(Base Address Register),分别对应DMA描述符环和寄存器空间;MSI+表示启用了Message Signaled Interrupt,这是高性能NIC的标配。
如果lspci看不到设备,或者看到的是Unknown device,那问题一定出在PCIe Hard IP的配置上。最常见的原因是:pcie_id设置错误,或者pcie_link_up信号没有正确反馈给Hard IP的link_up端口。
其次,加载Corundum内核驱动。Corundum提供了标准的Linux kernel modulecorundum.ko。编译它需要内核头文件和正确的.config。我推荐的做法是:在目标服务器上,用uname -r获取当前内核版本,然后下载对应版本的linux-source包,解压后进入目录,执行:
make menuconfig # 确保 CONFIG_NET_VENDOR_CORUNDUM=y make modules M=drivers/net/ethernet/corundum sudo insmod drivers/net/ethernet/corundum/corundum.ko加载成功后,dmesg | tail应该能看到:
corundum 0000:04:00.0: enabling device (0000 -> 0002) corundum 0000:04:00.0 eth0: registered on PCI bus 0000:04:00.0 corundum 0000:04:00.0 eth0: Corundum 100G NIC, MAC address: 00:11:22:33:44:55但这时ip link show可能还看不到eth0。这是因为驱动虽然注册了,但还没有完成硬件初始化。Corundum驱动会在probe函数里,向FPGA的寄存器空间写入一系列初始化序列,包括:复位MAC、配置MTU、使能RX/TX队列、设置中断掩码。这个过程需要几毫秒。如果驱动在写某个寄存器时超时(比如readl_poll_timeout返回-EINVAL),说明FPGA逻辑没有正确响应。此时,回到UART debug输出,看FPGA是否打印了“MAC init done”之类的日志。如果没有,问题就在RTL层的寄存器解码逻辑上。
最后,是性能调优。刚起来的eth0,iperf3 -c <server> -t 30可能只能跑到60Gbps。瓶颈通常在两个地方:
- RX Ring Size:Corundum默认的RX descriptor ring size是1024。在100G线速下,这个大小会导致频繁的中断和CPU上下文切换。我把它改成了8192,并在
/etc/default/grub里添加net.core.netdev_max_backlog=5000,重启后提升到85Gbps。 - IRQ Affinity:
cat /proc/interrupts | grep corundum找到对应的IRQ号,然后用echo 1 > /proc/irq/<irq>/smp_affinity_list把中断绑定到一个专用CPU核心上,避免和其他进程争抢。这一步能让CPU利用率从95%降到60%,吞吐稳定在92Gbps。
踩坑实录:有一次,
dmesg里反复出现corundum 0000:04:00.0: DMA write error。查了好久,发现是Region 0的BAR地址被内核映射到了一个非cacheable内存区域,而Corundum的DMA engine期望的是write-combining。解决方案是在驱动的probe函数里,调用ioremap_wc()而不是ioremap_nocache()来映射BAR0。这个细节,只有在看了Intel Software Developer’s Manual Vol. 3A Chapter 11之后才恍然大悟。
5. 验证闭环:从“Link Up”到“99.999% uptime”
一个成功的移植,最终要落在可量化的、生产环境级别的指标上。不能只满足于“能ping通”,而要建立一套完整的验证闭环。我在VV4上搭建了一个四节点测试床:一台作为Corundum NIC的服务器(DUT),一台作为流量发生器(使用Moongen + DPDK),一台作为接收端(同样用DPDK),一台作为控制台。整个验证流程分三个层次:
第一层:功能验证(Functional Verification)
ethtool eth0:检查link speed是否为100000Mb/s,port是否为FIBRE,driver是否为corundum。tcpdump -i eth0 -c 1000 icmp:抓1000个ICMP包,确认无FCS错误、无truncated packet。iperf3 -c <receiver> -P 4 -t 60:用4个并行流跑1分钟,记录平均吞吐、抖动(jitter)和丢包率(loss)。合格线:吞吐≥90Gbps,抖动≤50μs,丢包率=0.000%。
第二层:压力验证(Stress Testing)
stress-ng --vm 4 --vm-bytes 2G --timeout 1h:在DUT上同时运行内存压力测试,观察iperf3吞吐是否波动。理想情况是波动<1%。./moongen.lua --duration=3600 --rate=100000000000:用Moongen生成线速100G的UDP流,持续1小时,监控/sys/class/net/eth0/statistics/下的rx_dropped和tx_errors计数器。它们必须保持为0。
第三层:可靠性验证(Reliability Validation)
for i in {1..100}; do echo "Test $i"; sudo reboot; sleep 120; ip link show eth0 | grep "state UP"; done:连续100次冷重启,每次重启后检查eth0是否自动up。这是检验BIOS、HMC、FPGA bitstream加载流程稳定性的终极考验。watch -n 1 'cat /sys/class/hwmon/hwmon*/temp1_input':监控VV4板载温度传感器,在满负载下,核心温度应稳定在75°C以下。超过85°C,HMC会触发降频保护,导致吞吐骤降。
这套验证流程,我跑了整整一周。最惊险的一次,是在第72次重启测试中,eth0没有up。dmesg显示corundum: probe failed: timeout waiting for MAC reset. 拆开看,发现是FPGA的mac_reset_n信号在上电瞬间有一个短暂的glitch,被PHY误认为是复位指令。解决方案是在top.v里,给mac_reset_n加一个20ms的上电延时电路(用一个计数器实现),确保它在所有电源稳定后再释放。这个20ms,是Bittware硬件手册里明确写的“Power-On Reset Hold Time”。
最后分享一个小技巧:Corundum的
corundum.ko驱动支持一个隐藏的debug参数debug=1。加载时用sudo insmod corundum.ko debug=1,它会在dmesg里打印每一笔DMA描述符的提交和完成,以及每一个RX packet的长度和校验和。当你遇到偶发性丢包时,这个日志就是唯一的救命稻草。我曾经靠它定位到一个极其隐蔽的bug:在高并发下,FPGA的RX FIFO的rd_en信号有时会多打一个脉冲,导致同一个packet被读了两次,第二次读到的是无效数据,被驱动丢弃。修复方法是在FIFO的读控制逻辑里,加一个简单的“读使能防抖”状态机。
这个移植项目,远不止是把一段开源代码搬到一块新板子上。它是一次对现代高速网络硬件栈的深度解剖:从硅片上的晶体管开关,到PCIe协议栈的TLP封装,再到Linux内核的SKB内存管理,最后到应用层的TCP拥塞控制。每一步,都要求你既懂硬件时序,又懂软件抽象,还得有把两者拧在一起的工程直觉。Corundum的价值,正在于此——它不给你一个现成的答案,而是给你一张足够详细的地图,让你自己走出一条路来。这条路的终点,不是“能用”,而是“可靠、高效、可维护”。而这,正是所有真正有价值的开源硬件项目的终极目标。