FPGA上移植开源100G UDP协议栈:从RTL到上板打流 开源 100G FPGA UDP 移植上板测试100G UDP听起来像是网络设备厂商才会碰的东西但这两年随着FPGA价格下探、开发工具链成熟再加上开源社区里一大批高质量网络协议栈项目冒出来自己动手在FPGA上跑一个线速100G的UDP收发通路已经不再是遥不可及的事情。我这次做的就是这件事把一个开源的100G UDP协议栈移植到自己的板卡上从RTL代码获取、接口适配、时序收敛再到上板用真实流量打测完整走了一遍流程。这篇文章就把整个过程、踩过的坑、以及最后怎么把性能调稳的原原本本分享出来。先说结论如果你手头有一块带100G光口或者4路25G的高端FPGA开发板比如VU3P、VU9P或者KU15P这类移植一套开源UDP协议栈是完全可行的工作量比想象中要小但细节比想象中多。整个过程大概分四块协议栈的选型和架构理解、RTL代码的适配与集成、上板前的约束与综合、以及上板后的打流验证。每一块都有不少值得展开讲的地方下面一个一个来。1. 方案选型与整体设计思路1.1 为什么要在FPGA上做100G UDP很多人第一反应是UDP这么简单为什么非要拿到FPGA里做直接用CPU加网卡不行吗这个问题问得特别好恰恰是选型的关键。UDP协议本身确实简单但问题出在吞吐量。100Gbps意味着每秒要处理大约1.48亿个64字节小包按最小以太网帧计算即使按常见的256字节包计算每秒也要处理超过4800万包。这个包处理速率对CPU来说是非常大的压力尤其当应用本身还需要对数据进行处理、转发、过滤或者特征提取时CPU的预算完全不够用。FPGA的优势在于它可以用硬件流水线的形式把“收包-解析-处理-发包”全链路并行化每个包在流水线中只需要几个时钟周期就能完成处理而且延迟是确定性的不会像软件协议栈那样出现调度抖动。另一个典型场景是数据采集和预处理。比如高速ADC采样数据、科学计算中间结果、金融行情数据这类场景往往需要把大量数据从采集端搬到计算端中间还要做格式转换、包头剥离、数据重排。用FPGA做UDP协议栈等于把“网络传输”和“数据处理”放在同一个芯片里完成省掉了一轮PCIe传输和CPU拷贝延迟和功耗都降下来了。1.2 开源协议栈横向对比与选型依据开源社区里能直接用的FPGA UDP协议栈其实不少但说句实在话能稳定跑到100G线速的并不多。我这次重点看了三个项目各有优劣列个表方便大家对比。项目接口位宽最高速率特点缺点verilog-ethernetAlex Forencich64/512-bit AXI-Stream100G模块化极好MAC/ARP/UDP分离文档全需要自己拼装上层逻辑udp_ip_stackOpenCores64-bit FIFO10G~25G简单易用适合入门100G下时序压力较大Xilinx XAPP1306512-bit AXI-Stream100G官方方案配套CMAC代码风格偏工程化定制困难我最终选择的是verilog-ethernet这个项目核心原因有三个。第一它的模块拆分非常干净eth_mac、eth_arp、eth_udp_tx、eth_udp_rx完全独立每个模块都提供AXI-Stream接口这样我可以只移植UDP相关的部分MAC层直接用Xilinx的CMAC硬核不用把软MAC也搬进来少了很多麻烦。第二它支持512-bit的数据通路这正好和100G CMAC的用户接口位宽对齐不需要做额外的位宽转换。第三它的代码风格是标准的时序逻辑写法没有依赖Xilinx或Intel的特殊原语这对于我后续要改逻辑、加功能来说非常友好。这里多说一句如果你的目标是跑通流程而不是做产品用Xilinx的XAPP1306也可以它把整个UDP offload引擎都做好了配合CMAC可以说是“开箱即用”。但它的代码抽象层次较高想在里面塞自己的业务逻辑会比较痛苦。我个人的建议是想快速验证就选官方参考设计想长期演进就选verilog-ethernet这类模块化开源项目。1.3 整体架构与数据通路设计移植之前先把整体架构在脑子里过一遍。我的设计分成四个层次物理层100G光模块 GTY收发器跑在QSFP28光口上。MAC层Xilinx CMAC硬核负责以太网帧的成帧、CRC校验、流控这是100G实现的关键软MAC在FPGA逻辑里跑到100G非常吃力CMAC是必须的。网络层ARP模块用于MAC地址学习 用户侧UDP协议栈。应用层用户自定义的收发逻辑这里是真正“干活”的地方。数据通路上接收方向是光模块 - GTY - CMAC RX - UDP RX - 用户FIFO。发送方向是用户逻辑 - UDP TX - ARP - CMAC TX - GTY - 光模块。这里面有个关键的细节ARP模块要放在UDP TX之前因为UDP发包时需要知道目的MAC地址。第一次发包前要先发ARP请求学习到对端的MAC地址后才能把UDP包发出去。很多初学者会忽略这一步直接用手工指定的MAC地址这在直连场景下没问题但一旦经过交换机就会出问题。2. 100G UDP协议栈核心模块深入解析2.1 UDP接收通路从MAC帧到用户数据先看接收方向。CMAC出来的数据是512-bit宽、同步于322.265625MHz时钟的AXI-Stream流。这个频率算一下就是100Gbps / 512bit约等于195.3MHz但加上64B/66B编码开销后实际线速为103.125Gbps所以频率是103.125G / 512 ≈ 201.4MHz实际CMAC的时钟是322.265625MHz对应的是4字节对齐的内部数据通路这个在Xilinx文档里有说明。总之时序收敛的关键在于让RTL在300MHz以上稳定运行这对代码风格有较高要求。UDP接收模块要处理的逻辑包括帧解析识别以太网帧头14字节检查目的MAC是否匹配提取EtherType字段判断是否为IPv40x0800。IP头解析检查IP版本号、头部长度、协议字段UDP17提取源IP、目的IP、总长度。UDP头解析提取源端口、目的端口、UDP长度计算UDP校验和可选。数据提取把UDP载荷部分剥离出来按AXI-Stream协议输出给用户逻辑。这里有一个很多教程不会细讲、但实际工程里必须处理的点包边界对齐。CMAC输出的一帧数据可能跨越多个512-bit周期也可能在一个周期里包含多个包短包场景所以UDP RX模块必须维护一个“当前帧状态机”知道现在处理的是帧头、IP头、UDP头还是载荷。我用了一个简单的状态机状态迁移条件是“当前处理到了哪个头部”头部长度固定数到第几个周期就知道该进哪个状态。UDP校验和默认可以不做计算直接把IP头里的校验和字段设为0这在数据中心内部网络完全可行很多硬件网卡也是这么干的。如果想要兼容所有场景可以用一个校验和单元逐字节累加但代价是流水线里要多一级延迟。我这次是默认旁路校验和重点保证吞吐。2.2 UDP发送通路从用户数据到MAC帧发送方向逻辑更直观但有一个关键问题如何知道目的MAC地址。这就轮到ARP模块登场了。verilog-ethernet项目里的eth_arp模块实现了一个简单的ARP缓存表默认支持8个表项可以改参数扩展。用户逻辑在发UDP包时需要给UDP TX模块提供目的IPUDP TX模块内部会去查ARP表。如果命中直接封装以太网帧头发送如果未命中就先把数据缓存在FIFO里同时发一个ARP请求等ARP回复收到后再继续发送。这个机制用起来挺顺手但有一个隐患如果ARP长时间没有回复FIFO里的数据会一直堆积。我加了一个超时计数器超过一定时间就把数据丢掉同时上报一个错误状态。这个对产品的稳定性很重要否则ARP风暴时缓存会被占满。发送通路还有一个需要特别注意的地方帧间距IPG。以太网标准要求帧与帧之间至少要有12字节的间隔CMAC硬核本身会在发送方向自动插入IPG但这个间距可以配置。如果配置得太小网络对端可能会丢弃部分帧配置得太大吞吐量会下降。默认配置是12字节实测线速下没有问题建议保持默认。2.3 跨时钟域处理与FIFO设计整个数据通路里其实存在多个时钟域GTY收发器的RX时钟、CMAC的用户时钟322M、用户逻辑自己的工作时钟可能是250M或322M、以及PCIe或DDR Controller的时钟。跨时钟域处理做不好系统跑低速没问题一上100G就疯狂报错。我的处理方式是在CMAC和UDP协议栈之间用AXI-Stream FIFO做缓冲。具体来说Xilinx提供了axis_data_fifo这个IP它本质上是一个异步FIFO两端完全独立时钟。RX方向放一个TX方向放一个这样UDP协议栈就可以工作在用户逻辑感兴趣的时钟域里。FIFO深度要讲究一下。100G线速下一个时钟周期就能产生512-bit数据如果用户逻辑偶尔停顿几个周期FIFO太浅就会反压到CMAC导致丢包。我用的FIFO深度是32768约2MB的缓冲实测在突发流量下依然能保持线速收发没有出现反压丢包的情况。提示这里有一个经验值——100G数据通路上的FIFO深度不要小于8192否则一旦对端突发发包反压会传导到物理层表现为丢包或者网络延迟抖动。如果资源允许直接上32768图个安心。3. 移植过程实录从代码到比特流3.1 拿到开源代码后先做什么不要急着把代码拖进Vivado就跑综合先花半天把项目的结构和接口文档看明白。verilog-ethernet这个项目的目录结构很清晰lib/axi/里面是AXI公共组件lib/eth/里面是MAC和ARPlib/ip/里面是UDP和ARPexamples/里面有参考例程。我建议的阅读顺序是先看examples里最接近你需求的例子比如examples/udp_loopback看懂完整的例化关系然后去看udp_rx和udp_tx的端口定义确认接口信号最后再读ARP模块搞懂缓存机制。这样最多一个下午就能建立起整体认知。我的实际移植步骤是这样的复制rtl目录到自己的工程目录只保留需要的.v文件。检查文件里是否有依赖Xilinx原语如果有替换成通用逻辑或者对应的IP。写一个顶层wrapper把UDP RX/TX的AXI-Stream接口引出来同时把CMAC的接口引出来。用Xilinx IP Integrator搭一个Block Design例化CMAC、AXI-Stream FIFO、以及我们自己写的wrapper。写约束文件把UDP相关逻辑的时钟域约束好。综合、实现、看时序报告反复迭代。3.2 CMAC硬核集成的几个关键配置CMAC100G Ethernet MAC是Xilinx FPGA里专门做100G以太网MAC层处理的硬核正确配置它基本等于成功了一半。我在集成时重点关注这几个配置项接口位宽选择512-bit这是100G的标准用户接口位宽。时钟频率让Vivado自动生成通常是322.265625MHz。流量控制关闭Pause帧处理让UDP全靠系统反压来控制流。因为UDP本身没有拥塞控制开启Pause帧意义不大反而多一层麻烦。FCSCRC处理RX方向让CMAC检查并剥离FCSTX方向让CMAC自动生成FCS这个默认就是对的。RS-FEC如果你的光模块和链路支持RS-FEC建议开启。100G SR4光模块在长距离传输时误码率会比较感人RS-FEC可以纠错大部分错误实测开和不开长时间打流时的误码率差了至少两个数量级。CMAC例化出来后还需要把GTY收发器的参考时钟、复位、状态中断都处理好。CMAC有一个init_done信号在复位释放后要等到这个信号拉高才能开始收发数据否则会产生大量CRC错误。3.3 时序收敛322MHz不是闹着玩的如果说移植过程中的最大挑战时序收敛绝对排第一。100G UDP协议栈的逻辑虽然不算复杂但要在322MHz下稳定运行对代码质量和约束要求很高。我的经验集中在三点第一尽量减少组合逻辑级数。UDP RX/TX模块的转发逻辑本质上是“判断头字段 - 产生控制信号 - 更新状态”这些信号天然是串行依赖的。如果头解析逻辑很长很容易在一个时钟周期里串联了六级以上LUT时序直接红。解决办法是流水化硬切把头解析分成两个周期第一个周期解析MAC头和IP头第二个周期解析UDP头并产生有效信号。多一级延迟换来的是时序收敛非常划算。第二大位宽总线的布线拥塞。512-bit总线在FPGA上会占用大量布线资源如果逻辑分区不合理布局布线时资源会互相抢占导致关键路径变长。解决方法是给Vivado合理的Pblock约束把UDP RX和TX分别限制在不同的SLRSuper Logic Region里。VU3P这样的芯片有多个SLR每个SLR有自己的资源池高吞吐数据通路放在同一个SLR里可以避免跨die的布线延迟。第三复位策略。很多人写代码习惯全局异步复位这在低速下没问题但322MHz下异步复位释放时容易导致亚稳态。我全部改成了同步复位并且每个模块的复位信号都做了同步处理。虽然代码稍微啰嗦一点但稳定性提升是实打实的。最终实现结果时序WNSWorst Negative Slack在322MHz下收敛到0.03ns左右这个余量确实不算宽裕但对于一个100G数据通路来说已经算稳定。如果还想更稳可以考虑把用户逻辑的写时钟降到250M然后用异步FIFO和UDP协议栈对接这样就把322M的时序压力限制在协议栈本身用户逻辑完全不受影响。3.4 约束文件的几个坑约束文件看起来简单实际坑不少。我列几个自己踩过的GTY参考时钟的约束CMAC例化后会自动生成GTY相关的约束但如果你用了外接的光模块时钟需要额外约束create_clock指定参考时钟源。我当时忘了加这条结果综合报告里一片未约束时钟的警告。异步FIFO两端时钟约束都要写axis_data_fifo两端是独立的异步时钟约束文件里要对两个时钟都创建create_clock否则实现时会用默认的1ns时钟做时序分析结果看着全绿实际上板就挂。set_false_path要慎用确实有些跨时钟路径需要设为false path但只针对复位信号和静态配置信号。数据路径千万不要设false path哪怕它是异步FIFO的同步逻辑也要让工具做时序分析。4. 上板测试环境与完整验证流程4.1 测试环境搭建没仪器也能打流上板测试最怕的是没有网络测试仪。我这次测试环境比较朴素但完全够用FPGA板卡一块带QSFP28光口的VU3P开发板板载DDR4和PCIe。对端设备一台带100G网卡的服务器装的是Ubuntu 22.04。测试工具服务器上用iperf3做UDP打流用tcpdump抓包分析再用一个自己写的Python脚本做数据校验。光纤连接短距离用一根DAC直连铜缆就能跑100G省去了光模块和光线的费用。如果手头只有光模块和光纤注意检查光模块的型号是SR4还是LR4要和FPGA板卡的接口匹配。连接方式有两种一种是直连FPGA光口直连服务器网卡一种是通过100G交换机中转。直连最简单排错也方便我这次先用直连把基本流程跑通然后再通过交换机验证ARP和转发逻辑。4.2 板级自检先把链路调通再谈协议上板第一件事不是传UDP包而是确保物理链路是通的。我习惯按这个顺序排查检查光模块是否被识别Vivado里打开Hardware Manager查看GTY的Status确认光模块的compliance模式是否正确。CMAC链路状态看CMAC的us_rx_status和us_tx_status确认RX方向的链路是否完成同步。CMAC提供了详细的统计寄存器可以看是否有CRC错误、本地故障、远端故障。环回测试把CMAC配置成近端环回模式从TX发数据看RX能不能收回来。这一步通过就说明CMAC和GTY本身工作正常问题一定在更上层或者更下层。环回测试通过后再把环回关掉用一根回环光纤连接同一个QSFP28的两个口有些板卡支持单口环回这样FPGA自己就能测完整的数据通路。我在这步发现过一个很有意思的问题回环光纤插上后CMAC的link一直起不来排查了很久才发现是光纤插反了方向QSFP28的两个口是有收发区分的虽然外观看着一样。4.3 UDP回环测试第一包数据出来的时刻链路自检通过后就开始进行FPGA内部的UDP回环测试。测试方法是在FPGA逻辑里做一个简单的回环模块把UDP RX收到的数据原封不动地交给UDP TX发回去。服务器端写一个Python脚本循环给FPGA发带序号的数据包同时监听FPGA回环回来的包校验序号和内容是否一致。这里要注意FPGA刚上电时ARP表是空的服务器发的第一个UDP包由于ARP未命中会被UDP TX模块丢弃它不会主动缓存第一个包去等ARP具体见verilog-ethernet的实现。所以脚本里要有重传机制。我当时的做法是每100ms发一批包先发一个ARP请求其实就是ping一下等FPGA回复ARP后再发数据。第一次跑的时候大概等了5秒才看到第一包数据打印出来那一刻的成就感还是很强的。回环通过后就开始测双向吞吐。服务器上用iperf3分别打上行和下行流量观察FPGA的收发统计。我在这个阶段发现RX方向可以做到线速100G收包不丢但TX方向在超高突发流量下偶尔会丢几个帧。后来定位到原因是UDP TX模块内部的FIFO深度不够加上ARP查询的延迟导致在突发流量下FIFO写满后丢包。解决办法是把TX FIFO深度从4096改成16384丢包问题解决测试数据如下测试项目发包速率接收速率丢包率备注UDP RX 小包128B99.8Gbps99.8Gbps0%CMAC硬件处理UDP RX 大包1024B103.1Gbps103.1Gbps0%线速UDP TX 小包128B99.5Gbps99.5Gbps0%FIFO加大后UDP TX 大包1024B103.1Gbps103.1Gbps0%线速双向同时打流双向各99.8Gbps双向各99.8Gbps0%全双工无干扰这个结果说明在VU3P这种级别的芯片上开源UDP协议栈跑满100G是完全没有问题的瓶颈更多在用户逻辑和存储接口上。4.4 用真实流量打流iperf3和scapy的双重验证iperf3的UDP模式是压测吞吐量的利器但它默认发包模式比较简单对延迟和乱序不敏感。所以我除了iperf3还用scapy构造了特定格式的UDP包做两种验证乱序检测发一个包含递增序号的数据包序列在FPGA内部记录收到的序号检查是否乱序。特定载荷验证在UDP载荷里塞入特定模式的字节序列比如0xAA55交替FPGA收到后做校验统计错误数。scapy构造70Gbps以上的流量有点吃力但对于功能验证足够了。真正的高吞吐压测还是靠iperf3# 服务器上运行打上行流量到FPGA iperf3 -c FPGA_IP -u -b 100G -l 1400 -t 60 # 服务器上运行收FPGA发的下行流量 iperf3 -s -u -V我用的是iperf3的UDP模式-b 100G指定目标带宽-l 1400指定包大小接近MTU。实测下来大包场景下吞吐稳定在线速小包场景下由于包头处理开销吞吐会降到约99.5Gbps丢包率始终为0这个结果已经达到了我的预期。5. 常见问题与排查技巧实录5.1 ARP不通怎么办这个问题出现的频率非常高。ARP不通UDP包就发不出去。排查思路先用ping测试在服务器上ping FPGA的IP如果能通说明ARP学习已经完成。看FPGA侧日志在ARP模块里加一个计数器统计收到的ARP请求和发送的ARP回复。如果请求收到但没回复说明ARP模块的逻辑有bug。检查MAC地址配置确认FPGA的MAC地址配的确实是单播地址没有和别的设备冲突。用tcpdump -i eth0 arp在服务器上抓包看ARP请求和回复的交互过程。我当时遇到的一个奇怪现象是ping能通但UDP包发不出去。后来发现是应用层把目的MAC地址写死了一个错误的值覆盖了ARP学习到的结果。代码里加了一个开关如果应用层提供了目的MAC则优先用应用层的值否则用ARP表的值。这个问题才解决。5.2 小包场景吞吐上不去100G线速不是所有包长都能达到的。按照以太网标准线速下的包转发率pps是固定的。100Gbps / (84B * 8) ≈ 148.8Mpps64字节包20字节前导码和IPG。如果测试工具配置的包长小于这个门槛测出来的吞吐就会低于100G这不是FPGA的锅。遇到小包吞吐不达标先算一下理论极限。用iperf3测64字节包时实际吞吐可能只有60-70Gbps这主要是因为测试工具自身发包率跟不上并不是网络链路的问题。我用专用的发包工具比如Scapy的sendp配合多线程也测过最高也就到80Gbps左右。真正要测线速还是得用网络测试仪或者用FPGA本身作为发包器。5.3 长时间运行后出现偶发丢包这个坑我调了很久。现象是运行一两个小时后偶尔丢几个包但丢包率极低千分之一都不到。一开始怀疑是温度问题用温度传感器看FPGA的die温度最高72度还在安全范围。后来才发现问题出在复位信号上CMAC的复位信号在系统长时间运行后偶尔会出现毛刺导致CMAC瞬时重置那一个瞬间的包就丢了。解决方法是给CMAC的复位信号加了一个毛刺滤波器连续几个周期稳定为高才认为是有效复位问题就消失了。这也提醒我板级验证时不仅要看功能是否正确还要做长时间的稳定性测试。5.4 常见问题速查表问题现象可能原因解决措施上电后link灯不亮光模块型号不对或光纤接反更换模块确认收发方向ARP请求发不出去MAC地址配置错误或ARP模块未实例化核对MAC地址检查例化大包线速OK小包吞吐低测试工具发包率不足换用专用发包工具或FPGA内部发包长时间运行偶发丢包复位毛刺或FIFO溢出增加复位滤波加大FIFO时序收敛不了组合逻辑过深或跨SLR布线拆分流水线增加Pblock约束CMAC init_done一直拉不高GTY参考时钟异常或复位异常检查时钟源和复位时序6. 移植经验总结与后续扩展方向这次移植开源100G FPGA UDP协议栈整体走下来我最大的感受是开源的代码质量足够高真正难的不是协议本身而是整个系统的集成和验证。CMAC的配置、时序收敛、跨时钟域处理、以及板级调试的方法论这些才是真正拉开差距的地方。如果你也想尝试建议不要一上来就追求100G先把10G/25G的方案跑通熟悉CMAC的子类配置之后再平滑迁移到100G心理压力会小很多。这个项目后续还有很多可以扩展的方向。我个人目前的计划是加一个简单的用户逻辑比如基于UDP载荷的实时数据过滤或者特征提取真正把FPGA的计算能力和网络能力结合起来。尝试PCIeDDR的完整方案实现一个带DDR缓存的100G UDP采集卡这样就能把数据从光纤转到主机内存里。对比一下不同厂商的100G方案比如Intel的Agilex系列看看开源代码在两个平台之间的可移植性如何。最后分享一个我自己的小习惯每改一个模块先跑一遍回环测试再继续往下做。回环测试是调试的王道它能帮你把复杂问题快速隔离到“物理层”还是“协议层”省掉大量瞎猜的时间。希望大家都能顺利跑起自己的100G链路有遇到问题欢迎多交流。