ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

100G FPGA UDP协议栈移植实战:从开源代码到上板调试

100G FPGA UDP协议栈移植实战:从开源代码到上板调试 作为常年混迹在FPGA网络处理这块的人我先说个扎心的局面10G/25G的UDP协议栈在FPGA上跑通后你会觉得“网络协议也就那么回事”但一旦接到“把UDP栈移植到100G上”的活儿心态很快就会崩。100G不是简单地把总线位宽从256bit扩到512bit也不是把时钟从156.25MHz提到322.265625MHz就行——MAC/PCS、FEC、校验和、时序收敛、DMA接口任何一个环节都能让你在实验室耗掉两周。这篇帖子记录一次完整的100G FPGA UDP移植上板测试过程。代码主体基于开源项目Verilog-EthernetFPGA侧用Xilinx VU9P板卡物理层走QSFP28光模块对端用一台带Mellanox ConnectX-5 100G网卡的x86服务器。测试工具就用iperf3打UDP流配合Wireshark和tcpdump做报文验证。整篇的重点不在“怎么抄代码”而在“移植时为什么这么改、上板后怎么调、打流时怎么看数据”适合正在做100G网卡设计、FPGA UDP offload、或者被领导安排从25G往100G迁移的同行参考。1. 项目全景与方案选型思路1.1 100G UDP到底在解决什么问题先说使用场景。FPGA做网络加速时UDP是绕不开的协议。数据中心里AI推理、加解密、存储集群的数据搬运很多都走UDP/RoCE因为TCP栈在硬件里实现又贵又复杂而UDP足够简单可靠性交给应用层去补。到了100G这个速率CPU纯软处理UDP的代价已经很高100G线速下1500字节报文每秒约有810万个64字节最小包更是高达1.48亿个任何一个包都要进CPU多核也扛不住多久。所以把UDP收发卸载到FPGA上是性能和功耗双赢的做法。100G带来的第一个挑战是数据通路带宽。100Gbps换算过来大约是12.5GB/s这个速率下DDR4带宽、PCIe Gen3 x16的极限约110Gbps实际用户态数据95Gbps左右、AXI-Stream总线位宽全部开始吃紧。第二个挑战是时序。Xilinx 100G CMAC用户接口常见配置是512bit数据位宽配合约322MHz的用户时钟任何组合逻辑超过一定深度时序直接崩。第三个容易被忽略的挑战是调试手段ILA在512bit宽的总线上抓信号一两万个采样点的深度根本不够看而且占资源严重。1.2 开源方案横向对比做100G UDP摆在我们面前有几条路全自研RTL、基于Verilog-Ethernet移植、基于Corundum/OpenNIC这类完整开源NIC方案。全自研的坑先不提光MAC/PCS/FEC这几层就够喝一壶100G的PCS带64B/66B编码、RS-FEC自己写不现实基本都是调IP核。所以本质区别在于MAC以下用IP核MAC以上的协议栈是自己写还是用现成的开源。方案协议层覆盖复杂度适用场景全自研RTLUDP/IP/ARP全写高开发周期数月需要极致定制、特殊处理逻辑Verilog-EthernetUDP/IP/ARP/Checksum齐全模块化清晰中可裁剪纯UDP数据通路、实验验证、对PCIe DMA需求不强Corundum/OpenNIC完整NIC含PCIe DMA、多队列、RSS高涉及驱动和内核生产级网卡、数据中心场景需要CPU与FPGA深度交互这次项目的目标就是把UDP数据通路跑通并测出真实吞吐不涉及复杂的PCIe DMA队列管理所以核心选Verilog-Ethernet做协议层Xilinx CMAC IP做物理层接入。Verilog-Ethernet比较香的一点是模块边界清晰ethernet、ip、udp、arp、checksum都是独立模块用AXI-Stream接口串起来改参数就能换IP地址和MAC地址对上板验证非常友好。如果你的目标是一步到位做带PCIe DMA的100G网卡Corundum更合适它有完整的RX/TX引擎、多队列中断、流控就是移植成本高一些驱动要重新编译和主机内存交互的调试点也多。这个项目先把UDP通路验证完后面再考虑往Corundum迁。1.3 硬件环境准备这次用的板卡是VCU118FPGA型号VU9P板载QSFP28光口。对端服务器是一台双路Xeon配Mellanox ConnectX-5 Ex双口100G网卡。选VU9P不是因为它最便宜而是因为LUT和BRAM资源充足100G的FIFO、校验和逻辑、ILA调试逻辑同时开也不紧张。资源少的板子为了省BRAM去优化FIFO深度会非常痛苦。光模块这里要特别提醒100G QSFP28光模块有两种常见形态SR4和LR4。SR4走多模光纤配合MPO接头短距离100米内够用LR4走单模能到10公里。实验室环境用SR4加MPO线缆最便宜但Polarity一定买对否则光路不通会误判成逻辑问题。如果不想碰光模块直接用100G DAC高速线缆也行短距离3~5米就没那么多麻烦事。软件环境用的Vivado 2022.1。这里有个经验之谈100G CMAC IP在不同Vivado版本里接口时序和寄存器映射会有细微变化尤其是FEC配置和RS-FEC的选项。建议选定一个版本就别随便升IP核重新生成后时序可能大变样。2. 开源UDP协议栈的结构拆解与移植要点2.1 数据通路架构开源栈移植的关键是先看懂它的数据通路别上来就往工程里塞文件。Verilog-Ethernet的100G UDP数据通路大概是这样的接收方向QSFP28光模块信号经过CMAC IP解出AXI-Stream进入RX MAC模块做CRC校验、FCS剥离然后到UDP RX模块做IP首部解析、UDP校验和校验、端口匹配最后通过AXI-Stream FIFO送给用户逻辑。发送方向用户逻辑产生的数据先写进FIFOUDP TX模块自动填UDP首部源端口、目的端口、长度、校验和然后IP TX模块填IP首部源IP、目的IP、TTL、协议号ARP模块负责查MAC地址表或发ARP请求最后进CMAC的TX接口发出去。这个结构里有一个容易踩的坑UDP TX和IP TX的校验和是分模块算的UDP校验和会把伪首部源IP、目的IP、协议号、UDP长度一起算进去。做移植时如果你把UDP模块单独拿出来用忘了给它喂正确的源IP和目的IP发出的包在Wireshark里会显示“checksum incorrect”而且不同网卡对错误校验和的容忍度还不一样有的网卡直接丢丢得你莫名其妙。CMAC IP的AXI-Stream接口是512bit位宽用户逻辑里如果直接处理512bit总线组合逻辑很容易成为时序瓶颈。我的做法是在CMAC输出和协议栈之间加一个异步FIFO把跨时钟域问题解决掉同时把FIFO的数据位宽调整到协议栈更舒服的宽度。虽然FIFO会带来几个时钟周期的延迟但对UDP这种无连接协议来说微秒级的延迟完全可以接受。2.2 校验和与分片处理校验和是100G UDP移植里最需要动脑子的地方。UDP校验和算法是“反码求和”把伪首部、UDP首部、数据按16bit对齐拼起来逐16bit相加超出16bit的进位要回卷再加最后按位取反。10G时代数据位宽64bit一拍校验和逻辑很容易做到100G数据位宽512bit一拍进来32个16bit word进位链如果做成串行延迟很长Fmax上不去时序一定挂。正确做法是把32个16bit word做二叉树求和第一级16个加法器、第二级8个、第三级4个、第四级2个、第五级1个树的深度是log2(32)5级。每一级做完后把进位回卷处理最后取反。这个加法树在FPGA里实现LUT资源消耗不小但对时序友好得多。还有一个细节UDP数据长度如果是奇数校验和计算时最后一个字节要补0x00再算这是标准里明确写的但在开源栈里不一定处理得很明显。移植时如果你的用户逻辑往UDP栈里送的数据经常是奇数字节长度一定检查一下校验和模块是否有这个补零逻辑。分片问题也得提前想清楚。硬件UDP栈一般不做IP分片和重组因为重组需要缓存乱序报文资源消耗极大。100G线速下分片包的乱序到达概率不低硬件做重组性价比很差。我采用的做法是IP首部里fragment offset不为0或者MF位为1的包直接丢弃并计数同时建议应用层把发送MTU调小从源头上避免分片。实测大多数测试场景尤其是iperf3打流MTU控制在9000以内分片情况很少出现。2.3 时序收敛经验100G移植里时序收敛比10G麻烦一个数量级。我在VU9P上的实际经历是第一版综合后WNS是负数关键路径就在UDP校验和加法树和用户逻辑的计数器链上。解决思路有几个。第一校验和模块的加法树尽可能用DSP或LUT的级联结构减少中间寄存器的扇出第二用户逻辑里那些宽位宽的计数器比如包计数、字节计数不要用单一大计数器拆成多个小计数器再加总或者接受一个时钟周期的延迟用流水线寄存器输出第三跨时钟域的FIFO一定要用IP核生成别自己手写异步FIFO——100G下FIFO的读写指针同步逻辑很容易搞出亚稳态问题而且问题出现得非常随机很难排查。时序约束方面CMAC IP内部的约束是IP核自带的不用操心。需要重点约束的是用户逻辑和FIFO之间的路径特别是异步FIFO两侧的时钟分组set_clock_groups -asynchronous。另外ILA调试核挂在512bit总线上会增加布线压力实测ILA核开得越多WNS越差所以上板前的仿真验证尽量做充分ILA只留最关键的两组信号。3. 移植实操从GitHub仓库到生成比特流3.1 Vivado工程搭建与源码组织从GitHub拉下来的开源栈先不要急着全部加入工程花半小时理一下目录结构。Verilog-Ethernet的rtl目录下有ethernet、ip、udp、common等子目录按功能分好每个子目录里的.sv文件按依赖关系添加。我的Vivado工程组织方式是这样project_root/ ├── rtl/ │ ├── ethernet/ # 以太网MAC层相关 │ ├── ip/ # IP/ARP层 │ ├── udp/ # UDP层 │ └── common/ # FIFO、crc等通用模块 ├── ip/ # Xilinx IP核CMAC、FIFO、ILA ├── sim/ # testbench ├── constraints/ # XDC约束 └── scripts/ # 综合实现脚本源码添加完成后接着例化100G CMAC IP。在Vivado IP Catalog里搜索“100G Ethernet”选择100G Ethernet Subsystem。关键配置项包括Line Rate选100G接口类型选AXI4-Stream数据位宽选512bit时钟频率按IP默认生成。FEC这里先不使能等基本链路通了之后再开启RS-FEC验证这样可以减少变量。IP配置完需要在RTL里做一个顶层wrapper负责三件事连接CMAC和开源协议栈、给UDP栈设置本机IP和MAC、把用户逻辑的AXI-Stream接到UDP栈的收发端口。具体IP和MAC地址的参数化Verilog-Ethernet里通常以parameter形式暴露比如IP_ADDR、MAC_ADDR直接在顶层例化时改掉即可。3.2 用户逻辑如何与UDP栈对接用户逻辑和UDP栈之间就是标准的AXI-Stream接口核心信号tvalid、tready、tlast、tdata。这里我写了一个最简单的echo测试逻辑收到UDP报文后把payload原样回发源IP和目的IP互换。这样做的好处是打流时发什么就能收什么校验和错误、数据位错乱一眼就能看出来。module udp_echo ( input wire clk, input wire rst_n, // rx 方向 input wire [511:0] rx_axis_tdata, input wire rx_axis_tvalid, output reg rx_axis_tready, input wire rx_axis_tlast, input wire [63:0] rx_axis_tuser, // tx 方向 output wire [511:0] tx_axis_tdata, output reg tx_axis_tvalid, input wire tx_axis_tready, output wire tx_axis_tlast ); reg [511:0] data_reg; reg has_data; reg last_reg; always (posedge clk or negedge rst_n) begin if (!rst_n) begin data_reg 512d0; has_data 1b0; last_reg 1b0; end else if (rx_axis_tvalid rx_axis_tready) begin data_reg rx_axis_tdata; has_data 1b1; last_reg rx_axis_tlast; end else if (tx_axis_tvalid tx_axis_tready) begin has_data 1b0; end end assign tx_axis_tdata data_reg; assign tx_axis_tvalid has_data; assign tx_axis_tlast last_reg; assign rx_axis_tready ~has_data || (tx_axis_tvalid tx_axis_tready); endmodule这个模块非常简单但它把AXI-Stream反压的典型场景展示出来了一个数据寄存器收到就存发出去就清空。实际项目中这里的RX/TX FIFO深度至少要能容纳几十个最大帧否则上板打流时会因为反压丢包。FIFO深度不要省100G下一微秒就有约1.5KB的数据进入深度不够丢包率会很感人。3.3 约束与实现流程约束文件的重点有三块时钟、复位、引脚。CMAC IP会用QSFP28的参考时钟生成用户时钟这个时钟在XDC里要创建为primary clock推荐让IP自动处理。复位信号建议做成异步复位同步释放防止复位释放时沿和时钟沿竞争。引脚约束主要看板卡原理图QSFP28的TX/RX差分对、参考时钟、复位、中断引脚一根都不能错。注意CMAC IP的TX和RX引脚如果接反链路完全不通而且光模块不会报错问题排查起来非常迷惑。综合实现时我建议先跑综合看时序报告里的WNS。如果WNS小于0先点开关键路径看是哪个模块再用前文说的加法树优化、打拍、拆分计数器等技巧处理。不要指望一次综合就通过100G设计综合布局布线一次大概要半小时到一小时尽量把修改集中在几次内完成节省时间。另外说一个很多人忽略的问题Vivado里Synthesis和Implementation的策略会影响时序。默认策略下100G很难收敛我通常把Synthesis的Strategy改为“PerformanceExplore”Implementation改为“Performance_ExtraTimingOpt”能显著改善WNS代价是运行时间变长。如果还收不敛再把UltraFast设计方法里推荐的寄存器复制、物理优化选项打开。3.4 仿真先行的必要性上板之前一定要做仿真。不是说开源栈的代码一定有问题而是你自己加的顶层wrapper、用户逻辑、IP配置很可能有接口位宽不匹配、信号名打错这类低级错误。这些错误在仿真里一分钟就能发现上板后用ILA抓要花半天。Vivado Simulator就够了不需要ModelSim。Testbench里例化顶层把CMAC的TX直接回环接到RX然后从用户逻辑侧注入一个UDP报文看能不能正确收回来再验证一下校验和是否正确。开源栈一般自带一些testbench比如arp_testbench、udp_testbench用好这些基础用例能省不少事。仿真里还要重点验证跨时钟域FIFO的行为。CMAC用户时钟和用户逻辑时钟如果不同源在仿真里就要模拟不同频率看FIFO的empty/full信号是否正常有没有出现数据丢失或重复。4. 上板测试全流程与实测数据4.1 环境搭建与接线上板的第一步是物理链路。VCU118的QSFP28口插上光模块光纤连接到服务器网卡。服务器网卡是ConnectX-5用ethtool确认link状态ethtool enp4s0f0np0 | grep Speed # 期望输出Speed: 100000Mb/s如果Speed不是100000Mb/s先别怀疑FPGA逻辑优先检查光模块和光纤。很多“链路不通”的问题都是光模块Polarity不对或者光纤损坏造成的。测试环境里我踩过一次MPO线缆一端是Key Up一端是Key Down插上后光路不通ethtool显示link down折腾了半天才发现是线缆问题。服务器端配置IP地址。需要注意FPGA端的UDP栈IP地址和服务器网卡IP要在同一子网否则ARP都过不去。我在FPGA侧配置IP为192.168.10.1服务器配192.168.10.2ip addr add 192.168.10.2/24 dev enp4s0f0np0 ip link set enp4s0f0np0 up配置完后先ping一下如果能通说明ARP和ICMP处理基本正常。这里有个细节很多FPGA开源UDP栈对ICMP的处理是直接忽略或不回ping不通不一定代表链路不通。验证ARP是否成功可以用arp -a看有没有FPGA侧MAC地址的记录。4.2 iperf3打流测试方法链路通了之后正式测试用iperf3做UDP打流。这个工具大家都很熟但100G下用iperf3有几个坑。第一单线程iperf3很难打满100G。CPU单核处理UDP收包的能力大约在30-50Gbps左右到不了100G。所以要用多流并行iperf3 -u -c 192.168.10.1 -b 40G -l 1472 -t 60 -P 4四个流每个流40Gbps总计期望达到接近线速。实测在ConnectX-5网卡和VU9P的搭配下四流UDP打流能达到约95Gbps在默认MTU 1500下CPU占用在60%~80%之间。再往上加流收益不明显说明瓶颈在PCIe和CPU收包路径。第二UDP的丢包统计是核心指标。iperf3输出的Lost/Total Datagrams就是丢包率。FPGA回环echo模式下实测丢包率为0.000%说明FPGA侧UDP协议栈和CMAC处理没有丢包但PC向FPGA方向单流40Gbps时丢包率会在0.01%~0.1%波动这是CPU收包能力不足导致的不是FPGA的问题。判断瓶颈在哪可以看CPU占用率如果多个核跑满基本就是主机侧瓶颈。第三payload长度。iperf3默认UDP payload是1472字节1500 MTU减IP/UDP头如果想测巨帧性能服务器和FPGA侧都要把MTU调到9000ip link set enp4s0f0np0 mtu 9000MTU 9000下包速率大幅降低CPU开销小更容易跑满100G线速。实测巨帧模式下八流就能稳定跑到98Gbps以上。4.3 Wireshark抓包验证打流的同时在服务器端用Wireshark抓包验证报文内容。过滤条件设udp重点看三层内容源/目的IP地址、源/目的端口、UDP长度和校验和。这里有个常见的误判Wireshark里显示“incorrect checksum”并不一定是报文真的错了。如果你的网卡开启了UDP校验和offload很多网卡默认开那么Wireshark抓到的包其实是网卡驱动软件注入了错误的伪校验和真正的硬件发出的包没问题。判断方法是看同样过滤条件下接收方向服务器接收的包有没有报checksum错误如果只有发送方向报错那就是offload的锅。用tcpdump也能做快速验证tcpdump -i enp4s0f0np0 udp -c 100 -vv能看到IP、UDP端口、长度和校验和字段配合iperf3的丢包数据基本能判断FPGA发出的UDP包是否符合规范。特别要注意IP首部的Identification、Fragment Offset字段如果这些字段异常说明IP层封装有问题。4.4 与PCIe DMA联调扩展如果项目后续要把UDP载荷直接送到主机内存就需要在FPGA侧接入PCIe DMA推荐用Xilinx XDMA或者迁移到Corundum。这里提前说一个经验100G UDP PCIe DMA联调时小包64B、128B的PPS才是真正的挑战。64字节小包在线速下约148.8Mpps每个包在DMA里都需要一个描述符会产生中断。如果DMA每收一个包就中断一次CPU100万PPS就能把CPU打满。解决思路是多队列RSS中断合并让不同流的包分散到不同CPU核中断事件合并到一定数量再上报。Corundum在设计时就考虑了这些所以真要做生产级100G网卡Corundum确实是比手搓栈更靠谱的起点。5. 问题排查与避坑记录5.1 常见问题速查表现象可能原因排查方向光模块link down模块损坏、Polarity不对、光纤故障换线换模块ethtool看link状态ping不通但链路upARP没通、ICMP被忽略、IP配置错误服务器上抓包看ARP请求检查FPGA侧ARP表打流丢包率异常高FIFO深度不足、用户逻辑反压、CPU瓶颈看FPGA侧ILA确认tvalid/tready配合是否正确Wireshark报checksum incorrect网卡offload导致误报在接收方向验证或用tcpdump看原始包时序WNS为负校验和加法树过长、计数器链太长拆分加法树、打拍、调整综合策略CMAC错误计数增加光模块信号质量差、FEC没开查看CMAC IP的统计寄存器考虑开启RS-FECUDP包长度不对用户逻辑tlast时序错误检查AXI-Stream的tlast是否随最后一个tdata周期送出链路能up但数据不通TX/RX差分对接反、光模块收发反检查原理图、交换TX/RX测试5.2 避坑技巧第一个避坑经验开源UDP栈的IP/MAC地址参数不一定只在一处设置。比如ARP模块、UDP模块、IP模块各有一份参数如果只改了UDP模块的IPARP模块还是默认值发出来的ARP请求源IP就是错的对端学习到的MAC表就是错的链路时通时不通。第二个坑是CMAC IP的复位顺序。Xilinx CMAC IP对复位时序有要求复位释放后要等待IP内部PCS状态机锁定再开始收发数据。如果在IP还没锁定时就猛灌数据可能导致内部状态错乱表现就是链路up了但数据全丢。解决办法是例化CMAC时把rx/tx_reset_done信号引出来用户逻辑等这两个信号都拉高后再启动收发。第三个坑是100G光模块的热插拔。实测FPGA上电后热插拔光模块偶尔会导致CMAC IP内部状态异常需要做复位。所以测试时尽量一次性把光模块接好再上电如果中途换模块建议做一个热复位端口方便远程恢复。第四个坑是关于测试工具的。100G下用iperf3单流跑不满是正常的别在单流上死磕。另外iperf3在高带宽测试时建议加--affinity参数把发送进程绑定到特定CPU核能减少调度抖动数据更稳定。如果追求极致的包速率测试建议用DPDK的pktgen可以单核达到几十Mpps的发包能力比iperf3强得多。6. 写在测试之后最后聊一点我自己的体会。开源100G UDP栈移植真正花时间的往往不是改RTL而是搭验证环境和排查物理链路。FPGA侧逻辑只要仿真充分上板后很少有大bug大部分问题都出在光模块、线缆、IP配置这类看起来很不起眼的地方。所以我的习惯是先把10G或25G的链路调通再切到100G别一上来就100G硬啃调试手段跟不上会很痛苦。另外这个项目做完之后如果想继续往下走扩展方向无非是这几个开启RS-FEC提高信号质量、把UDP栈接到PCIe DMA做完整的数据中心网卡、或者用Corundum这种成熟方案替换自己拼的协议栈。每个方向都有不少坑但100G这条路的底层逻辑你已经摸清了后续就是在这个框架里添砖加瓦的事。
RELATED READING

延伸阅读

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