上个月我把一块吃灰很久的UltraScale+板卡搬出来,配好QSFP28光模块和100G网卡,打算把开源的100G FPGA UDP协议栈移植上板。原本以为两天能搞定的事,结果从选型、源码集成、CMAC例化到真正的iperf3打流,前前后后折腾了近两周。这篇博文就是整个过程的完整记录:怎么挑开源方案、移植时哪些接口最容易翻车、上板测试到底怎么打流、最终实测能跑到多少,以及我踩过的几个坑。
这篇文章适合手里正好有100G板卡想做UDP加速的工程师,也想给那些在"商用IP太贵"和"开源方案不敢用"之间犹豫的人做个参考。
1. 为什么非要折腾100G FPGA UDP:软协议栈扛不住,商用IP又太贵
先说说背景。项目需求其实很简单:需要一块能持续收100Gbps数据的板卡,把实时数据流拆包后交给后端处理,丢包率要求极低。一开始我们评估过两个方向:
- 用服务器自带的高性能网卡(Mellanox ConnectX-5/6这种),靠DPDK或AF_XDP收包。这个方案吞吐没问题,但如果想在数据路径里做用户自定义处理(比如按端口分流、特定报文特征过滤,甚至在内联做加密),灵活性就差了很多。网卡offload功能被绑定死,很多定制需求实现起来非常费劲。
- 用FPGA做网卡方案,把网口控制权完全掌握在自己手里。数据怎么拆、往哪送、要不要过滤,都是RTL说了算。可一旦涉及到100G MAC,事情就不那么轻松了。
商用100G MAC IP核价格不低,而且授权流程长。授权周期和价格,对个人或中小团队来说都是实实在在的门槛。幸运的是这几年开源FPGA生态发展得不错,比如大家都知道的Alex Forencich那套verilog-ethernet,已经能较完整地支持10G/25G/100G MAC和UDP协议栈,这给了我们一个可行路径。
那为什么是UDP而不是TCP?做硬件的应该都清楚:UDP无连接、无状态、不需要维护拥塞窗口和重传队列,状态机非常简单,非常适合FPGA逻辑实现。而TCP的状态机太复杂,开源项目里虽然有做得好的,但要做到100G线速运行的TCP offload engine(TOE),难度完全是另一个数量级。所以这次移植先把UDP打通,是符合实际情况的第一步。
100G UDP本身的实际应用场景其实很多:高速数据采集后端(示波器、探测器数据回传)、网络流量发生器(打流工具)、流表转发设备(SDN数据面)、分布式存储节点间的数据传输等等。这些场景的共同特点是:要么需要极低延迟,要么需要极高的自定义程度。FPGA + UDP的组合恰好够用。
2. 开源方案选型:100G UDP核挑来选去,核心看这五件事
开源100G UDP方案,绕不开的项目其实就几个。核心是Alex Forencich的verilog-ethernet,另有一些基于它的二次封装项目,比如Corundum。我在选型时重点看了以下五点:
| 选型维度 | 关注点 | 我的判断 |
|---|---|---|
| License | 是否允许商业闭源使用 | MIT/BSD类优先,GPL类直接排除 |
| 模块完整性 | 是否包含MAC、UDP、ARP、CRC等 | 缺一不可,否则又要自己写一堆胶水逻辑 |
| 速率范围 | 是否原生支持100G | 只支持10G/25G的项目,强改到100G很痛苦 |
| 活跃度 | 最近是否有commit、issue回复情况 | 长期不维护的项目,遇到bug只能自己啃 |
| 接口风格 | AXI4-Stream是否标准 | 方便和DMA、FIFO等对接 |
2.1 各家方案对比
verilog-ethernet:这个项目其实更像一个"以太网组件库",包含10G/25G/40G/100G的MAC,以及UDP、ARP、PTP等上层模块。每个模块都是独立、参数化的,可以自己拼装。License是MIT,代码风格规范,模块间基于AXI4-Stream接口。
Corundum:相当于在verilog-ethernet之上做了一整套100G网卡方案,还带PCIe DMA、多个队列、流分类等数据中心网卡功能。功能非常强大,适合想直接做成品网卡的人。不过它提供的是一整套系统,如果你只想用其中一部分,裁剪成本比直接用verilog-ethernet更高。
其它还有像open-nic、or-tools之类,但下载量和活跃度都不如上面两个,遇到问题都没处问。
2.2 为什么最终选了verilog-ethernet
最后选了verilog-ethernet,主要原因就是"克制"。它没有替你把整个网卡做成黑盒,而是给出了标准、清晰的积木块:MAC就是MAC,UDP就是UDP,你要哪块拿哪块,不需要的绝对不会被编译进来。这次移植需要的是100G MAC + UDP收发,正好是它覆盖得最扎实的部分。
另外一点是它的代码风格很稳。寄存器命名规范,时序边界清晰,内部带有跨时钟域处理,不是那种"仿真能过、上板就挂"的示例代码。我在移植时改动的部分很少,主要精力花在板级集成上。
2.3 硬件准备:板和光模块
移植前先确认硬件,否则后面全是坑。我这里用的是Xilinx UltraScale+系列的板卡(自带的100G Ethernet Subsystem硬核,也就是CMAC),光口是QSFP28。如果你的板子上用的外部PHY方案(比如Synopsys的DesignWare Eth QoS用外部PHY),驱动方式会有所不同,这篇文章的后半部分我会专门说一下差异。
时钟方面需要注意,100G Ethernet通常需要两个时钟域:
- 参考时钟:给CMAC/PCS使用,频率一般是161.1328125MHz或322.265625MHz。
- 用户逻辑时钟:CMAC输出到用户侧的时钟,在512bit位宽下大约是322.265625MHz。
这些时钟必须保证来源正确,很多移植问题的根源都在参考时钟上。上板之前,用频谱仪或者说至少用Vivado里的眼图监测看一下时钟状态,能省掉后面几天的调试时间。
3. 移植过程实录:从克隆源码到第一个数据包正常上线
这次移植的整体流程是:先克隆代码,然后在Vivado里建一个测试工程,把MAC和UDP部分接起来,先跑仿真验证逻辑正确性,再上板。
3.1 源码克隆与工程组织
基于当前主分支版本移植,把rtl/目录全部加入Vivado工程,不需要的模块最终综合时会因为悬空被优化掉,但为了后续维护方便,我只加入了用到的文件。先大体说一下用到哪些模块:
eth_mac_100g:100G MAC核心。负责以太网帧的封装、拆解,处理前导码、FCS校验等。它适配Xilinx CMAC硬核,或者在仿真的时候可以使用标准接口。udp_complete:UDP收发完整模块,含UDP校验和生成、验证。axis_fifo:AXI4-Stream FIFO,用于缓存。eth_axis_rx/eth_axis_tx:在MAC和用户逻辑之间做以太网帧格式转换,剥离/添加以太网头。
由于100G接口位宽大,MAC核心与用户侧的数据宽度一般设置为512bit。当线速100G时,AXIS时钟约322.265625MHz。这个频率在UltraScale+上综合起来本身没问题,但如果用户逻辑在这条路径上写得太深,时序就开始紧张了。
3.2 顶层的例化流程
顶层逻辑步骤大致如下:
- 例化CMAC,或者说通过Xilinx IP Catalog生成100G Ethernet Subsystem,然后在工程里用verilog-ethernet的
eth_mac_100g作为适配层,把它和CMAC连接起来。 - 例化
eth_axis_rx和eth_axis_tx,把MAC侧的AXI-Stream转成内部更易处理的接口。 - 例化
udp_complete,配置成"只处理指定端口"模式,这样非目标端口的报文会被直接丢弃,省去很多没用的处理。 - 用户侧自己写一个简单的收发逻辑:发送端定时构造UDP包,接收端把收到的payload通过串口或者ILA导出,先验证最基本的通路。
一个典型顶层例化片段大概是这样的感觉:
// 顶层集成示意(非完整代码) eth_mac_100g #( .DATA_WIDTH(512) ) u_mac ( .gt_rx_clk (gt_rx_clk), .gt_tx_clk (gt_tx_clk), // -- 与CMAC硬核/外部PHY的接口 -- // ... // -- 用户侧接收 -- .rx_axis_tdata (mac_rx_tdata), .rx_axis_tvalid(mac_rx_tvalid), .rx_axis_tlast (mac_rx_tlast), .rx_axis_tuser (mac_rx_tuser), // -- 用户侧发送 -- .tx_axis_tdata (mac_tx_tdata), // ... ); udp_complete #( .DATA_WIDTH(512), // 配置默认端口等 ) u_udp ( // ... );这段代码看起来简单,实际上前前后后接了一整天,因为100G的数据位宽带来的对齐问题确实让人头疼。尤其在MAC拆出/填充字节时,tkeep信号的组合逻辑非常容易被忽略。你不能把一个40G的设计简单地把位宽翻倍就往100G上怼,包边界的处理方式、CRC处理位置、对齐有限状态机的设计都是不一样的。
3.3 100G设计中容易忽略的"时序合法性"问题
在100G数据通路上,有一个经常被仿真掩盖掉的问题:tvalid和tready握手。仿真时大家都按理想情况来,可100G线速下,每拍512bit、频率接近322MHz时,一旦某个模块在start_of_frame时没有准备好,后续数据就没法连续,影响吞吐。要保证线速能力,整条数据通路必须做到"每个周期都能接收新的有效数据",不能有反压。
这里我建议,在移植阶段,先不必追求完整协议的正确性。先把#我的第一步定成"FPGA主动发包,PC收包"。PC端随便用Wireshark先看包结构,确认MAC头、IP头、UDP头的字节内容是对的。然后再做PC发包给FPGA,FPGA收到后通过ILA观察数据内容。以上全部通过后,才轮到真正的iperf3打流。
3.4 综合后的资源占用和频率
第一次综合时,资源占用大概是这样的水平(具体取决于项目和附加逻辑):
- 100G MAC相关(含CMAC):LUT约2万-3万,FF约3万-4万。如果是用硬核,这块的资源主要由PCS和适配逻辑贡献。
- UDP完整协议栈:LUT约8000-1.5万。
- 用户自定义的DMA/发送逻辑:LUT约5000-10000。
时序收敛一开始报了-0.3ns左右的slack,主要卡在UDP校验和生成器和MAC发送接口之间。解决方式是把校验和路径拆成两段流水,为checksum预计算在包头还未到达时就提前算好部分前缀和后缀,最终才压到了接近收敛的状态。
4. 上板打流测试:标称100G,实际跑多少?怎么测才不算耍流氓
移植后的第一步是"通":能收能发,包内容完全正确。第二步才是"快":打满线速。很多项目卡在第一步,但反而更常见的是"第一步还好,第二步怎么都上不去"。
4.1 搭建测试环境
我的测试环境是这样:
- FPGA板卡:Xilinx UltraScale+,QSFP28光模块,按100G速率先在BIOS/板卡层面确认光模块识别正常。
- 对端设备:一台带100G网卡的服务器,网卡是Mellanox ConnectX-5,直连。
- 测试软件:iperf3,记好版本,iperf3 3.7以上对高带宽UDP支持更好;另外准备Wireshark做包内容分析。
- 链路:使用单模光模块,QSFP28直连,确认光线长度在合适范围内。
4.2 从低速率热身到逼近线速
先别直接100G起跳,那样出问题根本没法定位。建议先让FPGA以大约1Gbps的速率发包,PC端用iperf3收,看丢包率、吞吐。确认无误后再逐步提高。
比如用iperf3测试UDP接收端套路:
# PC端,先作为接收端起一个UDP测试,端口5201 iperf3 -s # 然后调整FPGA发送端速率,逐渐逼近线速如果需要纯接收:FPGA作为一个静态接收端,PC端用iperf3发UDP数据,参数可以这么写:
iperf3 -c 192.168.1.10 -u -b 80G -l 1384 -t 30这里-b 80G是目标带宽,-l是UDP负载长度,推荐设置接近MTU范围的值,通常是1384字节(TCP/UDP/IP头占了多余字节,实际最大负载看具体配置)。小包测试是另一回事,后面单说。
4.3 100G的实测结果
在纯UDP发送方向,FPGA满载荷发包到PC端,iperf3实测大概在94Gbps上下浮动。这个数据和理论线速基本吻合。很多人不理解为什么100G口跑不到100Gbps,其实是因为以太网线上还有前导码、帧间隔(IFG)和帧校验序列(FCS)。按报文头+IP头+UDP头总共42字节开销、UDP载荷1384字节来算,理论最大UDP吞吐约为:
ETH帧长(不含前导码和IFG)= 42字节头部 + 1384字节UDP载荷 = 1426字节
UDP有效载荷率 = 1384 / (1426 + 8字节前导码 + 12字节IFG) ≈ 95.7%
然后和线速相乘,约95.7Gbps,实际跑到94Gbps以上已经相当接近上限。
在有反压、有DMA、有软件协议栈参与的场景下,能持续稳定跑90Gbps以上就算优秀。你要是看到有人晒"100G UDP打满",大概率是用了非常理想的小包/特殊帧结构,或者在统计口径上有争议。
4.4 用ILA和Wireshark联合定位问题
上板后如果发现收发异常,第一件事就是用Vivado的ILA去抓用户侧的AXI-Stream信号,确认数据、valid、ready、last的时序是否存在异常。比如在我这次测试中,PC端iperf3一旦报告丢包,我就在FPGA内部把rx_axis_tvalid && rx_axis_tready的有效接收计数和端口过滤后的计数做对比,立刻就能定位丢包是发生在大卡MAC接口附近,还是发生在UDP过滤之后。
另一个有效手段是PC端同时用Wireshark抓包。如果Wireshark里面能看到大量CRC错误或者错包,说明链路物理层有问题,优先查光模块、时钟和信号完整性。如果Wireshark完全没包,那问题大概率出在FPGA发包逻辑上,比如源MAC地址/目的MAC填错了。
5. 复盘:这次移植踩过的坑
整个移植过程踩了不少坑,挑有代表性的几个记录一下,这些坑在仿真阶段极难发现。
5.1 时钟复位设计的坑
100G这种高速设计,复位信号的释放是可复位逻辑和硬核顺利初始化的关键。常见问题是一个全局异步复位直接接到所有模块上,结果CMAC还没起来,用户逻辑的复位已经在跑了,两边出现"认为对方在线,实则没准备好"的假象。
解决办法是使用同步化的复位释放电路:在CMAC的user clock域里先打几拍,将复位信号与时钟对齐后再释放。特别要注意,CMAC自己会有tx_reset_done/rx_reset_done信号,你必须在这些信号拉高后再开始传输,否则MAC发送的第一个包大概率被截断。
5.2 校验和与CRC的"重复计算"问题
100G网络中,UDP报文在IP层的字段如果填了全零,某些接收端会拒收。IPv4 UDP的校验和是可以为0的,表示"未计算",但前提是整个IP头校验和正确;如果你在硬件里自动计算了UDP校验和却忘了开启"checksum offload"模式,那包到了PC端会直接被协议栈判为bad checksum并且丢弃。
这个问题在verilog-ethernet中表现为:UDP模块有一个开关控制是否计算校验和,而MAC层又有一个控制是否自动添加/校验FCS的开关。两个开关如果设置不当,就会出现"看起来有个包,但统计里全是错误"的情况。我踩过一次:MAC层同时被要求自动生成FCS和手动插入FCS,导致每个帧结尾多了4字节冗余,链路虽然能通,但吞吐少了一截,而且对端一直在报FCS错。
5.3 跨时钟域:用户逻辑时钟与MAC时钟的偏差
100G设计里,CMAC输出给用户侧的时钟并不是一个绝对固定值的时钟,不同IP配置下接到的时钟可能不一样。比如有的配置用户侧是512bit宽,时钟是322.265625MHz;有的配置是640bit宽,时钟是257.8125MHz。如果你在用户逻辑里默认"512bit@322MHz"去处理时序,但实际生成的CMAC Core却是640bit@257.8MHz,那么你的FIFO读写时钟完全对不上,表现为随机丢包或偶发卡顿。
这是"文档不看仔细"的典型代价。重新阅读CMAC IP的用户指南后在用户侧加了一个异步FIFO,彻底把两边时钟域隔离,问题才消失。
5.4 光模块的"假死"问题
QSFP28光模块上有很多控制引脚,比如TX_DISABLE、Reset、LPMODE。上电默认状态如果不确认,可能把光模块置于disabling状态或者低功耗状态,此时链路完全无光。一开始我查了很久MAC寄存器,后来才发现是TX_DISABLE引脚默认拉高了。大多数FPGA板卡会有EEPROM或者拨码开关控制,但板卡上电顺序不同,默认状态也会不同。
调试顺序建议:先ethtool eth0看对端是否检测到link,再看FPGA侧CMAC的link状态,最后才看数据通路。光模块的link up是基础中的基础。
5.5 性能瓶颈:你以为在MAC,其实在DMA
当测接收方向时,FPGA接收100G UDP数据后,如果只是简单丢弃或者通过UART打印,那无瓶颈。但如果要让数据进入DDR或者通过PCIe送到主机,瓶颈立刻转移到存储带宽和PCIe传输上。100G线速对应的数据量大约每秒12.5GB,这个吞吐对PCIe Gen3 x16已经接近极限,对DDR4控制器的带宽也是一场压力测试。
所以如果你想做的是完整的100G数据采集或网卡功能,我强烈建议在一开始就把DMA和缓冲区带宽纳入规划,不要只盯着MAC和UDP。我这次测试,接收方向实测只能稳定跑到62Gbps的吞吐,瓶颈就出现在DMA写的page分配和cache一致性处理上,和数据通路没有关系。
6. 从100G UDP再往前:扩展方向和更实际的建议
如果你这次移植跑通了,后面有几个方向可以继续推进。
把UDP变成更完整的传输层能力:比如支持多端口、支持ARP应答、支持可选校验和策略、支持VLAN标签处理。这些都是相对独立的小模块,verilog-ethernet本身都有参考实现,直接拿过来接上就能用。
再进一步,如果要做成真正的100G网卡,那就绕不开PCIe DMA。这个方向可以直接参考Corundum项目的实现思路,它的队列管理、描述符环、流分类在开源方案里算是相当完整的。但在扩展开工之前,先把UDP链路的丢包定位方法论沉淀下来,成为团队的通用调试手段,比堆新功能更有价值。
给正在上板的人三个最实用的建议:
- 拿着板卡先花一个晚上反复看100G Ethernet相关的用户指南,尤其是时钟、复位、状态寄存器、用户接口时序。看懂了再动手,能避免80%的初期问题。
- 仿真能过的部分不要掉以轻心。时序收敛不等于上板正确,上板正确不等于线速稳定。每一层都要做明确验证。
- 打流测试前,把Wireshark抓包、网卡性能参数、iperf3版本这些"周边"准备好再开始。因为到了上板打流阶段,真正令人痛苦的往往不是FPGA逻辑本身,而是你根本不确定对端的工具统计口经。
这次100G UDP移植的成功,让我更确认了一件事:开源方案在高速网络方向已经完全可以作为入门和产品原型的起点。它省下来的授权费用和时间,足够你踩完所有该踩的坑,还能剩不少预算去吃顿好的。