FPGA跑通100G UDP协议栈:移植适配与上板测试全记录

FPGA跑通100G UDP协议栈:移植适配与上板测试全记录 做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 FIFOUDP发送引擎按配置的源/目的MAC、IP、端口组装帧交给MAC校验CRC后送入100G Ethernet IP再经过GTY/GTM高速收发器发到光模块。接收方向则完全对称光模块进来的数据经100G IP上送到MACUDP接收引擎剥掉头部把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_10g10G/25G/100G通用MAC内核支持AXI-Stream接口eth_udp_10gUDP协议栈包含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.125Gbps100G以太网实际速率四路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_tdata512bit、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/24FPGA侧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不通但对端能收到ARPARP请求/响应帧格式错误或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设备对打还要检查对端网卡的流控和卸载设置。某些网卡默认开启了LROLarge 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延迟压到最低。这个方向上手后其实有很多可以玩的空间考虑到目前的开源生态已经相当完整个人或小团队做高速网络的门槛已经比几年前低太多了。