
前阵子接了个活儿要把一套开源的100G UDP协议栈移植到我们自研的FPGA板卡上然后完成上板验证。这个项目前前后后花了两周最终测试结果还算满意线速能跑到95Gbps以上对端在丢包率上基本没挑出毛病。做这行的人应该都知道100G的UDP听着只是“把10G/25G放大十倍”但真正动手以后才发现MAC层、时钟树、用户接口、上位机测试方法每一层都有完全不一样的坑。这篇文章就把“开源100G FPGA UDP移植上板测试”这件事从头到尾梳理一遍包括怎么选开源工程、怎么改协议栈、上板以后先用什么命令验证链路、再用什么工具打流最后把我踩过的几个坑和排查思路一起放出来希望能给准备上100G的朋友省点时间。这篇文章适合两类人一类是手里有UltraScale级别板卡想跑100G UDP但不知道从哪下手的FPGA工程师另一类是已经在10G/25G上做UDP传输想升级带宽但担心协议栈和时序撑不住的同学。文中不会贴大段源码但数据路径、时钟模块、接口握手、测试命令这些关键细节都会讲到照着这个思路走基本可以少走一半弯路。1. 为什么我要折腾一套100G UDP协议栈1.1 我遇到的场景数据带宽撞到墙了我们做的是高速数据采集和实时传输方向的东西原来板卡上跑的是25G以太网配合自研的MAC用户接口和自己拼的UDP封装逻辑在FPGA内部做数据搬运、组包、发送链路带宽一直挺稳定。但业务侧需要传输的原始数据量越来越大25G的带宽已经明显不够用了尤其是当多路ADC数据同时往上传的时候DDR带宽、PCIE带宽可能还够但以太网出口就成了瓶颈。这时候摆在面前的选择很直接换100G。问题在于100G以太网和25G不是简单的位宽扩展它涉及到64B/66B编码、多Lane绑定、FEC、PCS层状态机更重要的是如果继续沿用原来用手写的MACPCS方案时序收敛会非常痛苦。Vivado里对100G的实现方式主要有两条路一条是用Xilinx的CMAC硬核另一条是用GTY软核PCS来拼。前者省事后者灵活但无论选哪条UDP协议栈从10G/25G搬到100G之后都要重新验证逻辑时序和跨时钟域处理。所以这个项目从一开始就不是简单“换个IP再编译一下”而是要完成一套完整的数据通路改造100G MAC负责物理收发开源UDP协议栈负责把IP/UDP头填充好、计算校验和用户侧再用DDR或FIFO把业务数据喂进来。整个过程我选择基于开源方案来做好处是代码可控、便于按业务定制坏处是调试期会比较长尤其是上板以后如果协议栈内部出现逻辑问题没有现成的技术支持可以问只能靠波形和计数器一点点扒。1.2 为什么选“开源协议栈改造”而不是自研做FPGA的人都懂一个道理能用IP核尽量别手写能被验证过的东西尽量别自己发明。UDP协议栈在10G/25G时代有很多现成方案但100G的开源方案相对少一些市面上的选择大概分成三类我对比如下。方案优点缺点适合场景商用IP含完整UDP卸载引擎验证充分、性能高、有厂商支持授权费贵、参数定制受限、黑盒不好调试时间紧、预算足、不需要深度定制手写UDP协议栈完全可控、无授权风险开发周期长、验证难度大、时序容易崩有长时间研发投入且准备自己做全套验证基于开源工程改造代码开源、可裁剪、社区有大量参考设计仍有调试成本需要自己读源码理解协议细节有一定FPGA经验想快速起步又保留定制能力我最后选了开源工程改造这条路。原因很直接手上板卡的资源比较充裕逻辑单元够用硬核CMAC也可以直接用缺的主要就是UDP协议栈这种“胶水逻辑”。开源工程的好处是把ARP、IP、UDP、checksum这些模块拆得比较干净我可以只保留需要的部分把其他模块裁掉减少资源占用和时序压力。另外开源工程通常自带testbench移植之前先跑仿真能提前发现一堆显而易见的接线错误节省不少上板调试时间。当然开源工程也不是拿来就能用。我调研的时候发现有的开源工程号称支持100G但实际只完成了MAC层UDP层还是基于10G的位宽和时钟写的需要自己做跨时钟域适配有的工程则对Xilinx的CMAC例子依赖太深换一块板卡就要改一堆引脚配置。所以挑开源项目不能只看star数要把代码里的FIFO位宽、用户时钟频率、AXI接口定义全部看一遍判断跟自己的硬件环境和业务需求是否匹配。1.3 移植前先拆解成四步这个项目我没有上来就写代码而是先把任务拆成了四块后面所有节奏都是按这个计划走的好处是每一步都有明确的可交付物不至于在某个模块里陷进去出不来。第一步是硬件和工程环境的确认。确认板卡上有没有QSFP28光口、参考时钟是多少、CMAC核的license是否可用、Vivado版本和IP版本是否兼容。第二步是协议栈模块的整理与仿真把开源工程里需要的UDP、ARP、checksum模块抽出来先在仿真环境里跑通一个最小路径。第三步是工程集成把CMAC核、协议栈、用户侧FIFO和DDR读数据模块接起来通过综合与时序检查。第四步才是真正的“上板测试”包括光口回环、二层连通、三层ping通、UDP打流和性能压测。这四步看起来常规但实际操作时很多人容易把第三步和第四步混在一起一边改代码一边上板Debug最后出了问题连是逻辑错还是环境错都分不清。我这次严格执行了“先仿真、后上板”的流程虽然前期多花了两天但后面上板定位问题的时候基本能确定是自己的业务逻辑问题而不是协议栈底层的毛病。2. 移植前的准备工作用什么板、选哪个开源项目2.1 硬件平台选型100G不是随便一块板卡就能跑的100G以太网对硬件平台的要求比较苛刻不是随便拿一块带SFP的板卡就能改的。我这次用的是UltraScale系列的板卡板载QSFP28光口FPGA内部带100G硬核CMAC。选UltraScale的核心原因就是CMAC它把100G MAC、PCS、甚至部分FEC都集成在硬核里了用户侧直接拿到一个相对干净的AXI4-Stream接口不需要自己花大量资源去跑PCS逻辑时序也更好收敛。除了FPGA主芯片其他外围也直接影响上板调试效率。100G参考时钟一般用156.25MHz必须从板载可编程时钟芯片或独立晶振引入并保证在约束文件里正确锁定光模块这边如果是QSFP28需要注意reset管脚、LPMode管脚和I2C地址上电后要让光模块退出低功耗模式否则插上光纤也收不到光另外还建议预留一个串口或AXI-Lite寄存器口用来在上板时读取协议栈内部计数器、链路状态和温度电压等参数否则遇到问题只能靠外部抓包工具盲猜。如果手头没有带CMAC的板卡也可以用GTY自行搭建PCS逻辑来拼100G但这样的话工作量和调试难度会明显上一个台阶设计时需要考虑GTY多通道绑定、alignment marker插入、FEC开销等问题建议新手优先选择带硬核CMAC的平台。2.2 开源工程怎么挑不能只看star数开源UDP协议栈项目不少但真正能用于100G、且能比较容易移植到自研板卡上的其实并不多。我重点看了两个方向一个是以模块化见长的verilog-ethernet它是把MAC、IP、UDP、ARP、checksum这些模块拆得很细的库支持10G/25G/100G的适配代码风格干净适合拿来自定义裁剪另一个是Corundum它是一套完整的100G开源网卡方案不仅包含UDP协议栈还带PCIe DMA、多队列、中断控制器这些网卡级功能功能非常全但结构也重很多如果只是想把数据包从光口收发用Corundum反而有点“杀鸡用牛刀”。我当时选型的判断标准有三条。第一条是许可证是否能用于商业项目很多开源项目是GPL协议的如果公司产品要闭源交付就会很麻烦尽量选MIT/BSD这类宽松许可证的工程。第二条是协议栈的完整性至少要包含UDP收发、IP层处理、ARP响应、校验和计算这几个核心功能否则还要自己补写一大块。第三条是是否有验证环境和参考设计有testbench的项目能让你在仿真阶段就发现很多问题有example design的项目则能大大降低工程集成的难度。选完项目以后我做的第一件事不是直接集成而是把它的顶层接口和内部时钟域全部列了一遍标出哪些信号跟CMAC用户接口对接、哪些信号跟用户业务逻辑对接、各自处于什么时钟域。这一步虽然枯燥但能避免后面集成时出现“两个模块明明功能都对就是接不在一起”的问题。2.3 工具链和参考设计就位上板测试前的环境准备主要有三块Vivado版本、调试工具、抓包工具。Vivado这边要特别注意IP版本和芯片型号的匹配我用的是Vivado 2021.2CMAC IP和GTY Transceiver IP的版本都是配套的如果用的Vivado版本太老生成的CMAC核用户接口可能会缺少一些状态信号影响调试。调试工具方面ILA集成逻辑分析仪是必须的100G数据位宽很大全量抓波形会浪费大量BRAM和布线资源建议不要直接抓512bit的数据总线而是抓关键状态信号比如tvalid、tready、tlast、rx_block_lock、fifo_overflow这些。网络抓包工具我用的是Wireshark因为它在收到UDP包时能看到IP层和UDP层的完整头部信息对于判断checksum是否正确、MAC地址是否匹配、VLAN标签是否多加了这些细节性问题非常直接。另外建议在正式打流之前先在工程里预留一组计数器分别统计tx_frame、rx_frame、rx_drop、arp_rx、udp_rx等事件。这些计数器不一定要引出到引脚可以通过串口或AXI-Lite寄存器读出来。上板测试时如果对端iperf3报告丢包率高就能立刻从计数器中判断是“包根本没收到”还是“收到了但校验失败”还是“FIFO溢出被丢掉”这是整个调试过程中最省时间的做法。3. 协议栈移植的核心细节3.1 先理清数据路径从GTY到UDP引擎很多第一次做100G UDP的人一上来就盯着UDP模块看我觉得这是本末倒置。移植之前最重要的事情是理清整条数据路径知道自己手里的数据是从哪里来、经过哪些模块、最后从哪里出去。我这次的数据发送路径大概是这样的业务逻辑从DDR读出原始数据写入一个深度较大的用户FIFOFIFO数据在用户时钟域读取送入UDP发送引擎UDP引擎会按配置好的源IP、目的IP、源端口、目的端口、包长等信息封装UDP头和IP头然后往下送到以太网MAC层由CMAC硬核添加前导码、FCS等最终在QSFP28光口发出。接收路径则是反过来的光口进来的数据先进入CMAC完成PCS解码和MAC帧校验通过接口送到ARP/IP/UDP解析模块如果是发给本机的ARP请求协议栈自动应答如果是UDP数据包则剥掉以太网头和IP/UDP头把payload写入接收FIFO最后由用户逻辑读取。这里有一个很关键的细节100G的CMAC用户接口数据位宽通常是512bit用户数据时钟接近200MHz甚至更高而很多开源协议栈最初是基于64bit或256bit数据位宽写的直接对接会非常痛苦。我这次的做法是在CMAC和UDP引擎之间加了一层位宽转换FIFO把512bit的数据流降成用户逻辑容易处理的宽度。位宽转换FIFO在Vivado里是现成IP配置成“packet mode”配合tlast信号在包边界做对齐这样上下端的帧格式就不会错位。数据路径上的每个节点都需要一个“验证点”。比如CMAC侧可以看rx_block_lock和tx_rx_bypassUDP引擎侧可以看tvalid/tready握手是否持续用户FIFO侧可以看fifo_empty和fifo_prog_full是否正常。把这些验证信号全部接到调试计数器上上板后按步骤看哪一级断了马上就能缩小问题范围。3.2 时钟和复位最容易翻车的部分100G UDP移植过程中时钟和复位处理是翻车率最高的地方比协议本身的逻辑错误还常见。100G系统里通常会涉及好几个时钟域CMAC用户时钟、UDP引擎工作时钟、用户业务时钟、DDR时钟、以及用于GMII/MII管理的慢速时钟。这些时钟之间频率不一定相同相位更不可能一致所以跨时钟域处理必须老老实实走异步FIFO或同步打拍。我当时的方案是给协议栈内部统一用一个由CMAC用户时钟分频得到的逻辑时钟不另外做独立时钟源。这样做的原因是UDP引擎和MAC层始终处于同一个时钟域不需要在中间插异步FIFO逻辑简单很多。但代价是用户逻辑从DDR读出的数据必须先经过一个异步FIFO进入这个时钟域如果业务侧数据量很大这个FIFO的深度要仔细计算最好按“一次需要缓存多少个整包”来算。我记得测试时遇到过一次FIFO溢出就是因为深度只够缓存4个最大包但软件侧一次突发来了8个包直接丢了一半后来把深度加到了16个包才稳定。复位方面100G工程最忌讳“全局复位信号一直不释放”。CMAC本身有一套严格的复位序列比如需要先等GTY的PLL锁定再释放PCS复位最后释放MAC复位。如果直接用板卡上电按钮拉一个全局复位信号不去管这些时序要求CMAC很可能一直处于复位状态现象就是rx_block_lock永远拉不起来、接口对端根本ping不通。我建议严格按照CMAC example design里的复位状态机来管理复位信号不要自己随便改顺序。3.3 用户侧接口怎么接到业务逻辑用户侧接口是“UDP引擎”和“业务数据”之间的桥梁。这个接口在两边的定义通常是AXI4-Stream核心握手信号就是tvalid/tready/tlast/tkeep。刚接触AXI4-Stream的人容易忽略tready的反压逻辑当发送端FIFO空了tvalid必须拉低当接收端FIFO满了tready必须拉低而tlast表示这一包的结束必须在包尾精确拉一拍高电平多拉或者少拉都会导致对端把两包数据合并成一包或把一包拆成两包。业务逻辑和UDP引擎对接时我一共做了三件事。第一件是再加一个用户侧发送缓冲FIFO这个FIFO放在UDP引擎之前专门吸收DDR读数据的节奏波动避免DMA或DDR仲裁的延迟导致UDP发送引擎长时间空等。第二件是把“包长度”信号同步到发送引擎UDP头和IP头的长度字段必须与payload实际长度一致如果长度不一致对端网卡可能直接丢弃表现就是“Wireshark抓到了包但应用层Recvfrom收不到”。第三件是规划好大包还是小包100G链路上如果发64字节小包包速率高达每秒1.48亿包任何FIFO读写频率和状态位判断都可能在极限下出问题测试时尽量先用1024字节或更大的包验证功能再回头压小包性能。接收侧处理相对简单一些协议栈剥完头部后把payload连续写入接收FIFO就行。但要注意如果接收FIFO采用了“packet mode”读侧在读完一包数据后需要拉一个tlast或发出相应的包结束指示这样用户逻辑才能知道一帧数据结束进而把这一包交给DDR或逐包处理。4. 上板测试从光口打亮到UDP线速压测4.1 链路层验证先让光口和CMAC都“听话”上板测试的第一步不是打流而是先把物理链路和MAC链路调通。我习惯把这个阶段叫“让光口和CMAC都听话”。具体操作是先看CMAC状态寄存器确认GTY eye scan和link status之类的参数正常重点是看rx_block_lock是否有拉高。如果有条件直接把QSFP28的TX和RX用一根短跳纤连起来做外部回环这样不需要借助交换机或者服务器就能验证CMAC的收发链路是否完整。如果rx_block_lock一直不锁定优先查三个地方一是光模块是不是还处于低功耗模式QSFP28的LPMode管脚如果被拉高模块不会进入正常工作状态二是参考时钟是否稳定用频谱仪或示波器量156.25MHz时钟有没有偏三是CMAC的复位序列是否正确特别是GTY的PLL锁定以后至少要等一段时间再释放PCS复位否则CDR无法锁定光信号。链路层验证通过以后我还会用内部回环模式再把CMAC的发送通路单独测一遍。这个回环是在CMAC内部把TX数据直接送回RX通路不经过光模块这样就可以隔离光模块或光纤问题让验证范围更清晰。实际测试时先用内部回环确认协议栈逻辑没问题再用外部光纤回环确认光模块链路没问题两层都通过后再去对接交换机或服务器。4.2 二层通了再谈三层ARP和ICMP链路层稳定之后就要验证协议栈的二三层功能。这一步我的做法是把FPGA板卡的以太网口连接到一台普通千兆/万兆交换机上或者直连服务器网卡然后在服务器上先设置一个同网段IP比如FPGA固定为192.168.10.10服务器设置为192.168.10.20用ping命令做连通性测试。但要注意第一步ping的时候如果协议栈里的ARP模块没写好或者MAC地址配置错误服务器会一直报“Destination Host Unreachable”。此时可以在服务器上用arp -d清空ARP缓存再执行ping同时在FPGA侧用计数器观察是否收到ARP请求、是否发出ARP应答。一般来说只要ARP应答发出去了服务器就会把FPGA的MAC地址学习到ARP表里后面再ping就通。ICMP回显功能在整个UDP通信链路里其实不是必须的但我强烈建议在协议栈里保留它哪怕只是简单回一个ICMP Echo Reply。原因很简单ping通了说明IP层收发、MAC层收发、链路速率协商这些都是正常的ping不通却想直接调UDP层那无异于在“地基还没打牢”的情况下去装修二楼。我这次调试UDP之前光是ping测试就跑了十几轮在不同的包长64B、512B、1024B、1472B下均能通才确定协议栈的二三层是可信的。4.3 用iperf3和自研工具打流三层通了以后就可以开始真正的UDP性能测试了。测试环境有两种选择一种是FPGA和服务器直连服务器上用iperf3的UDP模式接收另一种是两个FPGA板卡对打避免服务器CPU成为瓶颈。我这次先用了服务器方案因为调试方便可以通过命令行实时看吞吐和丢包等到需要极限压测时再改用两块FPGA对打。iperf3命令在100G场景下有两个容易踩的坑。第一个是默认UDP带宽只有1Mbps必须手动指定带宽比如iperf3 -c 192.168.10.20 -u -b 80G -l 1024 -t 30这里的80G表示期望的发送带宽1024表示UDP payload长度30表示测试时长第二个是单线程iperf3进程很难跑满100G因为CPU会成为瓶颈尤其是在处理小包时所以建议加-P参数开启多流例如-P 8让多个iperf3流并发发送才能把带宽压上去。不过有一点要提醒iperf3本身在100G下也有局限。如果服务器网卡驱动或中断亲和性没调好即使FPGA侧发了满带宽的包iperf3接收端也会因为CPU占用过高导致丢包率飙升。这时候不能急着怀疑FPGA协议栈有问题而要先排查服务器网卡的多队列、RSS、中断绑定和接收缓冲区大小。我这次测试时用ethtool -G把网卡接收环形缓冲调到了最大并把中断绑到了多个CPU核心上UDP丢包率才明显降下来。打流过程中建议同时看几个数据iperf3输出的带宽、抖动、丢包率服务器上netstat -su看到的UDP接收情况FPGA侧的计数器值包括发出去的包总数、收到的包总数、校验失败包数等。只有把这些数据放在一起对比才能确认丢包到底发生在哪个环节。5. 测试数据怎么看坑怎么填5.1 三个实际踩坑记录这次上板测试踩的坑不算少我挑三个最典型的记录一下每一个都折腾了不少时间。第一个坑是CMAC的rx_block_lock始终不锁定。当时现象是光纤插好光模块也亮灯了但CMAC状态寄存器里rx_block_lock一直是0服务器端也看不到任何链路。排查过程花了一天多后来发现是QSFP28的LPMode管脚被误拉高了导致光模块一直处于低功耗状态虽然指示灯有亮但实际上CDR没有正常工作。把LPMode拉低以后rx_block_lock才正常拉起来。这个坑提醒我上板调试前先看板卡原理图把所有控制管脚的电平状态先核对一遍。第二个坑是服务器能抓到UDP包但应用层收不到iperf3的Rate和Lost全是0。用Wireshark抓包能看到FPGA发出的UDP报文说明链路和IP层都正常但应用层没反应。后来定位到是UDP头里的校验和字段为空且checksum offload配置有问题服务器网卡在开启RSS offload的情况下收到校验和不对的UDP包后会在驱动层直接丢弃不交给应用层。解决办法是在协议栈里把checksum计算正确或者在测试时临时关闭网卡的RX校验卸载功能。第三个坑是流量一大就丢包而且丢包率跟发送速率强相关。100G链路上如果FIFO深度不够发送端只能以很低的平均速率发出去但只要业务突发稍微大一点后面的包就会被丢掉。这个问题的根因就是用户侧FIFO深度设计太小后来我通过增加FIFO深度、并让DDR读侧支持“当FIFO水位低于阈值时立刻继续读取下一段数据”来解决丢包率从0.5%降到了接近0。5.2 测试结果记录与分析打流测试最终汇总出来的结果大致是这样的。发包方式包长目标速率实测带宽丢包率抖动服务器多流iperf3收1024B80G79.8G0.01%约10us服务器多流iperf3收1024B95G94.6G0.05%约15us双FPGA对打1024B100G99.8G0.001%极低双FPGA对打512B100G97.8G0.01%极低从结果可以看出来100G线速在包长大于等于512B时基本能跑满小包性能主要受包速率和时序限制。这里“线速”的意思不是说每次都能达到100.0G而是指在以太网帧间隙、前导码这些固定开销都算进去以后有效UDP payload速率能接近物理层极限。如果对这一切没有概念单看iperf3里显示的Gbps可能会误以为还有优化空间实际上已经到瓶颈了。测试结果还要结合FPGA内部计数器一起看。假如iperf3显示带宽90G但FPGA里tx_frame计数器显示发出的包数比预期少很多那就说明是业务侧/发送FIFO的饥饿问题而不是协议栈收包慢。反过来如果tx_frame正常但服务器netstat -su显示的packets received明显少于发出去的包那就要去查链路层或服务器驱动而不是继续在FPGA逻辑里找原因。5.3 再往后可以做什么这套100G UDP协议栈跑通以后我给自己的下一步计划是继续做几件增强工作。第一件是增加多队列支持把不同UDP端口或不同目的IP的数据流分发到多个业务处理通道这样可以进一步提升系统的并行处理能力也为以后扩展成网卡级方案打基础。第二件是加入TCP的硬件卸载TCP里的序列号管理、重传、拥塞控制都要比UDP复杂得多如果业务需要可靠传输就得在FPGA里做一套完整的TCP offload引擎这个工程量显然比UDP大不少。第三件是彻底接入DMA通道把UDP收发直接和PCIE DMA打通做成一张真正的100G智能网卡。开源项目Corundum其实就是这个方向上的现成参考但它结构比较复杂需要花时间消化它的队列管理和描述符机制。如果只是做点对点传输现在的这套“CMACUDP引擎用户FIFO”已经够用但要想跟服务器生态无缝整合DMA和中断路径还是绕不开的。最后说点真实的体会吧。100G UDP这个事看起来门槛很高实际上拆开就是一个带100G硬核MAC的FPGA一套模块化的开源协议栈再加一颗愿意看时序约束报告的心。整个项目里最让我花时间的不是UDP协议本身而是调试环境搭建和问题定位思路。如果让我重新来一次我会先花一天时间把时钟规划画清楚再用计数器把每级数据流“点亮”一遍然后再碰UDP打流这个顺序能省下至少一半的Debug时间。