作为常年混迹在FPGA网络处理这块的人,我先说个扎心的局面:10G/25G的UDP协议栈在FPGA上跑通后,你会觉得“网络协议也就那么回事”,但一旦接到“把UDP栈移植到100G上”的活儿,心态很快就会崩。100G不是简单地把总线位宽从256bit扩到512bit,也不是把时钟从156.25MHz提到322.265625MHz就行——MAC/PCS、FEC、校验和、时序收敛、DMA接口,任何一个环节都能让你在实验室耗掉两周。
这篇帖子记录一次完整的100G FPGA UDP移植上板测试过程。代码主体基于开源项目Verilog-Ethernet,FPGA侧用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以上的协议栈是自己写还是用现成的开源。
| 方案 | 协议层覆盖 | 复杂度 | 适用场景 |
|---|---|---|---|
| 全自研RTL | UDP/IP/ARP全写 | 高,开发周期数月 | 需要极致定制、特殊处理逻辑 |
| Verilog-Ethernet | UDP/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 硬件环境准备
这次用的板卡是VCU118,FPGA型号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送给用户逻辑。
发送方向:用户逻辑产生的数据先写进FIFO,UDP 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 <= 512'd0; has_data <= 1'b0; last_reg <= 1'b0; end else if (rx_axis_tvalid && rx_axis_tready) begin data_reg <= rx_axis_tdata; has_data <= 1'b1; last_reg <= rx_axis_tlast; end else if (tx_axis_tvalid && tx_axis_tready) begin has_data <= 1'b0; 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.2:
ip 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调到9000:
ip 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每收一个包就中断一次CPU,100万PPS就能把CPU打满。解决思路是多队列+RSS+中断合并,让不同流的包分散到不同CPU核,中断事件合并到一定数量再上报。Corundum在设计时就考虑了这些,所以真要做生产级100G网卡,Corundum确实是比手搓栈更靠谱的起点。
5. 问题排查与避坑记录
5.1 常见问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 光模块link down | 模块损坏、Polarity不对、光纤故障 | 换线换模块,ethtool看link状态 |
| ping不通但链路up | ARP没通、ICMP被忽略、IP配置错误 | 服务器上抓包看ARP请求,检查FPGA侧ARP表 |
| 打流丢包率异常高 | FIFO深度不足、用户逻辑反压、CPU瓶颈 | 看FPGA侧ILA,确认tvalid/tready配合是否正确 |
| Wireshark报checksum incorrect | 网卡offload导致误报 | 在接收方向验证,或用tcpdump看原始包 |
| 时序WNS为负 | 校验和加法树过长、计数器链太长 | 拆分加法树、打拍、调整综合策略 |
| CMAC错误计数增加 | 光模块信号质量差、FEC没开 | 查看CMAC IP的统计寄存器,考虑开启RS-FEC |
| UDP包长度不对 | 用户逻辑tlast时序错误 | 检查AXI-Stream的tlast是否随最后一个tdata周期送出 |
| 链路能up但数据不通 | TX/RX差分对接反、光模块收发反 | 检查原理图、交换TX/RX测试 |
5.2 避坑技巧
第一个避坑经验:开源UDP栈的IP/MAC地址参数,不一定只在一处设置。比如ARP模块、UDP模块、IP模块各有一份参数,如果只改了UDP模块的IP,ARP模块还是默认值,发出来的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这条路的底层逻辑你已经摸清了,后续就是在这个框架里添砖加瓦的事。