ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

FPGA 100G UDP协议栈移植实战:从仿真到线速的完整指南

2026/9/7 20:36:39 拓冰建站 浏览量
FPGA 100G UDP协议栈移植实战:从仿真到线速的完整指南 先说一个我反复跟身边做FPGA的人强调的观点UDP在FPGA里不是一个“模块”而是一条物理链路。仿真testbench里跑得欢的UDP协议栈移植到真实板卡上经常第一步就死。原因不复杂——仿真环境把物理层抽象得干干净净复位和时钟也给得服服帖帖真实世界里的光模块、SerDes、参考时钟、电源纹波全都站在你的对立面。这篇文章记录的是我最近做的一件实事把一个开源的100G UDP协议栈工程从原本适配的Xilinx评估板完整移植到另一块带QSFP28光口的FPGA板卡上并最终通过iperf3打流验证到接近线速的全过程。整个过程中比较典型的坑我都踩了一遍重新生成厂商IP、改RTL顶层、时序收敛、上板后第一个灯就是不亮、RX方向打流丢包等等。如果你手里也拿到了某个开源的100G UDP项目或者正准备把别人的高速网络demo搬到自己的板子上这篇文章大概率能帮你省下几个通宵。1. 开源协议栈“能仿真却上不了板”的根因先把100G整条链路盘明白很多刚接触高速网络的人会下意识地问UDP不是很简单吗不就是组一个IP头、组一个UDP头、算一下校验和然后发出去吗理论上确实是这样但在FPGA里UDP是架在以太网这条物理链路上端的。一个UDP发送包要从用户逻辑走到光纤上至少经过这么几关用户逻辑构造UDP头 → IPv4头与校验和 → ARP表项或静态MAC配置 → MAC层封装前导码、目的MAC、FCS → PCS层64B/66B编码、扰码、RS-FEC → SerDes并行转串行 → 光模块电光转换 → 光纤。任何一层出问题上层看起来都是“UDP不通”。仿真环境之所以能跑是因为testbench通常把MAC之后全简化了给你一个现成的时钟和一个已经对齐好的数据流。而真实板卡上光模块有没有link、GT参考时钟是不是稳定、PCS有没有完成lane对齐、MAC包的FCS校验对不对这些都是仿真里根本不存在的前提条件。从我接触过的开源方案来看目前用得比较多的是Alex Forencich的verilog-ethernet库MIT协议接口基本是AXI4-Stream代码风格统一里面已经有eth_mac_100g、eth_arp_100g、eth_ipv4_100g、eth_udp_100g这些模块拿来做一个百G的UDP offload引擎很合适。但要注意这个库本身并没有完全实现100G的PCS和SerDes它需要配合FPGA厂商的高速以太网IP来用。在Xilinx平台上就是100G Ethernet Subsystem这个IP由它负责把MAC层往下直到GT SerDes整个物理层托底。所以移植工作量和难点其实并不在UDP协议栈本身而在于把厂商的MAC/PCS/GT这套底座在新板卡上重新立起来。10G工程换到100G也不是简单地把数据位宽从64bit改成512bit就能完事。10G只有一条lane100G是4条lane同时工作PCS要把数据在4条lane上分发接收侧要做lane对齐、deskew这些IP内部帮你做了但前提是IP配置和GT参考时钟必须正确。我见过的上板翻车案例十有八九是物理层就没起来后面UDP栈代码写得再漂亮也无济于事。2. 移植板卡的硬件盘点光口数量、GT参考时钟与复位怎么对齐拿到一块新板卡第一件事不是打开Vivado开始改代码而是先把原理图和板卡的资源文档摊在桌面上逐项确认硬件底座是否满足100G UDP工程的需求。这一阶段走快了后面全是坑。2.1 光口和高速收发器资源核对100G以太网在物理层上通常走一个QSFP28光模块内部是4条25G lane。FPGA这边你至少要确认三件事板卡上有没有QSFP28接口FPGA的GT资源够不够用这些GT的布线能不能连到光模块对应的引脚上。很多老开发板虽然带QSFP接口但那只是40G速率的光模块形态线速率规划完全不同。真正跑100G的板卡光模块和FPGA之间的高速lane必须是专门走线到支持25Gbps以上的收发器bank。你可以下一个简单的约束在Vivado里试着布局看目标MGT bank是否支持参考时钟和高速pin脚走线。我在这次移植里就碰到一个情况原工程是写给VCU118评估板的评估板上用了一组特定的GTY bank和156.25MHz参考时钟新板卡虽然也是UltraScale架构但那个bank没有引出到面板光口如果直接套用原工程的XDC就算布线成功也完全没有信号。所以第一个动作就是把原XDC里的GT location、参考时钟引脚全部删掉换成新板卡原理图上实际连到的位置。2.2 参考时钟100G系统最容易埋雷的地方100G以太网的CAUI-4线速率通常是25.78125Gbps这要求GT参考时钟常见为156.25MHz。但请注意不同板卡设计可能用不同的参考时钟方案有些板子为了兼容多种协议光口附近放了一个可编程时钟芯片给GT提供156.25MHz或者其它频率。在生成厂商IP时你必须把这个参考时钟频率填进IP配置向导如果填错了IP内部PLL锁定状态永远不稳定物理层起不来。这类问题有多隐蔽我见过一个案例板卡原理图上明明写着156.25MHz结果实际贴片贴了一个另一个频率的晶振调试时GT的tx/rx始终无法锁定查了两天才发现时钟源根本不对。所以上电后第一件事就是实际拿示波器或频谱仪量一下光模块附近的参考时钟输出频率不要理所当然地相信原理图标注。频率对了再量一下波形质量100G这种速率对参考时钟的抖动要求比较高有的板子用了普通的通用时钟buffer波形勉强能用但是长期稳定性差可能出现“早上跑得好好的下午灯就灭了”这种玄学故障。2.3 复位链不同板卡的电源好信号天差地别原工程的复位信号大概率是这么来的板载时钟锁定模块输出locked信号加上几个电源域的power good信号经过同步和展宽后生成全局复位。换到新板卡你得重新看新板卡的电源管理芯片有没有输出PG信号或者需要一个I2C读寄存器来确认电源正常。很多移植工程就用一个简单的按钮复位JTAG复位。这其实没有问题但关键是要保证复位释放时间足够长至少要等GT参考时钟稳定和IP的aligned信号到来之后再释放。我这次移植时图省事一开始直接用了一个按键复位结果发现每次上电后能否成功建立100G link是随机的——按键按得早就起不来按得晚就能起来。归根结底是复位释放太早GT还没有完成初始化。最后把复位信号改成“电源稳定 IP的tx_rst_done/rx_rst_done”共同控制问题彻底消失。高速系统的复位从来不是简单拉低再拉高它本质上是一个状态机依赖的时序过程。3. 重新生成IP和改RTL这一版的改动清单必须逐条核对硬件底座确认完接下来就是真正动手改工程。这个阶段最容易犯的错误是“觉得原工程很干净改个引脚就能用”。实际移植一次你就知道厂商IP、时钟模块、顶层实例、接口位宽几乎每一条都得重新过一遍。3.1 重新生成100G Ethernet Subsystem在Xilinx平台下原来的100G Ethernet Subsystem IP必须先在当前器件型号下重新生成。新板卡的FPGA型号哪怕只是封装不同GT位置和引脚约束都不同。重新生成IP时重点检查这几个参数线速率、参考时钟频率、数据路径位宽、是否启用RS-FEC。RS-FEC这个选项特别容易忽略。相同的板卡要是对端设备打开了RS-FEC而你这端没开两边link能起来但误码率会高到没法用。反过来一方开了另一方没开link建立过程就可能失败。一般100G长距离光模块链路建议打开RS-FECRS(544,514)短距离DAC线缆则可以关闭关键是确认两端模式一致。IP生成后原工程里的那些clk_wiz、MMCM/PLL也得重新来一遍。Xilinx评估板上的系统时钟方案是评估板专用的新板卡的输入时钟频率和引脚都不一样直接沿用原约束根本综合不过去。我习惯在IP内部把用户侧AXI时钟、GT参考时钟、寄存器配置时钟的关系理清楚再去生成时钟模块而不是反过来让时钟模块适应IP。3.2 砍掉评估板的演示功能重建干净的UDP顶层开源工程往往不只是UDP协议栈还带着一堆评估板demoPCIe、DDR4、UART、ILA、LED流水灯、温度监控甚至还有MicroBlaze软核。这些代码在评估板上能跑不代表在你的应用里需要也不代表换一块板卡还能综合过。移植的第一步就是狠心做减法把和“UDP收发”无关的模块全部摘掉只保留GT/MAC/PCS物理层、UDP/IP/ARP协议栈、用户侧AXI接口以及必要的状态监控寄存器。切开之后顶层会变得清爽很多。然后按照verilog-ethernet库的端口定义把eth_udp_100g、eth_ipv4_100g、eth_arp_100g依次实例化。注意这些模块之间的连接不能搞错eth_udp_100g下层接ipv4ipv4下层接arp和eth_maceth_mac再往下就是厂商IP的用户接口。这几个模块的位宽都是512bittkeep是64bit时钟域一致但各自的tvalid/tready握手逻辑必须对齐。很多移植工程在这里就悄悄埋下了一个雷原工程可能用了一些厂商特定的FIFO原语比如Xilinx的FIFO36或Block Memory Generator换到别的器件或工程后不一定还能用。如果代码里有这类原语建议统一替换成XPM_FIFOXilinx的跨平台FIFO原语或者干脆用标准AXI4-Stream FIFO。这样以后再移植都省事。3.3 改MAC地址、IP地址、端口号这些“硬编码”开源工程里MAC地址、IP地址、UDP端口这些参数十有八九写在RTL的parameter或localparam里而且很可能就是作者开发环境的地址。比如IP是192.168.1.128MAC是02:00:00:00:00:00。上板测试前一定要改成你自己网络的地址否则ARP表、IP过滤、UDP端口滤波统统对不上。这里要注意字节序问题。AXI总线上的数据从低位到高位排列MAC地址在帧里也有固定的字节顺序。很多人在仿真里看不出问题因为收发两端用的是同一套代码错了也“看起来正常”。但一旦和PC网卡通信PC端严格按照IEEE 802.3的字节序解析帧你的字节序反了ARP请求就没人响应。这个坑我见过太多次排查方法很简单在ARP模块输出前插一个ILA把发出的报文和标准Ethernet帧格式对一遍帧头前8字节一目了然。3.4 用户侧跨时钟域tready/tvalid不是你想的那么省心100G的MAC用户接口通常工作在几百兆赫兹的高频下而你的用户逻辑往往在另一个时钟域里可能是DDR控制器时钟也可能是PCIe时钟。两者之间必须插入异步FIFO这个大家都懂。但100G有个特点数据包的长度可能非常大尤其开了巨帧后一个UDP包最大可以到9000字节对应4500多个512bit周期的数据。如果你的异步FIFO深度只有1024或2048一个满包就已经把FIFO塞爆了tready会被长时间拉低MAC侧被迫反压整个链路吞吐掉下来。所以设计用户侧缓冲时深度不能按“平均包长”估算要按“最大帧长度 一个或多个帧的突发余量”来算。这一点在后面RX丢包排查时还会再讲但做RTL阶段就应该把FIFO深度留够。4. 时序收敛100G系统里SDC写得不够细板上就会随机出鬼仿真全通过、RTL也改完了接下来最磨人的一关就是时序收敛。100G用户侧时钟通常在300MHz上下3纳秒左右就一拍相比10G系统的156.25MHz时序余量空间被压缩了一大半。同样一段RTL在10G下跑得好好的搬到100G下就会出现大量的setup violation。4.1 最容易出现时序问题的几个位置第一个重灾区是位宽转换逻辑。比如你从512bit用户的AXI总线上取出数据要转成128bit给DDR写如果你的位宽转换是用一个大mux或纯组合逻辑实现的那一长串组合路径在300MHz下几乎必然violation。正确做法是设计流水线寄存器把一次转换拆成多拍如果拆不开就考虑用厂商提供的位宽转换FIFO。第二个重灾区是AXI握手信号。512bit总线的tready/tvalid组合逻辑往往涉及多个模块的仲裁一旦形成“a模块的tready依赖b模块的tvalidb模块的tvalid又依赖a模块的tready”这种组合环就会出现严重的时序问题。我在这次移植中就碰到一条4.2ns的组合路径追根溯源是代码里两个模块之间有一根反向的忙信号纯组合绕了一圈。解决办法是沿链路加入一到两级寄存器把忙信号打拍后再反馈出去牺牲一点点延迟换来回合格的时序。4.2 XDC约束的几个细节100G工程的约束和低速逻辑不一样我有几点体会一定把GT参考时钟声明成primary clock并显式设置到IP的时钟输入端口。如果漏了这条工具会把GT时钟当成普通数据或乱分组最后时序报告看到一堆莫名其妙的跨时钟域路径。异步时钟组要写清楚。用户逻辑时钟和MAC时钟如果本来就是异步的不要用set_clock_groups把全部时钟都设为false path而是要区分哪些路径真的不需要约束。如果一刀切全部async工具可能把必要的时序检查也优化掉了。GT本身的输入输出延迟通常不需要你手动填set_input_delay/set_output_delay厂商IP里已经有约束冒然添加反而会过约束。我第一次单独填了set_output_delay结果时序收敛反而更差拿掉后立刻好了。4.3 仿真没问题但上板偶发丢包很多时候就是时序问题功能仿真通过、上板也能link但打流时偶尔丢几个包、速度一大就出错这类问题很多人会去怀疑光模块、怀疑FIFO其实最容易被忽略的是布局布线后的时序余量。如果时钟频率只收敛了十几ps的余量温度一变化、电压一波动路径就不满足了出现偶发错误。我习惯在跑Implementation之后打开时序报告先看WNS最差负余量和TNS。WNS是正数但只有几十ps依然不敢掉以轻心。稳健的做法是把时钟频率余量留到5%以上也就是说用户时钟如果300MHz至少要能收敛到285MHz以下没问题。如果收不到就回到RTL里拆流水线不要靠碰运气上板。5. 上板实测全记录从link建立到iperf3跑满100G线速时序收敛、bit文件生成、烧录上板后真正的测试才开始。上板测试不是随便拿个网线插上就ping100G场景下链路建立、地址解析、打流验证每一步都有自己的一套顺序。5.1 上板前的硬件自查先别急着烧bit把这几项快速过一遍QSFP28光模块是否插到位光纤两端是否插紧对端设备是100G网卡还是交换机板卡供电是否正常尤其光模块的3.3V电源有没有异常JTAG下载器连接是否稳定。我有一次调了半天发现光模块没插到位link灯一直不亮这种低级错误最浪费时间。烧入bit后第一步不是打流而是确认物理层状态。如果设计中预留了状态寄存器或者逻辑分析仪探针就去看GT的tx/rx resetdone信号、PCS的block lock、lane align状态、FEC的符号错误计数。这些信号全部有效后才说明物理层真的准备好了。如果卡在lane align优先怀疑参考时钟不对或信号质量太差如果FEC错误计数持续增长那大概率是光模块信号完整性问题或FEC模式不匹配。5.2 ARP、ping与UDP的连通性验证100G的UDP demo和普通网卡不一样它不一定实现ICMP协议所以你还得确认一下工程里有没有回包逻辑。我常用的方法很简单先把FPGA侧配置成一个固定的静态IP和MAC地址然后在PC端向这个地址发起ARP探测如果PC的ARP表里能解析出对应的MAC就说明链路层和数据链路层是通的。如果ARP能通再尝试直接向FPGA的UDP端口发包看FPGA侧的接收计数器是否增长。很多开源工程自带一个回环或发包模块你可以开启它让FPGA周期性向PC发送UDP包PC端用Wireshark抓包验证源MAC、源IP、UDP端口号是否符合预期。不要跳过Wireshark抓包这一步因为数据能通和数据结构完全正确是两回事。我们这次拿到100G网卡后我没有用交换机而是让PC端的Mellanox ConnectX-5网卡和FPGA光口用一根DAC线缆直连。这里有个细节DAC线缆两端如果都是高速接口要确认线缆的EEPROM里有没有正确的链路速率协商信息否则有的网卡会默认协商成40G导致两边速率对不上。5.3 iperf3 UDP打流参数与结果解读UDP打流最常用的工具是iperf3。PC端如果是Linux用以下方式起一个UDP接收端iperf3 -s -u -p 5001FPGA侧如果是主动发流你需要一个简单的用户逻辑或者微处理器程序来向指定IP和端口连续发送UDP包。为了保证测试走到线速往往直接把发包模块的间隔调到最小让数据源尽可能快地把包推给MAC。反过来如果要测FPGA的接收能力就在PC端向FPGA发包iperf3 -c 192.168.1.10 -u -b 0 -l 1472 -t 30这里-b 0表示用UDP不限速发送-l 1472是UDP payload长度-t 30是持续30秒。重点观察两个数据发送端的发送速率和接收端实际收到的速率。如果在同一个主机上通过loopback测没什么参考价值跨设备打流时发送端显示多少接收端显示多少差额基本就是丢包。我这次测试的实际结果是在1472字节payload下FPGA到PC的发送方向稳定在97.5Gbps左右丢包为0PC到FPGA的接收方向也能跑到96.8Gbps统计丢包几乎为0。这说明在标准MTU下整个协议栈已经接近100G线速的极限水平。换到9000字节巨帧后吞吐还能再撑一截能到99.2Gbps左右因为同等数据量下包数量更少、MAC处理开销更低。如果目标是压极限还可以测小包场景。64字节帧下整个MAC和协议栈的处理压力全在包速率上吞吐会明显下降这是正常现象。100G的小包线速接近每秒1.48亿个包FPGA内部光是把每个包的头部解析一遍时钟周期就吃掉了一大半所以不用把“小包跑不满”当成bug关键是看大包和实际业务流量能不能贴近线速。5.4 打流时PC端UDP缓存不容忽视有个很常见的现象FPGA发过来的UDP数据其实都到了网卡但PC应用层就是收不到或者能看到偶尔丢包。这往往不是链路问题而是操作系统UDP接收缓冲太小。100G的流量一秒钟几千万个包默认几十KB的socket缓冲区瞬间就满了。在Linux下可以用sysctl临时调大UDP缓冲区sysctl -w net.core.rmem_max134217728 sysctl -w net.core.rmem_default67108864如果是在Windows下需要去注册表修改AFD参数里的DefaultReceiveWindow重启后生效。这类调整在10G时代不明显100G打流时几乎是必做的否则iperf3的接收端会报大段大段的丢包把你误导到FPGA链路有问题上。6. 遇到RX丢包时我完整的排错链路和最终修复最后这部分我想完整还原一次我在上板测试中遇到的RX方向丢包问题因为它很有代表性。现象是10G速率下PC发FPGA一切正常切到100G后FPGA侧的接收计数器比PC发送计数器少了约5%也就是说保底丢了5%的包。这个丢包率稳定、可复现和光模块、温度都没关系。6.1 排错第一步把“丢包发生在哪一层”先定位到我没有一头扎进RTL里去猜而是先做一个分层计数。整个接收链路从光纤到用户逻辑大致可以切出物理层GT/PCS/FEC、MAC层、IP/UDP协议栈、用户侧AXI接口与缓冲。我分别在这些层级挂上计数器物理层用IP自带的统计信号MAC层拿eth_mac_100g的状态输出协议栈层看eth_udp_100g模块收到的帧计数用户侧则看FIFO的写入计数。然后对比相邻两个层级的数值如果物理层计数本身就少说明是信号完整性或FEC丢包应该去查光模块光功率、FEC符号错误、GT眼图而不是在应用层找问题如果物理层计数和MAC层计数一致但MAC层到协议栈之间少了问题出在MAC与UDP栈的接口握手或FIFO溢出如果协议栈接收计数和用户侧FIFO写入计数一致但用户侧读出来的数据还是少了那问题就在用户逻辑消费速度上。这次的结果很有指向性物理层、MAC层、UDP栈的计数完全一致丢包发生在协议栈输出到用户侧FIFO这一层。问题范围瞬间从整条链路缩小到了一个点。6.2 抓住现场ILA探针看到的tail drop锁定范围后我在UDP模块输出到FIFO的AXI接口上加了一个ILA探针抓取tvalid/tready/包结束信号然后在100G满速率下复现丢包。ILA抓下来的波形让我看清了问题FIFO深度只有2048个512bit字而一个9000字节的巨帧就占掉大约4500个字。当连续几个大包打进来时FIFO没一会儿就满了tready被拉低。但注意tready是在FIFO真正满的那一刻才拉低而此刻MAC还在把当前帧的数据往外推后面的帧在MAC层被直接丢弃。这就是典型的tail drop不是FIFO没有反压而是反压来得太晚、太急。一个突发流量过来时FIFO状态从“还有空间”跳到“完全满了”中间的缓冲根本无法消化整条AXI链路上已经存在的在途数据。6.3 修复方案水位回压和缓冲扩容两手抓修复分两步。第一步把反压信号的触发点从“FIFO满”改成“FIFO剩余空间不足一帧”也就是设置一个提前量。比如FIFO深度加到16384当剩余空间小于2048个512bit字时就提前拉低tready给上游留出刹车距离。这一步解决的是tail drop的突发尾巴。第二步用户逻辑原来把UDP数据拆成单个大的DDR写请求一次要写很多个节拍DDR带宽一忙就卡住。我优化成把大帧拆分多次burst写降低单次请求对DDR仲裁的压力同时提高了整体写吞吐。改完后重新跑同样的100G满速率测试丢包从5%降到了0反复测了多次都稳定。这个过程给我最大的一个教训就是100G链路里反压不是“有就行”而是必须判断反压触发阈值和整条链路缓冲的关系。如果你拿10G时代的FIFO深度和反压策略直接套到100G大概率会在高吞吐瞬间崩掉而且崩得毫无规律。测完这个工程我个人最大的感受是100G UDP移植真正花时间的从来不是UDP协议本身而是它脚下的物理层、时序、缓冲和反压这些“底盘工程”。如果你也在移植类似的工程建议先花一下午把板卡原理图和原工程的XDC逐行对照一遍再开始动RTL。这步慢一点后面能省下好几个通宵的排错时间。