ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

开源100G FPGA UDP协议栈移植实战:从仿真到上板全记录

开源100G FPGA UDP协议栈移植实战:从仿真到上板全记录 这期拿一个硬核项目开刀开源100G FPGA UDP协议栈移植并成功上板跑通。这活儿听起来高大上其实就是把开源生态里几大金刚——比如Alex Forencich那套verilog-ethernet或者Xilinx/Intel自家的IP核从仿真环境里拽出来塞进真实的FPGA芯片让它和外面的交换机、网卡、测试仪真刀真枪地通信。我这次做的是100Gbps这一档用的是Xilinx UltraScale系列板卡底层物理层走的是4路25G高速收发器QSFP28光模块。整个过程从方案选型、代码移植、时序收敛到最后上板实测抓包踩了不少坑也沉淀了不少经验这篇就完整还原一下。这个项目适合谁要么是你正在做高速网络抓包、报文处理、硬件加速卡要么是你手里正好有一块带100G光口的高端FPGA开发板却不知道怎么让UDP先跑起来。无论你是刚入门想搞UDP offload还是做产品想评估100G UDP的可行性这篇都能帮你省下至少两周的摸索时间。1. 项目整体设计与方案选型1.1 为什么是FPGA做100G UDP而不是CPU先说一个很多人会问的问题100G UDP用服务器网卡加DPDK不香吗确实香x86跑到100G线速转发UDP小包DPDK能扛住大部分场景。但硬件加速、确定性延迟、纯用户逻辑自定义报文格式这些需求网卡就顶不上来了。FPGA做100G UDP的核心价值是你可以完全定制UDP载荷的生成与解析逻辑把校验和计算、MAC地址过滤、会话管理全部下沉到硬件流水线延迟做到纳秒级还不占CPU。但代价也很明显100G的线速是148.8Mpps按64字节小包算意味着每个时钟周期大概3.1ns322MHz要处理接近半个包这对流水线设计压力非常大。所以做100G UDP第一件事是接受一个现实——你不可能像100M/1G时代那样“串行”地处理包头必须打满并行度。1.2 开源方案怎么选三足鼎立我在这个项目里对比了三种可行的路径Alex Forencich的verilog-ethernet库GitHub上star量最高那一套。它包含eth_mac、udp_complete、axi_udp_lite等完整模块支持到100G纯SystemVerilog写的可综合、可仿真。这套代码最大的优点是结构极其清晰每个模块都带AXI4-Stream接口方便拼接数据通路。缺点是文档相对精简很多参数得自己对着源码啃。Xilinx 100G Ethernet MAC硬核IP。UltraScale的GTH/GTY收发器自带100G MAC硬核配合Vivado里的xdma、axi_ethernet等IP使用。硬核的优点是时序有保证资源占用极低FEC前向纠错也集成了。缺点是验证非常依赖IP配置出问题不好排查而且IP核产生的事务层消息如PCS状态、FEC统计需要单独解析。自研简化UDP协议栈。只实现IPv4/UDP的发送和接收定长包或者简单拼接。优点是完全可控资源最少。缺点是没有完整MAC和ARP逻辑基本只能点对点场景用脱离不了PC网卡当测试对端。最终我选了方案1的代码骨架 方案2的MAC硬核/软核做承载用verilog-ethernet库里的UDP/TX、UDP/RX、ARP模块框架底层MAC和PHY用Vivado的100G IP核来封装。这样既省去我写高速MAC的麻烦又能保留开源代码清晰的上层逻辑。这个组合也是目前不少开源网卡项目在走的路线。1.3 硬件平台和测试环构建板卡选择上我用的是一块国产KCU105级别的高端板XCVU9P芯片板载QSFP28光口直接插100G SR4光模块和测试仪。如果没有100G的板卡用多条10G/25G通道做链路聚合也是可行降级方案但UDP层没区别重点在MAC配置。测试环路上我搭了两种模式自环模式把FPGA侧发送的100G信号用光纤衰减器直接引回同一块板卡的另一个光口或者内部把TX回环到RX用于验证内部数据通路。外部模式FPGA板卡连一台支持100G的服务器网卡为Mellanox ConnectX-5 EX用iperf3和Wireshark做收发验证。刚开始做任何UDP移植我都建议先用内部自环把RTL逻辑跑通再上外部设备联调不然问题混合在一起很难定位。2. 核心细节解析与移植要点2.1 UDP协议栈在FPGA上的分工UDP通信本质分四件事MAC层收/发以太网帧、IP层解析/构造IP报文、UDP层解析/构造UDP数据报、以及上层业务数据的组装与搬运。在FPGA里前三层都体现在RTL模块级联上上层业务则由你自定义的AXI-Stream主设备比如DMA、计数器、FIFO来驱动。移植的核心是理清发送通路和接收通路两条流水线发送通路的典型流程是业务模块把待发送数据写成AXI-StreamTVALID/TREADY/TDATA/TKEEP/TLASTUDP TX模块自动加上8字节UDP头并计算UDP校验和可选置零IP TX模块加20字节IPv4头源/目的IP、总长度、协议号校验和MAC TX模块补MAC地址、前导码、FCS校验从GMII/AXI-Stream接口交给底层PHY接收通路反过来PHY收到的数据先进入MAC RX剥掉前导码/FCSIP RX校验版本号、总长度、校验和过滤目的IPUDP RX校验端口号、长度、校验和剥离头后输出载荷这个链条上最容易出错的是仲裁、背压、以及包长度字段的统计。UDP头里的length字段是UDP头加数据的长度IP头里的total_length是IP头加UDP头加数据的长度差一个字节都不行抓包一看全是checksum offload错误就是这个问题。2.2 关键参数和总线位宽选择100G UDP总线的位宽选择直接决定时序收敛难度。Alex的verilog-ethernet库里MAC层接口位宽通常是DATA_WIDTH512位跑322MHz。但UDP层为了减轻时序压力常见做法是接口位宽降到DATA_WIDTH256位跑644MHz或者DATA_WIDTH128位跑1.2GHz——后者基本不现实。项目里我实际用的是512位宽频率跑到312.5MHz这是为了和100G MAC IP的用户时钟对齐含64B/66B编码开销折算。这样的总线位宽意味着一个周期最多能搬运512bit64字节也就是一个普通以太网帧的一半所以处理小包时一个包至少占2个周期需要特别注意TLAST信号的时序对齐。时钟架构也是移植里的大坑。100G以太网在Ultrascale上内部用户时钟通常分三档gt_refclk156.25MHz给GT参考时钟、coreclk线速的1/64或者1/66大概312.5MHz/322.266MHz、axi4_lite_clk控制总线。我实测下来UDP协议栈的逻辑最好全部跑在coreclk域上层的业务数据如果来自另一个时钟域比如DDR控制器时钟必须用同步FIFO做跨越绝对不能偷懒直接打拍子。2.3 ARP模块和MAC地址处理只做UDP RX/TX但不管ARP板卡一接交换机就抓瞎因为对端不知道你的MAC地址。Alex库里的arp_cache和arp_eth_rx/tx模块是通用的我在移植时只改了里面的表项深度和默认网关IP。这里有一个很实用的参数ARP_CACHE_COUNT默认是4如果对端设备多比如同时连交换机、测试仪、服务器建议提到16或32不然表满了新ARP请求直接丢表现为偶发性丢包极难排查。另一个细节是ARP请求响应的MAC地址过滤很多FPGA例程不检查ARP响应里的源IP直接学表这在实验室环境没问题但在真实网络里会被ARP欺骗攻击影响我的做法是加了一个可配置的静态白名单表。2.4 校验和计算与卸载UDP校验和、IPv4校验和是移植中最容易出Bug的部分。很多开源代码为简化实现把UDP校验和直接置0合法UDP允许checksum为0表示不校验但IP校验和必须正确。Alex库里用的是经典的“16bit求和后取反”逻辑关键点是在数据包流经时一字一字地累加最后对总长度要补字对齐。如果数据载荷是奇数字节校验和的累加要补零算漏了这一条偶发一个错包就是你排查一整天都找不到原因的那种。另外如果你用网卡发UDP给FPGA对端网卡会把UDP checksum offload交给硬件计算你抓包看到checksum是错的也正常但如果是FPGA发给网卡网卡一般只做接收校验不再重算。所以板卡端一定按标准算好否则对端应用层收不到数据。3. 实操过程与核心环节实现3.1 Vivado工程搭建和IP集成我先说结论不要试图把所有逻辑全写在顶层Verilog里100G项目编译一次半小时起步必须模块化。我在Vivado 2022.2里新建工程后做这几步例化100G Ethernet MACIP核速率选100Gbps接口选AXI4-Stream数据位宽512bit。例化Xilinx UltraScale GTY Transceiver参考时钟选156.25MHz线速率53.125GbaudPAM44通道绑定成一个QSFP28口。这里有个坑如果板卡的QSFP28四路共用同一个GT参考时钟源必须要保证refclk连接正确不然只有部分通道能link up。把Alex库里的udp_complete.sv、ip_complete.sv、arp_cache.sv、eth_mac相关RTL文件全部添加进工程注意文件依赖顺序SystemVerilog的package文件要在使用它的模块之前编译。顶层连接业务FIFO输出 -udp_complete的input_axis又分为ARPUDP两条通道 - MAC IP的s_axis - GTY - 光模块。3.2 代码级移植工作详解Alex库在设计时是按“独立于厂商IP”来写的它自带一个eth_mac参数化模块可以直接用RTL实现MAC。但到了100G我建议直接放弃他自带的100G MAC性能优化有限且没有PCS/FEC层改为对接Vivado IP的MAC。所以核心移植工作变成了一个“适配层”把udp_complete输出的标准AXI-Stream接口映射到axi_ethernetIP的用户接口上。这一步的代码逻辑容易但容易犯错的点在于时钟域和复位udp_complete里所有模块都是同一个时钟而axi_ethernetIP的MAC用户接口时钟是clk_322m它和GT的时钟相位未必严格对齐。我最终在udp_complete外部包了几层同步FIFO每个axis方向各一个FIFO深度256就够了保证两侧总线位宽一样都是512bit频率抖动就滤掉了。复位必须用IP输出的tx_axis_aresetn/rx_axis_aresetn同步到各自时钟域再释放。直接拿外部按钮复位或者全局复位GT的initial sequence没完成链路就异常。3.3 上板测试步骤完整记录环境准备清单FPGA板卡XCVU9PQSFP28光口100G SR4光模块一对或者一根100G DAC直连铜缆对端服务器Mellanox ConnectX-5 Ex网卡安装Linux RDMA驱动不跑RDMA只当普通网卡用测试工具iperf3UDP模式、Wireshark、本机自带ethtool第一步自环模式验证在Vivado里把GT TX直接内部回环到RXloopbackNear_End_PM不接光模块。这一步如果通了说明RTL逻辑本身没大问题。我在仿真里跑了几个随机包上板后立即抓UDP发现能通但丢包率3%后来查到是自环FIFO深度不够反压太紧改深后降到0%。第二步外部光模块直连接上QSFP28光模块到服务器网卡两边配同网段IP比如FPGA固定10.0.0.2/24服务器10.0.0.1/24。这里注意不要在服务器上把网卡配成默认路由否则ARP和路由表会干扰直连链路的调试。先在FPGA端写一个最简单的固定载荷生成模块每秒发1000个UDP包包长512字节目的IP 10.0.0.1目的端口9000。服务器上用tcpdump -i ens1f0 udp port 9000监听。如果能看到包恭喜你UDP TX通路通了。第三步双向收发和打流FPGA RX端把收到的UDP载荷原样回传loopback业务服务器端用iperf3 -s -p 9000开服务器另开一个终端用iperf3 -c 10.0.0.2 -u -b 0 -l 512 -t 60打流。注意-b 0表示不限带宽尽量压榨。这时看吞吐能不能到线速。我实测下来小包128字节到不了线速约40Gbps瓶颈在MAC IP的FIFO仲裁逻辑和PHY的包间隙大包1024字节以上能稳定跑到100G线速的96%-98%。如果你看到吞吐低于50%优先查TREADY拉低的频率也就是MAC在反压你因为输入FIFO满了。第四步Wireshark抓包验证我用Wireshark在服务器端采集FPGA发来的UDP包重点看IP头的total_length是否等于UDP头长度载荷长度IP checksum是否OKUDP checksum是否为0我改成0省一点逻辑但别在严格测试环境这么做源MAC地址是否等于FPGA侧设计值这里有个非常值得说的细节Wireshark显示“udp.checksum 0x0000”不一定代表错误因为很多网卡驱动默认开启RX checksum offload内核可能已经把字段填充为0表示未验证。要看真正的错误必须是ifconfig网卡看rx_crc_errors、ethtool -S看rx_errors、rx_missed等计数器我当时在这个上面被白坑了一晚上。3.4 性能压测数据我放一组比较有代表性的数据供参考测试项包长(Byte)吞吐(Gbps)丢包率说明UDP TXFPGA-PC64380%PC网卡丢服务器端单队列接收导致UDP TXFPGA-PC256820.02%瓶颈在PC网卡和DMAUDP TXFPGA-PC1024970%接近线速UDP RXPC-FPGA102499.20%数据经同步FIFO存储无丢包UDP 双向同时512190总计0%板卡双向全双工还测了时延从FPGA逻辑发起请求到收到响应光模块DAC线缆对端网卡回环的RTT约1.2微秒这个数值已经比绝大多数软件协议栈低两个数量级。4. 常见问题与排查技巧实录4.1 现象表快速定位我把踩过的坑列个速查表按“现象-原因-解法”结构组织。现象可能原因检查方法解决方案链路完全不UPGT refclk未锁定、光模块未插好ibstatus或FPGA内部gtpowergood信号检查refclk布线换直连DAC线自环正常外联不通FEC/RS-FEC配置不匹配ethtool -i看对端FEC模式把100G IP的FEC模式设为RS-FEC(544,514)或关闭服务器TCP/UDP能收但Wireshark显示CRC错误MAC层FCS生成错误ethtool -S看rx_fcs_error检查TX端CRC引擎是否按Ethernet标准多项式0xEDB88320初始值0xFFFFFFFF结果异或偶发丢包同步FIFO深度不够反压没及时拉低看TREADY平均拉低时间加大FIFO深度到512以上只有大包通小包全丢以太网帧间隙IPG过小或内部仲裁冲突看MAC的frame_lost统计调整MAC IP的tx_ipg最小值打开MAC的pause帧支持ARP能通但业务UDP不通端口过滤/会话表逻辑写错抓包看目的端口检查UDP RX模块端口比较逻辑是否大写小写混淆4.2 时序收敛的独家心得100G UDP工程在Vivado里综合、布局布线后时序是最让人头大的。我总结下来几个有效手段所有模块的输出寄存器打一拍。UDP层到MAC层的AXI-Stream总线长达512bit不加输出寄存器跨模块组合逻辑必挂。代价是延迟多1个周期3.2ns完全可接受。校验和加法器改流水线结构。16bit加法器不宜做组合逻辑大串会导致关键路径超长。用两级流水先算部分和再与进位相加实际上校验和模块内部的#(PIPELINE2)参数可以开起来。把UDP校验和功能关掉能救疲劳时序。项目里为了赶一个演示节点的时序我把UDP校验和强制传0省掉了整个加法器链时序立刻收敛到负slack接近0。如果业务环境允许比如内网互信这一步减持非常值得。FIFO的almost_full信号可以提前换到下一级时钟域。当时为了降低同步FIFO满信号的延迟我用两级同步器提前两个周期锁存almost_full效果明显把反压抖动控制在了3个周期内不丢包。4.3 Wireshark与iperf3的小技巧iperf3打UDP流时默认窗口大小很小-w 4M加上不然后端网卡缓冲直接丢包。我们还遇到过Windows端用iperf从WSL2打到FPGA不通的问题本质是WSL2的虚拟交换机绕过了物理网卡务必用-B指定物理网卡IP或者在Windows PowerShell下用原生iperf3跑。Wireshark上抓UDP包如果流量太大100G线速网卡都爆先在交换机或服务器NIC上配置tcpdump -s 96 -n只抓包头减少抓包文件写入压力。别想着把100G全流量都抓下来没有一块普通盘能接住正确的做法是分层验证物理层看错包计数器IP层看tcpdump头部业务层看应用日志。4.4 排查丢包的一个高效方法论丢包定位顺序强烈建议从外到内物理层ethtool -S看rx_crc_errors_phy、rx_symbol_errors、link_down_events如果有增加查光模块、DAC、FEC。MAC层看rx_pause、rx_oversize、rx_undersize计数判断是否存在帧间隙/长度错误。IP/UDP层在FPGA内部加计数器drop_cnt、checksum_err_cnt、total_rx_cnt打印到串口或AXI-Lite寄存器。应用层上位机PC看是否有UDP buffer overflowLinux里用netstat -su看RcvbufErrors。排查时每一步都留一个全局计数器比任何仿真都管用。我最后就是靠FPGA内部加的rx_udp_crc_err_cnt计数器定位到IP校验和偶算出错才修好一个隐蔽bugIP头总长度字段在倒序字节序下少加了一次头长。5. 资源占用率与性能瓶颈分析5.1 FPGA资源消耗估算移植完成后我统计了资源占用放在Vivado的Report Utilization里看逻辑资源LUTUDP TXRXARP完整协议栈约 24k LUT占XCVU9P不到2%大部分被FIFO和控制状态机吃掉。寄存器FF约28k。BRAM同步FIFO和ARP缓存共约12块(36Kb)。高速收发器4个GTY通道一共用了一个QSFP28口完整的100G全速率链路需要占用2个GT reference clock。DSP基本没用UDP本身不是计算密集主要是数据整形和缓存。如果你用的是Intel的Agilex或Stratix 10Vivado换成Quartus后IP例化基本对标25G/100G Ethernet MACHIP代码移植量主要在对齐avalon-ST和AXI-Stream接口的字节序和valid/ready时序上。5.2 100G UDP的真正瓶颈在哪上板测完之后我仔细想了一下硬件自身的瓶颈其实很少真正制约100G UDP实用化的是三件套DDR带宽与DMA架构。UDP数据要落地到内存或搬运到CPUDMA描述符的效率和读写内存带宽决定上限。如果只是把数据在FPGA内做转发line rateFPGA可以轻松跑到线速一旦加DDR缓存就要考虑内存页冲突和带宽损耗常见方案是多bank交叉加上Write Buffer。PC侧接收能力。服务器的PCIe通道、网卡中断速率、应用层Socket缓冲区随便一个都可能成为瓶颈。我测试时如果不调大rmem_max和net.core.rmem_defaultUDP接收侧到100Gbps必然在几十毫秒后开始丢包。像Windows系统还要去注册表改GlobalUdpSystemBuffer这里引入的热搜词“修改Window全局UDP系统缓存区”说的就是这个痛点调大后对端打流稳定性提升很明显。协议栈的状态存储能力。100G线速要求每个包处理周期极短如果UDP会话数很大比如同时几万条流哈希表查询和老化机制就要设计好。我这边用静态查表适合单流压测真正的多流场景建议改用BRAM里的关联存储器CAM或者开放地址哈希表并加老化定时器。6. 个人实操体会与后续扩展最后聊点贴身的经验。FPGA 100G UDP移植你问我最难的是哪一步不是RTL设计也不是Debug而是心态从“仿真过了”切换到“上板要稳”。仿真里你拍着胸脯说“这代码没问题”上板之后一堆K7、V7、UltraScale不同工艺带来的时序、复位、相位问题全部铺面而来。我这次最深的教训是永远先量一下复位和时钟的上升沿对齐情况很多诡异的功能错误最后发现是异步复位释放太晚导致第一个包状态机错乱。另外一个建议刚开始别追求做到100G线速先把功能跑通。先用1G/10G的MAC IP验证UDP业务逻辑再切到100G PHY这样每一步的风险都拆小了。我是直接在100G上干结果一个小小字节序错误就花了我整整两天的抓包时间。至于后续扩展我打算在这套100G UDP链路上再叠三样东西一个轻量的UDP Offload完整状态表支持NAT和端口映射用来做硬件网关原型。RS-FEC和Link Training的完整状态上报把物理层自愈能力做成标准寄存器接口方便上位机监控。把协议栈接入XDMA或者QDMA打通服务器内存和FPGA之间的零拷贝通路这样100G UDP就能真正变成一个低延迟网络加速卡的基础。如果你也在折腾同款项目希望这篇能帮你省下我踩过的那两周。有啥具体问题欢迎按现象描述清楚我尽量回复。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进