
前段时间接了个活儿要把一套在25G平台上验证过的开源UDP协议栈搬到100G的FPGA板卡上而且要求真的上板打流验证不是仿真跑通就算完。我一开始以为这事儿不难——改改数据位宽、换换IP配置、重新综合一下不就行了结果真上手才发现从环境准备到光口link从代码适配到打流数据达标前前后后折腾了小两周。这篇文章就把整套移植和上板测试的过程完整记录下来包括方案怎么选、代码怎么改、测试怎么打、坑怎么避给后面要做100G FPGA UDP的朋友们打个样。1. 100G UDP方案选型为什么用CMAC硬核加开源UDP逻辑1.1 100G线速场景下软件协议栈为什么扛不住先聊一个很多人容易忽略的前提100G以太网线速是什么概念按UDP小包算100Gbps除以最小以太网帧84字节64字节帧加20字节帧间隙和前导码大约每秒148.8M个包。这个包速率下软件协议栈即使上了DPDK、做了大页内存和CPU亲和性绑定纯软件解析UDP头、管理socket、做内存拷贝也是一件非常吃力的事。更别说延迟抖动、CPU占用率这些指标在金融极速交易、分布式存储转发这类场景里完全没法看。所以100G这个档位硬件UDP协议栈几乎是必需品。FPGA做UDP协议栈的本质就是把以太网帧解析、IP头校验、UDP端口匹配、校验和计算这些固定逻辑用流水线电路实现。好处是每个包的处理延迟基本恒定几十纳秒到几百纳秒级别而且不占CPU。1.2 三条路线对比商用IP、Corundum、CMAC加自研逻辑我当时认真评估过三条路线列个表大家看得清楚方案优点缺点商用完整UDP Offload IP如Xilinx、Solarflare的IP验证充分、性能有保障价格高、黑盒、定制灵活性差很多还得签NDACorundum全开源NIC方案100G MAC、DMA、PCIe全开源社区活跃工程框架庞大依赖其自带的PCIe和DMA架构想只取UDP部分反而费力Xilinx CMAC硬核加开源UDP逻辑硬核MAC免费、稳定UDP逻辑自研可控可裁剪需要自己搞定L2/L3/L4的处理调试工作量大我最后选了第三条把Xilinx的100G Ethernet CMAC硬核作为MAC层UDP协议栈逻辑基于开源工程改造。原因很直接CMAC是FPGA片上的硬核100G以太网的PCS、PMA、FEC这些最麻烦的物理层东西它都管了用户侧只面对一个AXI4-Stream接口省掉了一大堆GT收发器和编解码的调试时间。UDP逻辑层自己写几百行到一两千行Verilog就能覆盖ARP应答、ICMP回显、UDP收发和校验和灵活度很高后面想加多队列、过滤规则都方便。这个选择也符合“移植”这个词的核心。真正从开源项目里拿过来的是协议栈的上层逻辑底层MAC换成了100G CMAC硬核这就引出了后续所有适配工作。2. 移植前的硬件与工程准备板卡、license、CMAC配置2.1 硬件平台评估FPGA资源、收发器、光口先说板卡。100G UDP不是随便一块FPGA就能做的首先得有支持100G的硬核收发器。Xilinx这边Ultrascale系列里的GTY收发器可以支持到100GVU9P、VU3P、VU13P这些型号都行。我手头这块是XCVU9P带QSFP28光口单口100G正好满足需求。选型时有个容易忽略的点就是FPGA的收发器数量和光口数量要对上。100G QSFP28模块内部其实是4路25G SerDes所以FPGA上要至少有4个GTY通道组成一个100G组。VU9P这种级别的芯片GTY数量很充足但如果你用的小芯片可能物理上就凑不出100G通道这点要在项目立项时就确认。服务器端的100G网卡我也提前借好了Mellanox ConnectX-5PCIe 3.0 x16跑100G没问题。网卡驱动和固件版本建议在测试前就升到较新版本老固件对光模块兼容性差后面link不起来会非常痛苦的。2.2 License和IP配置这两个坑一定要提前查接下来是最容易被卡住的环节——CMAC的license。Xilinx的100G Ethernet CMAC IP虽然在Vivado IP Catalog里免费生成但综合实现时会校验UltraScale的license。当时我们板卡配套的Vivado版本里带了对应的license所以没被卡住。但如果你用的是评估版license或者Vivado版本和IP版本不匹配综合到一半报license错误是很常见的。建议动手移植之前先单独建一个最小工程只放CMAC IP完成一次综合和实现确认license没问题了再继续。这一步能帮你把“移植”和“license排障”两件事解耦开避免后面代码改到一半才发现环境根本不能跑。2.3 CMAC的关键配置参数CMAC的配置直接决定上层逻辑怎么写我当时记了几个关键项的配置后面代码全是围绕这些参数设计的用户接口AXI4-Stream数据位宽512bit时钟322.265625MHzFCS让CMAC生成和校验用户侧不干预RS-FEC先关闭直连测试信号质量好减小延迟方便定位问题MDIO接口开启方便读取光模块寄存器信息做排查这里特别说一下512bit322MHz这个概念。数据位宽512bit一拍的带宽是512x322.265625M约等于165Gbps远超100G线速。多出来的带宽是给AXI-Stream的ready/valid握手消耗的实际用户逻辑平均吞吐依然是100G线速。但也正是这个322MHz的时钟频率给后端的时序收敛带来了不小压力这一点后面会细说。移植前工程的顶层结构也要先规划好。我的方案是做一个统一的UDP协议栈模块对外暴露CMAC的AXI4-Stream接口同时挂一组AXI-Lite寄存器用来配置本机MAC地址、IP地址、端口号。寄存器配置这个设计强烈建议保留别把IP写死在代码里否则上板调试时每改一次地址都要重新综合一次功耗和头发都耗不起。3. 从25G到100G协议栈核心模块的改造细节3.1 数据通路位宽扩展从256bit到512bit是全局改动这次移植最核心的工作是把原有25G方案的256bit数据通路扩到512bit。很多人以为就是信号位宽改一下实际上牵扯到一整条链路的处理逻辑。原工程里AXI-Stream的tdata是256bit对应32字节100G下变成512bit对应64字节。tkeep从32位变成64位这个还好关键是每个处理引擎都要重新审视包头解析原来一拍解析一个32字节字现在要一拍解析64字节而UDP头加IP头加MAC头一共42字节如果包从一个64字节的边界开始42字节的头部可能跨两个周期才能解析完。原工程里包头解析是单周期的扩位宽以后必须改成状态机分两拍处理否则组合逻辑会爆炸。校验和逻辑原来每周期累加32字节现在64字节的累加链长度翻倍直接怼上去跑322MHz时序必挂。我的做法是把64字节拆成两组32字节并行累加下一拍再合并这样关键路径被切断了。FIFO宽度AXI-Stream FIFO的位宽从256提到512注意FIFO的读写时钟域CMAC侧和用户逻辑侧如果跨时钟一定要用异步FIFO。包长计算与TLAST以太网帧长度不是64字节的整数倍所以最后一拍的有效字节数完全由tkeep决定。100G下一拍64字节尾部tkeep的取值比256bit时复杂得多这个最容易出问题出问题的表象就是带宽看着正常但收端全是坏包。顺便说一句AXI-Stream的TLAST位置表示包结束CMAC对TLAST有严格时序要求TLAST必须和最后一拍数据同时有效不能早一拍也不能晚一拍。我当时就是TLAST晚了一拍导致CMAC把两个包当成一个坏包处理接收端疯狂报FCS错误。3.2 时钟、复位和跨时钟域设计100G CMAC的参考时钟频率和25G不一样。25G方案里我通常用的是156.25MHz参考时钟100G CMAC则需要一个322.265625MHz或156.25MHz的参考钟看配置用户侧逻辑时钟则可以直接用CMAC输出的user_clk避免自己折腾MMCM/PLL。复位时序是这次移植里另一个让我印象深刻的点。CMAC有好几级复位信号GT复位、MAC复位、用户逻辑复位它们之间有严格的先后顺序而且每个复位信号释放后需要等待对应的status信号拉高。比如GT复位释放后要等gtpowergood稳定再释放MAC复位等tx_reset_done和rx_reset_done都拉起来了用户逻辑才能开始收发数据。如果复位顺序不对现象很迷惑link状态看着正常GT的rxready也拉高了但收发就是不通统计寄存器全是零。我当时在顶层加了一个简单的复位状态机按顺序自动释放各级复位并且把每个status信号引到调试寄存器里这样上板后通过JTAG或VIO就能看到整个复位链路卡在哪一步。这个调试手段强烈建议保留排查100G链路问题的时候能节省大量时间。3.3 ARP应答、ICMP回显和UDP校验和的完整性检查大部分开源的UDP协议栈代码都自带ARP应答和ICMP回显模块但移植到100G平台后要重点检查这几个东西ARP应答模块必须常驻工作。PC和FPGA直连时PC要发ARP请求才能知道FPGA的MAC地址。如果ARP应答逻辑在移植过程中被简化掉或者MAC地址寄存器配置错误表面现象就是“刚配置好能通过几分钟就断了然后又莫名通一下”——这是PC端ARP缓存过期后重新请求但FPGA应答不可靠造成的。ICMP回显模块虽然业务上用不到但强烈建议保留。上板调试的第一件事就是ping如果ping不通你是没法判断是物理链路问题、IP配置问题还是协议栈收发逻辑问题的。ping通了至少证明MAC层、IP层、ARP层都通了再往下打UDP流才有的放矢。UDP校验和这里我要单独强调。IPv4的UDP校验和理论上可以填0表示“不校验”很多开源代码默认不计算校验和发送时直接填0。但实际联调中很多网卡驱动收到校验和为0的UDP包时会直接丢包尤其是Mellanox这种高性能网卡驱动默认会做checksum验证。所以移植后一定要确认发送路径把UDP校验和计算完整算法是伪头部源IP、目的IP、长度、协议号加UDP头加数据按16bit求和取反。接收路径上我建议把UDP校验和检查做成可配置的测试阶段可以先关闭校验方便定位问题。3.4 工程结构怎么组织移植改动多工程结构一定要清晰。我的UDP协议栈顶层大概分成这几块udp_stack_100g/ ├── top_100g_udp.v // 顶层 ├── cmac_wrapper.v // CMAC IP核例化和复位控制 ├── axis_sync_fifo.v // 用户逻辑与CMAC之间的同步FIFO ├── udp_rx_engine.v // 接收解析MAC/IP/UDP头 ├── udp_tx_engine.v // 发送封装组MAC/IP/UDP头 ├── arp_responder.v // ARP应答 ├── icmp_responder.v // ICMP回显 ├── checksum_calc.v // 64字节并行校验和计算 └── register_block.v // AXI-Lite寄存器配置其中udp_rx_engine和udp_tx_engine是业务逻辑的主战场也是这次从256bit扩到512bit的主要改动对象。接收引擎里包头解析、payload提取、端口匹配要记得拆成流水线级避免单拍组合逻辑太长。发送引擎里包头生成和校验和计算要提前算好存起来等到数据到来时直接拼接不要在数据路径上现算否则时序根本收不拢。4. 上板打流测试拓扑、工具与实测数据4.1 测试拓扑和环境配置上板测试的拓扑不复杂一句话FPGA的QSFP28光口通过光纤直连服务器的100G网卡。这里有几个环境配置细节要提前做好光模块方面我用的100G SR4模块配MPO多模光纤。SR4是短距离多模方案适合实验室桌面对连。如果你的板卡和网卡距离超过几十米还是考虑LR4单模方案。光模块插上以后先确认模块指示灯状态然后通过CMAC的MDIO接口读模块寄存器确认模块在位、温度、电压这些信息正常。服务器网卡配置几个关键点网卡IP我设成192.168.1.1/24FPGA侧IP设成192.168.1.10掩码24位直连场景不需要网关。关闭网卡的流控和卸载功能避免干扰测试数据。用ethtool可以把rx-checksumming、tx-checksumming这些关掉让网卡不要自作主张丢包。MTU建议先设1500测一段时间后再改9000测巨型帧分开验证。FPGA侧通过寄存器把本机MAC、IP配置好确认复位状态机跑到正常态CMAC的tx_reset_done和rx_reset_done都拉高这时候可以先ping一下。提示ping通不代表UDP协议栈没问题但ping不通一定说明链路或协议栈有问题。ping是移植后第一道必过的关卡。4.2 测试工具怎么选iperf3、tcpdump、ethtool服务器端我主要用了三样工具iperf3用-u参数走UDP模式做带宽打流和丢包率测试tcpdump抓包验证包格式对不对比如源IP、目的IP、UDP端口、校验和字段ethtool -S看网卡侧的rx_packets、rx_bytes、rx_dropped统计和FPGA侧统计对账FPGA侧我自己写了一套简单的统计逻辑在寄存器里记录发送包数、发送字节数、接收包数、接收字节数、错误包数。两侧统计数据一对问题就清楚了。4.3 完整的打流流程先测TX方向就是FPGA发UDP包到服务器。FPGA内部发包模块会按配置的包长和目的IP、目的端口循环发包服务器端iperf3跑接收模式iperf3 -s -u -i 1同时开一个tcpdump验证包格式sudo tcpdump -i enp1s0f0 udp -c 100 -XX这个方向主要验证FPGA发送路径上的MAC/IP/UDP头生成是否正确还有包长、tkeep对不对。如果FPGA发出去的包在tcpdump里能看到正确的源IP和UDP端口服务器iperf3能持续收到数据TX方向就通了。再测RX方向服务器发UDP到FPGA。服务器端iperf3作为客户端iperf3 -c 192.168.1.10 -u -b 100G -l 1400 -t 60FPGA侧没有真正的UDP应用层socket所以它只管收包并做端口匹配统计收包数。跑完以后读FPGA寄存器对比服务器端发出的包数和FPGA收到的包数就能算出丢包率。注意iperf3的-b 100G不代表真的能打满100G单核iperf3的性能是有上限的。想压满线速最好用多线程iperf3比如-P 4开4个并发流或者用专门的打流工具。最后测回环把FPGA接收到的UDP包原样转发回发端。这个方向最接近实际业务场景也最容易暴露问题。回环逻辑要注意改写以太网目的MAC和源MAC否则服务器网卡收到目的MAC不是自己的包会直接丢。4.4 实测数据长什么样我当时测了一组不同包长下的数据给大家一个参考。注意实际数据受光模块、网卡驱动、服务器PCIe带宽等因素影响不同环境会有浮动但趋势是一致的。UDP payload大小理论线速PPS实测TX吞吐实测RX吞吐丢包率64字节148.8Mpps146.2Mpps145.8Mpps0.1%256字节45.3Mpps45.1Mpps45.0Mpps0%1024字节12.0Mpps12.0Mpps11.9Mpps0%1472字节8.38Mpps8.37Mpps8.36Mpps0%8972字节1.39Mpps1.39Mpps1.39Mpps0%64字节小包场景能做到146Mpps以上大约是98%的线速这个结果对硬件UDP协议栈来说是合理的。小包丢包主要来自RX方向PCIE中断处理和iperf3软件收包瓶颈FPGA侧自身的收包统计其实是满的。这组数据说明一件事FPGA硬件UDP协议栈在100G档位线速能力是没问题的瓶颈往往在对端服务器的软件栈上。这也是硬件卸载的核心价值——不管对端怎么慢FPGA这边该收的包一个都不会丢。5. 移植和测试中踩过的坑与排查方法5.1 光口link不起来FEC配置不一致移植完成第一次上电最尴尬的事情就是link状态起不来。CMAC的status寄存器里tx_rx_link一直不拉高GT的rxready也不置位。我排查了半天发现问题是服务器网卡默认开了RS-FEC而FPGA侧CMAC配置里我为了省事把FEC关了两边FEC配置不一致物理层协商失败。处理办法很简单把CMAC的RS-FEC打开和网卡对齐link瞬间就起来了。FEC是物理层的编码增益机制虽然会带来一点延迟开销但长距离或光模块信号质量一般时建议开启。实验室短距离直连也可以两边都关闭FEC速率反而更干净。关键是两边必须一致。经验是遇到link起不来先别查逻辑先把两边的FEC配置、光模块类型、网线光纤类型三项对齐再说。这属于物理层的三要素任何一项不一致都白搭。5.2 能link但全是FCS错误包TLAST时序没对齐Link起来以后我兴冲冲地ping结果不通。服务器网卡统计里rx_error全是FCS错误FPGA发送侧统计却显示一切都正常。查了好几个小时最后定位到是TLAST信号时序问题。CMAC要求TLAST和数据最后一拍同拍拉高我的发送引擎因为包头生成逻辑多打了一拍导致TLAST比数据晚了一拍CMAC把当前包的末尾当成了下一包的开始整个包流全乱了。这个问题用仿真很难发现因为仿真里时钟频率低逻辑延迟影响不明显。上板后时序实际跑起来源头就暴露了。解决方法是把发送引擎的状态机重新梳理了一遍确保TLAST和TVALID同步拉高TLAST跟随数据一起输出不打拍。5.3 服务器能收到包但应用层看不到UDP校验和为0被丢弃TX方向测试时还遇到过一个很隐蔽的问题tcpdump能看到FPGA发来的UDP包字段都正确但iperf3应用层就是收不到数据带宽始终为0。后来在Wireshark里仔细看包详情发现UDP校验和字段是0x0000。Mellanox网卡驱动默认会对收上来的UDP包做校验和验证校验和为0的包会被标记为bad直接丢到错误队列里应用层自然收不到。这个坑在开源代码里太常见了很多demo代码图省事不计算校验和在千兆、万兆场景可能没问题但在100G高性能网卡场景一定会被卡。解决方式就是前面说的发送路径要把UDP校验和老老实实算完整。后来我把校验和计算模块跑通之后iperf3立刻就有流量了。5.4 通信几分钟后中断又恢复ARP缓存问题还有一个很典型的网络通信问题FPGA的UDP回环通了以后过几分钟通信就断了然后又自己恢复断断续续。这其实就是ARP缓存过期导致的。PC端ARP表项的生命周期通常是几十秒到几分钟过期后PC会重新发ARP请求。如果FPGA的ARP应答逻辑没有常驻运行或者应答的MAC地址跟实际包里的源MAC不一致PC就重新学不到表项通信自然中断。排查方法也简单断流的时候在服务器上跑arp -d清空缓存再ping如果能立刻恢复基本就是ARP问题。解决方式是确保ARP应答模块和网络收发解耦只要有ARP请求进来就必须回不能因为业务通道忙就不处理。同时检查ARP应答包里的源MAC和UDP数据包的源MAC保持一致否则PC会学错。5.5 512bit322MHz时序收敛的最后一公里最后说时序收敛这是每个做高速FPGA的人都会遇到的老大难。512bit数据通路在322MHz下组合逻辑稍微长一点就时序违规。我做了三件事把时序收下来第一校验和计算改成两级流水64字节拆成两半并行算第一级算出两个32字节的局部校验和第二级合并关键路径从一整个64字节累加缩短到32字节累加加一次合并。第二包头解析不搞单拍大组合逻辑把MAC地址比较、IP地址比较、端口匹配全部打一拍用流水线方式逐级判断牺牲一拍延迟换时序收敛。第三综合选项里把retiming和register balancing打开让工具自动优化寄存器之间的组合逻辑。最后实现时序worse negative slack归零整套逻辑跑稳。这里插一句FPGA上做100G的UDP协议栈绝大多数人觉得是逻辑设计问题实际上到后期全是时序工程问题。代码写得好不好在100G这个速率下用一次综合就能检验出来。6. 移植之后的几点优化建议这次移植测试完成以后我又把整个方案往回看了一遍有几点优化建议留给后来人。如果项目预算允许建议直接用带PCIe DMA的FPGA网卡方案让数据从FPGA收发后直接通过DMA进服务器内存配合Linux驱动使用。这样FPGA侧的寄存器配置、统计读取、数据搬运都走PCIe调试方便很多也更接近生产形态。其次建议把多队列和包过滤功能纳入后续规划。100G UDP业务一旦跑起来通常不是简单的单端口单队列而是要支持多端口匹配、按四元组过滤、多通道分发。在现在的协议栈框架上这些功能都是可以逐步加进去的架构上留好扩展位就行。还有一个细节是FEC的开关策略。如果你的光模块和光纤链路比较长或者信号完整性一般建议正式部署时打开RS-FEC。虽然FEC有额外的编码延迟但换来的误码率余量非常值得。如果是机柜内短距离直连关闭FEC可以减少延迟。100G FPGA UDP移植这件事做的时候感觉全是坑做完回头看其实链路很清晰选型、准备、适配、测试、排障每一步都有章可循。如果你手头也有类似的移植需求建议按我这篇文章的顺序一步步来先把物理层跑通再做协议栈适配最后才谈上板打流。只要链路本身是健康的剩下的问题都能通过寄存器统计和抓包一个个定位解决。