做100G网络传输,FPGA这边一直是个硬骨头。高性能网卡动辄几千块,通用处理器跑到线速要堆一堆核,而FPGA做UDP协议栈的优势在于确定性延迟和可定制的数据路径。最近我把一套开源的100G UDP方案完整移植到了自家板卡上,跑通了上板测试,把整个过程的思路、改动的关键点、排查过的坑整理出来,给打算入坑高速网络方向的同行一个参考。
这套方案的核心不复杂:底层用厂商的100G Ethernet IP负责物理层和MAC,上层用开源UDP协议栈逻辑接管ARP、IP、UDP的解析和封装。好处是代码全部可见、可改,出了问题能直接看RTL波形,不用对着黑盒IP猜。适合的场景包括高速数据采集回传、网络测试仪、分布式存储节点互联,以及做RoCEv2之类方案前先验证UDP数据通路。针对不同基础的读者,我的建议是先跑通IP核自带的回环测试,再逐步替换用户逻辑,不要上来就想着自己写PCS和MAC。
1. 项目概述与方案选型
1.1 为什么是UDP而不是TCP
很多第一次接触这个项目的人会问,100G都上了,为什么不用TCP?原因不复杂:TCP的状态机、滑动窗口、重传机制、拥塞控制,每一样在通用处理器上都有成熟实现,但在FPGA里每多一个状态都是在跟时序和资源较劲。100G线速下,最小以太网帧(84字节含前导码和IFG)的理论速率约为每秒1.488亿个包,意味着纳秒级就要处理一个帧,留给你做复杂逻辑判断的时钟周期极其有限。
UDP无连接、无确认、无重传,协议栈只需要完成四件事:解析目标MAC/IP/端口,查找ARP表项,组帧发送,接收时做校验和剥头。整个状态机简单到可以用一个有限状态机加几个FIFO搞定。所以对于FPGA做高速数据传输,UDP基本是唯一现实的选择。如果你的业务需要可靠传输,可以在上层自己加确认和重传机制,或者直接把UDP承载的数据交给后端CPU处理,这比在FPGA里硬啃TCP要灵活得多。
1.2 开源协议栈怎么选,我对比了哪些方案
开源FPGA以太网方案,目前社区里活跃度最高的是Alex Forencich维护的verilog-ethernet项目,支持从1G到100G的完整MAC和协议栈。另外还有Corundum这个更偏向完整NIC(网络接口卡)的项目,自带PCIe DMA和多个队列。我这次项目没有选择Corundum,原因是它的复杂度太高,需要同时搞定PCIe硬核和DMA引擎,而这个项目的目标是验证数据通路,直接给用户逻辑供数,暂不需要接入PCIe。
实际对比下来,verilog-ethernet的优势非常明显:
- 代码模块化清晰,
eth_mac、eth_udp、eth_arp各司其职,修改任意模块不影响整体结构 - 提供AXI-Stream标准接口,方便和各种FIFO、DMA对接
- 支持Jumbo帧,默认可以到9600字节
- 参数化做得很好,数据位宽、FIFO深度、ARP表大小都可以通过参数调整
- 许可证宽松,商用友好
相比之下,有些闭源IP核从表面看“开箱即用”,但遇到问题只能提工单,等待周期长,而且协议栈层面的可定制性几乎为零。做高速网络方向,我始终倾向于找开源代码做底座,哪怕初期需要多花时间读代码,后面排障会顺畅得多。
1.3 整体架构和资源评估
整个系统的数据流是:用户逻辑(或者测试数据生成器)将数据写入TX FIFO,UDP发送引擎按配置的源/目的MAC、IP、端口组装帧,交给MAC校验CRC后送入100G Ethernet IP,再经过GTY/GTM高速收发器发到光模块。接收方向则完全对称:光模块进来的数据经100G IP上送到MAC,UDP接收引擎剥掉头部,把payload写入RX FIFO,用户逻辑从这里取数。
这个架构在Xilinx UltraScale+系列上,资源占用大概是:查找表(LUT)约3到4万,触发器(FF)约3.5万,块内存(BRAM)约100到140个36Kb块,主要消耗在收发FIFO上。100G数据路径的位宽是512bit,时钟频率为322.265625MHz。这里面数据宽和频率的配合非常关键,512bit x 322MHz就是100G线速,任何一端的FIFO读写效率不满都会导致吞吐下降。
2. 移植适配的关键环节
2.1 从源码到工程文件,第一步是理清模块依赖
从GitHub拉下verilog-ethernet仓库后,我先做的不是直接编译,而是通读了一遍rtl目录下的模块清单,把依赖关系画清楚。100G相关的最核心模块有这么几个:
eth_mac_10g:10G/25G/100G通用MAC内核,支持AXI-Stream接口eth_udp_10g:UDP协议栈,包含ARP请求/响应、IP层转发、UDP封装/解封装eth_phy_10g:适配厂商10G/25G/100G Ethernet IP的接口封装eth_axis_rx、eth_axis_tx:将MAC帧与AXI-Stream互相转换的适配层
读代码时我发现很多模块是通过reset_sync、axis_fifo这些基础组件组合起来的,如果直接复制整个rtl目录进工程,Xilinx Vivado会自动综合很多用不到的模块,导致综合时间极长。我的做法是手动挑选需要的文件加进工程,保持最小依赖集。这个过程有点繁琐但值得做,后续每次修改综合时间都能保持在几分钟内,迭代效率高很多。
2.2 适配自家板卡的时钟与复位难点
上板测试之前,时钟是第一个拦路虎。100G Ethernet IP需要的参考时钟频率因线速率不同而不同,比如我们用的线速率是103.125Gbps(100G以太网实际速率),四路GTY参考时钟输入典型值是161.1328125MHz或322.265625MHz。如果板卡上晶振没有这个频点,就需要通过时钟芯片动态配置,或者用MMCM/PLL从备用频率合成。
我在这次移植中踩过的最深一次坑就在参考时钟上。板卡默认提供了156.25MHz的时钟,我一开始想当然地认为以太网IP会自动做频率转换,结果GTY的QPLL一直无法锁定,rx_gt_lol信号拉高,物理层完全不通。后来查了IP核手册才发现,100G Ethernet IP内部虽然也有时钟管理,但参考时钟必须在其要求的容差范围内。最后我在板卡上写了一个简单的I2C配置序列,把时钟芯片从156.25MHz切换到了322.265625MHz,再配合IP核的配置,QPLL才稳定下来。
复位逻辑也是同样的道理。FPGA上电后各个电源轨的稳定时间不同,高速收发器的复位释放至少要在参考时钟稳定之后,否则会出现偶发的“首包丢、后续正常”现象。我建议至少留三组复位:全局复位、GT复位、用户逻辑复位,在时序上逐级延时释放,级间延迟可以做到几毫秒量级。
2.3 用户接口对接:AXI-Stream的握手机制
UDP协议栈对外是标准的AXI-Stream接口,发送方向有m_axis_tx_tdata(512bit)、m_axis_tx_tkeep、m_axis_tx_tvalid、m_axis_tx_tready,接收方向对应s_axis_rx_*。AXI-Stream的核心就是tvalid和tready的握手:发送方拉高tvalid表示数据有效,接收方拉高tready表示准备好接收,两者同时拉高时一个数据传输完成。
如果tvalid已经拉高而tready为低,发送方必须保持数据不动,这是AXI-Stream最基础也最容易出错的地方。我在第一版用户逻辑里图方便,用了一个简单的状态机产生数据,当FIFO空时直接拉低tvalid,结果上游FIFO的读指针已经前移,数据直接丢了。查了半天发现是握手时序问题。正确处理方式是:只有同时满足tvalid和tready才允许推进源端FIFO。
另一个需要留意的是tkeep和tlast。发送端在每包末尾要正确设置tlast为高,表示帧结束,tkeep实时指示最后一个周期哪些字节有效。如果tkeep计算错误,会导致接收端把填充字节当成有效数据,CRC大概率出错,丢包率会非常诡异。
3. 上板测试的完整流程
3.1 测试环境搭建:硬件和工具链准备
上板测试前,我把环境分成了三层:
第一层是硬件连接。本次使用的板卡是Xilinx UltraScale+ VCU118评估板,它自带两个QSFP28光口,每个光口可以拆成4路25G或者直接跑100G。光模块用的是100G QSFP28 SR4,配套一根MPO光纤跳线。如果条件有限,没有光模块的情况下也可以直接把TX和RX回环到QSFP笼子的测试插座上,但这样只能验证数字逻辑,测不了模拟收发通路,建议完整测试还是上模块。
第二层是调试工具。Vivado的ILA(集成逻辑分析仪)是排查内部信号的利器,但要注意ILA本身会占用大量的BRAM和路由资源,在100G设计中采样深度不宜过大,否则影响布局布线。除了ILA,我还在工程里放了几个计数器寄存器,通过AXI-Lite总线读出,用于统计收发帧数、错误帧数,这比抓波形更高效,适合长时间稳定性测试。
第三层是上位机工具。Linux下用iperf3的UDP模式打流,Wireshark抓包分析,还有ethtool -S看网卡统计。如果电脑没有100G网卡,可以用10G/25G网卡配合交换机降速联调,但这样测不到线速,只能验证协议正确性。最终线速验证还是得两台100G设备对打。
3.2 内部回环测试:先把数字逻辑跑通
上板之后我第一步做的不是接光模块,而是先把MAC的TX直接回环到RX,也就是在FPGA内部把发送数据绕回接收通道。这一步的目的是把问题范围缩小到数字逻辑,排除光模块、线缆、模拟前端的干扰。
我写了一个最简单的测试模块:上电后发送固定数量的UDP包,包的payload从0递增填充;同时启动一个接收比较器,检查收到的payload是否和发送数据一致。然后我在ILA里抓了eth_udp_rx模块输出的数据,逐帧比对,确认MAC回环下数据和长度都正确。
这一步通过后,才把光模块插上,QSFP28的TX和RX用光纤连接到一个光电转换设备或者对端设备。内部回环测试时需要注意:如果TX FIFO读数据的速度跟不上MAC的发送速率,m_axis_tx_tready会周期性拉低,导致发送不连续。这种情况要在计数器中有所体现,否则测试结果会误导人。
3.3 外部主机联调:iperf3打流与丢包分析
数字逻辑跑通后,我进入外部联调阶段。一端是FPGA,用本方案发送和接收UDP数据;另一端是一台安装了100G网卡的服务器,操作系统为Ubuntu。我在这台服务器上把网卡设置为静态IP,例如192.168.1.1/24,FPGA侧IP设置为192.168.1.2/24,通过光纤直连,没有经过交换机。
先用ping验证链路是否通。UDP协议栈内置了ICMP回显功能,ping通说明ARP解析、IP层、MAC层都是通的,这是一个非常关键的里程碑。接着用iperf3在UDP模式下打流,命令大概是:
iperf3 -c 192.168.1.2 -u -b 100G -t 10 -l 1400-b 100G指定目标带宽,-l 1400指定UDP负载长度,这决定了每秒发包数量。这里有个细节:如果用默认的-l 8K,在100G下发包间隔极短,很多软件时间戳和调度会产生周期性抖动。我习惯先把包长调小,比如512字节,这样每秒包数更多,更容易暴露协议栈的瓶颈。
测试结果让我比较满意,在100G线速下发送端可以到99%以上的吞吐,接收端在开启较大socket缓冲区后也能跑到90G到95G。这里有个特别影响接收性能的参数是Linux UDP接收缓冲区,默认值很小,1Gbps下没问题,100G下直接就爆了。需要追加sysctl配置:
net.core.rmem_max = 67108864 net.core.rmem_default = 67108864否则接收端会大量丢包,而且丢的是内核socket缓冲区溢出,和FPGA协议栈没有任何关系,很容易让人误判定位。
3.4 收发帧计数器和错误统计器的设计
上板测试中我发现,无论外部工具报告什么结果,都不能替代FPGA内部的统计计数器。因为iperf3的丢包统计依赖包序号,而如果协议栈在解析时发生静默错误,某些包可能根本没有被计数,导致丢包率被低估或高估。
我在工程里加了四个核心统计寄存器:
- 发送总帧数(TX frame count):每次
m_axis_tx_tlast拉高时加1 - 接收总帧数(RX frame count):每次
s_axis_rx_tlast拉高时加1 - 接收错误帧数(RX error count):内部解析状态机进入error状态时加1
- 接收CRC错误计数:MAC层报错时加1
通过这些计数器和上位机的收发包数量对比,可以快速定位丢包发生在哪一级。比如外部发包100万,FPGA RX frame count也是100万,但应用层只收到99万,问题在内部FIFO或者DMA搬移;如果RX frame count就少于100万,问题在物理层或者MAC层。我强烈建议做100G项目的朋友都把这套计数器做成标准调试接口,它在后续调优中的作用比任何示波器都大。
4. 常见问题与性能调优
4.1 上板测试高频问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
rx_gt_lol拉高,链路不通 | 参考时钟频点不对或未稳定 | 检查时钟芯片配置、用IBERT误码仪验证GT通道 |
| ping不通但对端能收到ARP | ARP请求/响应帧格式错误或CRC错误 | 抓内部信号确认ARP帧内容,比对MAC地址字节序 |
| 小包吞吐正常,大包吞吐低 | 帧间隙(IFG)不满足或FIFO深度不足 | 检查MAC IP核IFG配置,增大TX/RX FIFO |
| 收发正常但偶发丢包 | 跨时钟域处理不到位、FIFO指针竞争 | 检查异步FIFO是否正确使用、复位释放是否同步 |
| 长时间运行后链路异常 | 温度漂移、GT通道眼图劣化 | 查看GT状态寄存器,做老化测试并加强散热 |
| 接收端缓冲区溢出丢包 | Linux socket缓冲区太小 | 调整net.core.rmem_max,加大接收缓冲 |
上面这些坑我基本都踩过一遍。尤其是参考时钟和跨时钟域这两个问题,几乎每个第一次搭高速链路的团队都会遇到。建议在项目初期把这些基础项用额外实验验证掉,不要带到联调阶段。
4.2 时序收敛与资源优化经验
100G设计对时序的要求极为苛刻。322MHz时钟下,一个周期只有3.1ns,而完整的数据路径包括FIFO读指针、协议状态机、CRC计算,组合逻辑路径稍长就会不满足时序。我遇到过一次在实现阶段时序违规导致bitstream生成失败,排查后发现是UDP校验和计算逻辑综合出了一长串加法链,关键路径延迟过大。
解决方案很常规但很有效:把校验和计算改成流水线实现,中间插入寄存器,让超长的加法链切分成两到三个周期完成。代价是数据路径多了几个周期的延迟,但对吞吐没有影响。另一个常用优化是给m_axis_tx_tdata和s_axis_rx_tdata这些高扇出信号加寄存器级,做输出打拍。在Vivado里可以用(* keep = "true" *)或者(* max_fanout = "X" *)约束辅助控制。
还有一点关于资源配置:Vivado综合时默认会推断很多分散的RAM和移位寄存器,导致BRAM利用率虚高。我建议在源码里直接用(* ram_style = "block" *)这类综合属性显式指定大FIFO使用BRAM,小FIFO使用分布式RAM,避免工具做次优决策。资源分配合理后,100G UDP协议栈整体可以控制在LUT 4万以内,这在多数UltraScale+器件上完全可行。
4.3 吞吐上不去时,如何定位瓶颈
如果测试中发线速只能跑到70G到80G,不要急着怀疑协议栈。我会按以下顺序排查:
先看TX侧。发送FIFO是否经常处于空状态?如果是,说明用户逻辑(或DMA)的读数据速率不足,协议栈本身没有瓶颈。这时加大FIFO深度、提高用户逻辑的突发读取长度,通常能立刻改善。再看MAC层是否有周期性反压。100G Ethernet IP的TX接口如果内部有背压,tready会周期性拉低,这与发送FIFO的空满状态共同决定了实际吞吐。
再看RX侧。最常见的是接收FIFO溢出,因为接收方向数据是突发到的,瞬时速率就是线速,而用户逻辑的读取可能是非连续的。解决思路是保证用户逻辑的消费速率不低于线速,或者在协议栈后端接一个足够大的缓冲,在统计学上吸收突发。
最后看接口本身。如果外部正好是两台100G设备对打,还要检查对端网卡的流控和卸载设置。某些网卡默认开启了LRO(Large Receive Offload)或GRO,会把多个包合并成一个大包上报,iperf3看到的包数会不对,但这不是丢包。测试时建议用ethtool -K eth0 lro off gro off关掉这些卸载功能,让统计口径保持干净。
4.4 Wireshark抓包与流量回放技巧
联调时Wireshark的作用不可替代,但在100G线速下,直接用普通网卡抓包基本不现实,因为PC的PCIe带宽和CPU性能不够,抓包本身就会成为瓶颈。我的做法分两种:
如果只是验证协议正确性,可以先把FPGA的发送速率降低,比如通过计数器控制发包间隔,让流量降到1Gbps级别,然后在服务器上开Wireshark或tcpdump抓包,重点看UDP头部、IP头部、MAC头部和CRC是否正常。
如果确实需要看线速下的行为,建议用专门的流量分析仪或者支持精准时间戳的网卡,普通消费级网卡抓到的数据本身就可能带噪声。另外,Wireshark的过滤表达式比如udp.port == 1234可以帮助快速筛选目标流,结合Follow UDP Stream功能可以直接看到payload内容,对于验证FPGA发送的数据格式非常方便。
我还常用一个技巧:在服务器上用tcpreplay重放之前抓到的pcap文件,输入到FPGA接收方向,验证协议栈对异常包的容忍度,比如包里CRC故意损坏、长度字段异常等场景。这种负向测试是安全验证不可缺少的环节。
这次整体移植和测试跑下来,我最大的体会是:100G UDP在FPGA上并不存在原理性障碍,真正的复杂度都在细节里。时钟的坑、握手的坑、时序收敛的坑,每个单拎出来都不算难,但如果没做过类似高速设计,很容易被一次上板就击穿信心。
如果让我给后来者一个最核心的建议,那就是在所有数据通路上加计数器,保证任意时刻都能回答“收发多少帧、丢了多少帧、丢在哪一级”。这个习惯让我的排障时间至少缩短了一半。
下一阶段我打算把PCIe DMA加进来,做一版真正意义的100G智能网卡雏形,同时把UDP的pipeline延迟压到最低。这个方向上手后其实有很多可以玩的空间,考虑到目前的开源生态已经相当完整,个人或小团队做高速网络的门槛已经比几年前低太多了。