
写这篇总结的时候我刚把一套开源的100G FPGA UDP协议栈从参考工程移植到自己的测试板卡上并且完成了上板测试。整个过程比预想中复杂得多从git clone到最终用iperf3跑通单向90Gbps以上断断续续花了两周中间有三天几乎都耗在链路不亮和小包丢包上。如果你正准备做类似的事情——比如给高速数据采集系统加一个网络出口或者把手里的FPGA加速卡改造成一台硬件UDP回环/转发设备——这篇内容应该能帮你避开不少弯路。需要先说明的是这里说的“移植”不是把一个IP核拖进Vivado、点几下Generate Output Products就完事而是把开源工程里和具体板卡强相关的硬件约束、时钟树、复位逻辑、GT位置全部替换成自己的平台再完成上板验证。下面所有内容都围绕这段经历展开包括方案选型、移植步骤、测试方法以及最后踩过的一堆坑。1. 100G UDP不是“把代码抄过来就行”想清楚协议栈为什么必须烧进FPGA1.1 为什么是UDP而不是TCP先想清楚目标什么场景需要100G UDP协议栈嵌入式圈子里“UDP”其实分两种待遇。一种是像STM32接个以太网PHY跑轻量协议栈用来做配置和状态上报那主要靠MCU软件完成另一种是数据面比如ADC采样数据、雷达中频数据、视频流、分布式存储的数据搬运速率要求高CPU根本处理不过来这时候FPGA就成了负责高速数据收发的核心器件UDP也成了默认选择。为什么不用TCP原因很现实TCP是有状态协议有连接管理、拥塞控制、丢失重传、按序到达和一堆定时器。在FPGA里把完整的TCP栈做得可靠要烧掉大量逻辑资源和开发周期而且性能一上去就极其难调。UDP无连接、无状态把IP头和UDP头组好、校验和算对剩下的就是纯数据搬运。今天高性能网络里的RoCEv2本质上也走UDP封装这已经说明了在高带宽场景下大家都默认UDP是扛原始数据的第一选择。1.2 100G线速意味着什么远不止带宽数字翻10倍如果你之前一直玩10G甚至千兆第一次接触100G时最该被刺激到的是这个数字100G以太网在最坏情况下即64字节小包每秒要处理约1.19亿个包。平均下来一个包只有0.84ns的处理时间。而FPGA上100G用户逻辑通常工作在322MHz上下一个时钟周期3.1ns等于一个周期内就要处理将近四个包。这种压力下任何“等中断进来再查表处理”的思路都是死路必须靠硬流水线完成包头解析、查找、转发决策和调度。这也是为什么“移植上板测试”不能只验证“功能能通”还得验证“线速能不能扛”。很多开源IP在仿真里一切正常上板以后小包一上来就丢就是因为小包速率超过了内部数据通路或者表项的吞吐能力。1.3 开始动手前先回答三个问题移植不是拿到代码就开编。我建议你先回答下面这三个问题否则大概率白干你的100G对端设备是什么是PC上的100G网卡还是交换机还是另一块FPGA板卡这决定了测试链路怎么搭也决定你是只做UDP端点还是需要支持ARP、ICMP、巨型帧、多端口等一堆附加功能。你的板卡上100G用的是哪组GT参考时钟是多少不同板卡的GT引脚位置、参考时钟来源、光模块管理脚都不一样。这是移植工程量的主要来源也是最多人卡住的地方。你要的是纯硬件协议栈FPGA内部直接完成收包、查表、转发还是带PCIe/DMA和主机交互的完整网卡方案这两个方向的开源项目完全不同选错路径会让你绕一大圈。这三个问题没有标准答案但没想清楚就git clone大概率会陷入“工程能编译、上板就是不通”的泥潭。2. 开源工程的选型复盘我把哪套代码拖进了Vivado2.1 100G协议栈的开源生态比想象中窄开源以太网/UDP IP并不少一限制到100G可选范围马上缩小很多。网上能查到的方案大致分三类。第一类是完整NIC方案代表是Corundum。它把PCIe、DMA、多队列、以太网MAC全做了还有完整的Linux驱动插上FPGA加速卡能当普通网卡用。但Corundum的定位是“把FPGA变成一个网卡”UDP协议栈跑在主机操作系统里FPGA内部只做DMA和收发队列并不是那种“硬件直接组UDP包并主动回复”的轻量协议栈。如果你的需求是“FPGA板卡自己处理UDP报文”选Corundum会绕一大圈还得先解决驱动的编译和移植问题。第二类是经典的开源以太网MAC/PCS库比如Alex Forencich维护的verilog-ethernet。代码风格工整是很多高速网络开发者的参考蓝本。但它主线支持的是10G/25G这一档100G需要自己往CMAC上衔接UDP端的代码也要根据数据位宽重新适配不适合想“最短时间跑起来”的场景。第三类是一些高校或团队开源的100G UDP/IP硬件栈。这类工程通常自带CMAC wrapper、ARP缓存、ICMP回应逻辑、UDP收发引擎接口一般做成AXI-Stream有些还带DDR缓存和IP分片重组。这就是真正意义上的“UDP协议栈移植”对象。我最终选的是这一类因为模块边界清晰CMAC和UDP核之间是简单的流接口方便在不改动协议内核的情况下替换自己板卡的硬件相关部分。2.2 我选型时用到的四条标准拿回一个开源工程我主要看四点可移植性底层是不是绑死了特定板卡的约束和IP有些工程把GPIO、DDR、LED、PCIe全捆在顶层拆起来极其痛苦。我要的是“链路层到用户接口”这种清晰结构。IP依赖度对Xilinx CMAC、PCIe、DDR IP的依赖强不强版本老不老。IP版本太老或者太新都会导致Vivado重新生成时报一堆错。模块清晰度UDP校验和、ARP表、ICMP回应、IP组包是不是独立模块数据位宽是64bit还是512bit改起来的工作量差很多。文档与活跃度README有没有写清上板步骤issue里有没有人提到和当前版本相关的坑最近有没有提交。很多人想移植的其实是加密网表edf版本的工程但edf工程接口完全不能动改硬件平台基本等于放弃所以我坚持选纯RTL工程。综合下来我的路线是“Xilinx CMAC硬核 开源UDP端点逻辑”。协议栈部分尽量不动硬件相关部分全部重写顶层、约束、时钟复位模块换成本人板卡的版本。3. 从README到生成bitstream移植过程的每一步3.1 硬件环境和版本适配先说环境。我用的Vivado版本是2023.1目标器件是带QSFP28的UltraScale系列加速卡。开源工程原始环境一般是2020.1到2022.2拉下工程后Vivado会弹出一堆IP升级提示。这里我的经验是不要无脑点Upgrade All先看哪些是100G Ethernet相关IP、哪些是DDR/PCIe等外围IP。CMAC这类硬核IP在不同版本之间差异很大升级后端口名可能变个别寄存器配置也会失效。建议直接在Vivado里新建工程把RTL源文件手工添加进去再把CMAC IP删掉重新生成不要依赖工具自动迁移整个IP树。另外如果你拿到的开源工程是edf加密网表那顶层接口就是锁死的只能按原样例化内部逻辑不能变甚至连Xilinx IP的版本信息都可能锁在里面。这类工程换板卡基本无解这也是我宁可多花时间找纯RTL版本的原因。3.2 替换CMAC IP和GT位置是整个移植的核心工程CMAC是UltraScale系列中100G以太网的硬核模块负责MAC、PCS和PMA直接接GTY/GTM高速收发器。不同板卡的QSFP28所接GT位置不同、参考时钟来源也不同。原工程里QSFP0可能接GTY Bank 128的通道0到3你的板卡可能接的是Bank 127的通道4到7参考时钟可能是板上156.25MHz晶振也可能是从光模块返回的时钟。这些信息只能查板卡原理图没有捷径。在Vivado里重新生成CMAC IP时有几点特别容易错选择线速率100G数据路径位宽选512bit用户侧时钟就是322.265625MHz参考时钟频率选156.25MHz还是161.1328125MHz必须和板卡实际供给一致否则GT初始化直接失败RS-FEC开关要保持和对端一致。如果对面是标准100G网卡默认开RS-FECCL-91通常最稳如果两块FPGA板卡直连两边必须配置一样GT位置约束在XDC里要改但不要在IP配置界面随手点Auto Place容易选到被其他接口占用的Bank。这些做完后CMAC输出的AXI-Stream接口是512bit、322MHz后面接UDP协议栈时要特别注意开源工程里的UDP核可能当初设计是64bit或256bit接口位宽不一致就得在接口处加位宽转换FIFO。这一步看着简单实际上很容易引入字节序和包头偏移错误。3.3 时钟、复位和DDR缓存三条关键链路上板测试阶段最难查的就是时钟和复位。CMAC IP对复位时序有严格的要求reset信号必须保持足够长然后由IP内部状态机完成GT初始化、PCS校准、链路训练。很多移植失败都是因为外部给了一个毛糙的复位导致gt_rxpolarity或pcs_rst_done永远不拉高。如果协议栈里带DDR缓存用于IP分片重组或大包缓存还要注意DDR用户时钟和core_clk的跨时钟域处理以及地址对齐问题。我调试时遇到过数据写到DDR没问题、读出来偶尔错几个字节的情况最后定位到是读写地址位宽没对齐DDR用户接口一次突发长度和UDP payload长度不一致导致的。这类问题仿真很难暴露因为仿真里的DDR模型时序理想上板后时序差一点就露馅。3.4 XDC里那些原工程没写明白的约束移植到新板卡端口约束和时钟约束必须全部重写。很多人以为改改引脚名就行实际上远远不够。这里面有几个高频踩坑点参考时钟要自己create_clock不能指望IP自动生成对外部管理接口比如QSFP28的I2C、中断脚如果有约定input delay要用set_input_delay把裕量写足对进入异步FIFO的跨时钟域路径必须显式设set_false_path或set_max_delay -datapath_only否则Vivado会把两条不相干的时钟域强行分析出大量violation反过来不要为了快速收敛时序随手把UDP核内部路径也设成false path。很多人喜欢把“不关心的路径”全屏蔽结果上板后出现概率性丢包查半天最后才发现时序违例被自己掩盖了。给一个实际用过的约束片段作参考create_clock -name gt_refclk -period 6.4 [get_ports qsfp0_refclk_p] set_input_delay -clock gt_refclk 1.2 [get_ports qsfp0_modprs_l] set_input_delay -clock gt_refclk 1.2 [get_ports qsfp0_int_l] # 异步FIFO两个时钟域之间不分析路径 set_false_path -from [get_clocks user_core_clk] -to [get_clocks mac_core_clk] set_false_path -from [get_clocks mac_core_clk] -to [get_clocks user_core_clk]注意period 6.4对应156.25MHz如果板卡给的是161.1328125MHzperiod就是6.208。这两个频率特别容易混我的教训是每次上板前先确认GT参考时钟频率再用ILA抓一下txoutclk确认IP真的锁定正确再继续。4. 上板测试三板斧回环、Ping、iperf3打流4.1 先让FPGA自己收到自己发的包移植完成后不要直接插光纤对接主机。第一步先做板内回环。CMAC IP一般支持PCS Loopback和MAC Loopback打开后FPGA自己的发送数据会从接收路径绕回来。这时在用户逻辑里放一个计数器每发送一个UDP包就加一同时在接收路径上做一个比对模块看收到的包是不是发出去的包把计数对照关系打出来第一个验证就完成了。这一步能快速暴露字节序反没反、CRC算得对不对、UDP校验和合不合规、AXI-Stream的tuser标记是否正确。经验是用ILA抓发送侧和接收侧的头几个周期重点看tdata和tkeep的对应关系以及tuser打点的位置。很多开源UDP核在头字段定义上稍有不同数据位宽一改就容易偏移。4.2 对端链路起来后先用Ping探活板内回环通过再接对端100G网卡。我这边用的是带QSFP28端口、支持100G的以太网卡配一根1米以内的QSFP28 DAC线直连。插上后先在FPGA侧看CMAC的link_status、pcs_rst_done、gt_rxbyteisaligned是否全部拉高同时在PC上运行ethtool ens2np0确认link speed是100000Mb/s。只要有一方显示link down就先别谈协议优先排查GT初始化、参考时钟、光模块管理脚。链路起来后把协议栈里的ICMP responder打开PC直接ping 192.168.x.x。Ping通意味着ARP请求和响应逻辑正常、IP/UDP基本收发通路正常、MAC地址和IP地址配置正确。这一步看起来简单实际上一半的移植问题都是在这个阶段暴露的。在PC上顺手看一下UDP层统计。Linux里cat /proc/net/snmp | grep Udp能看到InDatagrams、OutDatagrams、InErrors、NoPorts这些字段。RcvbufErrors如果快速增加说明收包太快而用户态来不及读Packets to unknown port received如果很大说明包已经到主机但没有socket在收。这些计数器在排查“FPGA发出来的包怎么不见了”时比抓包还快。4.3 iperf3 UDP打流的正确姿势Ping通之后就是重头戏100G UDP打流。工具首选iperf3但大多数人第一次用它跑100G都会遇到同一个困惑为什么带宽上不去几条经验直接给你iperf3的UDP模式要用-u并且显式指定目标带宽-b。默认情况下iperf3 UDP只发1Mbps不看文档的人会以为打流工具坏了。单线程很难跑满100G。我实测单线程大约60到70Gbps加-P 8多线程后才稳定过90Gbps。还上不去的话检查CPU频率、网卡中断有没有绑到同一NUMA节点。MTU调到9000。默认1500字节MTU在100G下包速太高CPU处理不过来容易触发网卡和协议栈瓶颈。对端网卡同样要设MTU9000并开启巨型帧。用短DAC线在隔离的直连环境测试不要经过中间交换机变量降到最低。一条典型测试命令# 对端PC上先启动服务端 iperf3 -s -p 5201 # 发起UDP打流 iperf3 -c 192.168.10.2 -u -b 95G -l 9000 -t 30 -P 8 -i 1如果FPGA工程是回环模式即收到UDP包后原样转发回去iperf3还能同时验证TX和RX两条路径。我测试的结果是MTU 9000、8线程、目标95Gbps的情况下接收端能稳定收到92到94Gbps丢包率低于0.01%对端CPU占用已经比较高。换成1400字节的普通包丢包率会明显上升这正好引出下一个话题100G小包才是真正的试金石。4.4 结果怎么判读带宽、丢包和错包iperf3输出里重点看四个数sender带宽、receiver带宽、丢包数和丢包比例、抖动。丢包比例比带宽数字更能反映系统健康。如果sender端显示95Greceiver端显示88G丢包率7%那就要分辨丢在哪如果是FPGA接收端丢看CMAC侧CRC错误计数、UDP核的FIFO full计数如果是PC接收端丢看网卡ethtool -S ens2np0 | grep -E rx_drop|rx_missed|rx_buffer如果是协议栈内部丢看DMA描述符是否耗尽、DDR是否写满、用户处理逻辑是否产生反压。最好在FPGA里放一组计数器发送包数、接收包数、CRC错包数、UDP校验和错误数、FIFO溢出数。有了这组数定位丢包点就快多了。5. 移植上板这几块绊脚石足够劝退大部分人5.1 链路训练失败Link灯全灭时的排查顺序100G链路协商本身就是一个大坑。最常见的现象是板卡上电、FPGA下载完bitstream、CMAC配置正常但对端网卡始终link downDAC线两端灯全灭。按这个顺序查确认参考时钟GT refclk频率对不对用ILA观测txoutclk确认光模块管理脚QSFP28的modsel_l、reset_l、lp_mode这些控制信号是否被正确拉高/拉低。有些开发板上这些信号默认由GPIO控制不初始化的话模块可能一直处于复位状态确认PCS状态机pcs_rst_done、rx_fsm_reset_done、tx_fsm_reset_done确认FEC配置一致两边一个开RS-FEC一个关RS-FEC大概率协商失败看CMAC的remote_fault、local_fault寄存器区分是发不出还是收不回。我印象最深的一次参考时钟、GT位置、FEC全对还是link down。最后发现是CMAC里设置的PMA/PCS模式少了RS-FEC的subtype使能位。这个位在Xilinx IP的example设计里通常默认配好换了自己板卡重新生成IP时容易漏掉。5.2 小包线速和DDR缓存索引重组是最考验设计功力的地方前面说过100G在64字节小包下有1.19Gpps的压力。实际测试时会强烈感受到“大包是入门小包是地狱”。很多开源UDP协议栈只保证单一UDP流或大包流量的线速遇到64字节小包时因为查表、分配描述符、更新状态机来不及处理而丢包。如果业务确实有大量小包就得检查接收路径的瓶颈点包头解析是否一拍完成ARP表/UDP端口表是否用查表实现而不是线性比较内部FIFO深度够不够后续有没有DDR缓存对IP分片重组做支撑。“fpga ip核缓存索引重组”这个方向就是干这个的。分片重组在100G上极其消耗资源要做哈希建表、按源IP/分片ID/偏移量排序、超时回收。我的建议是业务层能避免分片就尽量避免真避免不了才考虑DDR缓存方案并且一定要单独估算DDR读写带宽。DDR4-2400单根大概19.2GB/s看着多但分片重组的写一读一很快就吃掉一半。5.3 主机通道的隐形限制不一定死在UDP上如果这块FPGA最后要把数据交到CPU除了网络侧PCIe/DMA路径同样限制整体吞吐。我遇到过一次FPGA外部回环怎么测怎么满一接主机读写就掉到60多G排了半天发现是DMA描述符更新太慢MSI中断把CPU打爆了。对100G网络建议优先保证PCIe Gen3 x16以上。理论上单向约126Gbps实际能稳定供给网络侧90Gbps以上已经不错。如果板卡只有PCIe x8就别指望满带宽。主机侧打流时把网卡中断绑定到独立CPU核、关闭irqbalance必要时用DPDK绕过内核协议栈这些都属于常规操作。5.4 “概率性丢包”通常都是时序或跨时钟域问题最让人崩溃的不是“完全不通”而是“偶尔丢一个”。这种情况十有八九来自两块第一RTL里跨时钟域处理不干净第二时序收敛没有真正达成。Vivado的timing summary里显示少量setup/hold violation但bitstream也能出来系统也能跑只是偶发错误。跨时钟域问题在UDP协议栈里尤其常见ARP表的写时钟和读时钟不一致、DDR读数据和用户逻辑不在同一时钟域、MAC状态机状态位被异步打拍后采样出错。这些问题的共同特征是仿真时全对上板后丢包率随机波动。解决办法很明确严格使用异步FIFO加格雷码指针或者两级同步器加握手不要在模块间裸传多bit信号。时序收敛方面上板前跑report_timing_summary确认setup和hold都是0 violation。如果有一两级violation与其冒险上板不如先试试retiming、关键路径打拍、合理pipeline优化。100G用户时钟322MHz比10G时代高了一倍多很多10G下能凑合的代码习惯都要改。6. 移植流程复盘如果重新做一次我会怎么安排6.1 让链路先亮再谈吞吐如果重新安排这次移植的节奏我会把“链路先亮”当成最高优先级而不是一上来就怼完整协议栈。具体顺序是先做一个最简工程里面只有CMAC IP加一个最简单的发送计数器bitstream下载后直接插DAC线用ILA确认link up。这个工程半小时就能完成能筛掉一大半硬件和配置问题。第二步再做板内回环验证MAC层收发正常。第三步接主机把ARP和ICMP打通确认IP层逻辑。第四步才上UDP和iperf3。每次只引入一个新的变量出问题才能快速定位。如果一上来就跑完整系统然后发现link down根本无法确定是GT时钟的错、RS-FEC配置的错还是UDP校验和的错。6.2 RTL里提前埋好调试位别等出问题再抓头这一块是血泪教训。移植上板阶段RTL里提前留好这些调试信息能省掉大量时间一组发送/接收包计数寄存器通过AXI-Lite或JTAG读取CMAC的link状态、fault状态直接外接到LED或GPIO每个FIFO的full信号和溢出计数这是定位丢包最直接的线索一个可选的Loopback开关通过拨码开关或寄存器控制出问题时可现场切换测试模式。这些调试位不要嫌丑。上板调试阶段它们就是你的眼睛。等一切稳定下来再根据资源余量决定要不要删掉。6.3 给准备复现这个项目的朋友的实用清单最后整理一份清单基本覆盖了我在这个项目里遇到的大部分问题物料准备一块带QSFP28的FPGA板卡、一根1米以内的QSFP28 DAC线、一台带100G网卡的Linux主机网卡驱动要新工程准备Vivado版本和开源工程匹配CMAC等IP如果涉及License提前申请原理图准备把GT位置、参考时钟来源、QSFP28管理引脚全部标注出来上板前检查timing summary零violation跨时钟域路径全部设置到位测试顺序link up - PCS/MAC loopback - ARP/Ping - UDP大包 - UDP小包 - 吞吐优化心里时刻想着一切“偶尔丢包”问题先查时序再查跨时钟域最后才是逻辑功能错误。100G FPGA UDP这条路看着门槛高其实只要路线正确、顺序合理并没有想象中那么玄。真正耗时间的全是那些“看起来对了但实际差一点”的细节。希望这篇经验能帮后来的人少走几步弯路。