开源100G FPGA UDP协议栈移植上板测试与性能优化 前一阵子项目评估要在100G口上做UDP流式收发我第一反应是直接上商业UDP协议栈IP结果报价回来之后被领导留了一句“能不能先找个开源方案预研一下”。预研期给了一个月最后索性把社区里比较活跃的verilog-ethernet开源项目拉下来从UDP栈移植、CMAC对接、上板跑通到测出接近88Gbps的净吞吐整条链路都走了一遍。这篇文章就是这次“开源100G FPGA UDP移植上板测试”的完整复盘写给准备在100G FPGA上做UDP offload、又不想一上来就掏预算买商业IP的朋友。这个帖子不会教你怎么从零写UDP协议栈重点是讲清楚三件事为什么敢用开源方案、移植过程中哪些地方最容易翻车、上板之后怎么把测试数据解释得明明白白。内容都以我这次实际踩过的坑为准有些细节不同平台会不一样但排查思路是通用的。1. 移植选型免费开源UDP栈为什么比商业IP更值得赌一把1.1 100G UDP的逻辑卸载软件核根本扛不住先说为什么需要FPGA来干这个活。100G以太网口上的线速是100Gbps对应到最小以太网帧来看每个64字节帧的到达间隔是6.4纳秒左右。如果依赖CPU软核或者外部处理器在中断里处理每一个UDP包光中断开销就足以把整条链路拖垮。这也是为什么大家一提到100G UDP就往FPGA的协议栈卸载上想要么用自己的状态机在数据通路上直接处理UDP头要么把UDP、ARP、ICMP全部做成硬件逻辑让CPU只负责配置和统计。很多从单片机、STM32、嵌入式Linux转过来的朋友会习惯性地想“用lwIP、FreeRTOSTCP栈来跑”这个思路在千兆、哪怕是万兆的低负载场景都还能凑合但在100G口上完全没有操作空间。数据通路必须硬件化软件只做控制面。这也是我在这个项目里没有采用任何软核方案的根本原因。1.2 开源方案里绕不开的verilog-ethernet100G这个量级做UDP offload的可选开源项目并不多真正成熟、有社区维护、能在真实板卡上跑通的基本就是Alex Forencich维护的verilog-ethernet。它不是一个单独的UDP模块而是一整套以太网相关IP的集合MAC层适配、AXI-Stream FIFO、ARP缓存、ICMP应答、UDP收发、校验和计算都齐了而且代码风格很干净模块边界清楚方便按需裁剪。它还有一个非常关键的优势对100G链路不是简单“能支持”而是专门做了cmac_pkt这类适配模块可以直接对接Xilinx UltraScale的CMAC硬核。CMAC把100G物理层、PCS、MAC层的活都干完了对外给出AXI-Stream接口开源栈只需要在用户侧把UDP逻辑挂上去。等于物理层、数据链路层的脏活累活硬核都兜住了开源代码只需要专注在协议层移植风险一下子小了很多。相比之下如果目标平台没有CMAC这种100G硬核要用高速收发器加软核PCS做100G那工程量会膨胀好几倍。所以选择开源方案之前先确认自己的FPGA上有没有可用的100G MAC硬核这基本决定了移植的难度等级。1.3 License和社区风险写进评估结论里的三条底线用开源IP做产品或者预研最怕的不是代码本身有bug而是License边界不清楚、社区没人维护、出了问题没人管。我在选型阶段给领导汇报时只写了三条底线项目License是MIT商用无额外风险可以放心集成社区活跃度尚可最近一年内还有commitissue也有人回复如果后续出现严重bug最坏情况下我们有能力自己改源码代码量在可控范围内。这三点想清楚之后整个决策就变成了一个工程问题而不是一个“能不能用开源”的哲学问题。尤其是MIT License这一点对商业项目来说比GPL友好太多这也是我敢把它放进预研结论里的前提。2. 动手前先算清三笔账时钟、位宽、协议语义2.1 512bit用户接口的195.3MHz时钟是怎么来的100G CMAC硬核对外的AXI-Stream接口位宽是可以配置的常见的有512bit、256bit、128bit。位宽越低接口时钟越高。我这次在Vivado里生成CMAC IP时选了512bit数据通路对应的用户接口时钟就是100Gbps除以512bit大约195.3MHz。这个数字很关键后面的FIFO深度计算、跨时钟域设计、时序约束都以它为基准。有人会问能不能用256bit接口把时钟提到390.6MHz可以但FPGA逻辑在接近400MHz下跑一个完整的UDP协议栈时序收敛难度会明显上升。512bit接口配195.3MHz对多数UltraScale器件来说留了比较充裕的时序裕量尤其是还要塞ARP缓存、ICMP处理逻辑的时候这个选择会稳很多。另外要注意CMAC硬核的GT参考时钟通常由板卡提供常见的是156.25MHz。这个时钟和用户接口时钟不是一回事CMAC内部会自己做PLL和时钟管理。上板测试时如果链路起不来先查GT参考时钟是否稳定、频率对不对这是老生常谈但确实最容易出低级错误的地方。2.2 UDP头、checksum和MTU协议语义别想当然从纯逻辑角度看一个UDP包在以太网帧里的布局是以太网头14字节目标MAC 6、源MAC 6、以太网类型2IP头20字节UDP头8字节然后是payload。以太网类型字段对于IPv4是0x0800IP头里的协议字段对于UDP是17。这些语义看起来简单但移植协议栈时任何一位错了都会导致对端收不到包。更隐蔽的是校验和。CMAC硬核只处理以太网FCS不管IP头和UDP头的校验和。也就是说如果开源代码不帮你算校验和发出去的包对端网卡大概率直接丢。verilog-ethernet里对IP、UDP、ICMP都有校验和计算逻辑也支持配置成硬件计算或者由上层软件填充。我在移植时把校验和硬件计算打开省去软件参与数据通路全程硬件处理。还有一个很容易被忽略的点MTU。100G网卡通常支持最大9KB的jumbo frame但如果对端PC网卡没开jumbo frame默认MTU还是1500字节那FPGA发出超过1472字节payload的UDP包对端很可能会直接丢弃或者分片重装失败。这个问题的坑在于物理层、MAC层一切正常ARP也通就是大包不通。排查到最后才发现是PC网卡MTU设置的问题。2.3 ARP和ICMP协议栈活着的前提很多人在做UDP卸载时只盯着UDP收发觉得ARP、ICMP这些“辅助协议”可以先不做。但实际上对端主机要往FPGA发UDP包首先得知道FPGA的MAC地址这就要靠ARP。如果FPGA不做ARP应答PC端的ARP缓存里永远没有这个IP对应的MAC数据包根本发不出来。同样的PC端在测试时经常会用ping来确认链路是否通如果FPGA不处理ICMP echo requestping就会超时这会让人误以为整个链路有问题。虽然ICMP不是UDP数据通路的一部分但它是最常用的“探路工具”。我在移植时保留了verilog-ethernet里的ICMP模块上板之后第一步就是ping FPGA的IP通了再测UDP能省很多排查时间。这里要特别提醒从单片机lwIP转过来的朋友lwIP里的ARP、ICMP是软件协议栈的一部分你不太能感知到它们的价值但到了FPGA硬件卸载场景这些模块都是RTL逻辑需要你主动实例化并预留接口忘了任何一个都会在联调时卡壳。3. 把开源代码搬进Vivado工程一次完整的移植落地3.1 先梳理目录别急着全量添加verilog-ethernet项目拉下来之后里面有很多模块。我踩的第一个坑就是图省事把rtl目录下所有.v文件一股脑全加到Vivado工程里结果综合报出一堆未使用信号和端口不匹配的warning。虽然不影响功能但整个工程显得很脏后面对面看代码的时候特别费劲。正确的做法是先打开example design或者看顶层模块里例化了哪些子模块只添加工程真正需要的文件。我这次实际用到的模块包括UDP协议栈主体、ARP处理、ICMP处理、收发FIFO、以及CMAC适配模块。其他像GMII、XGMII、PCIe相关的文件一概不添加。这样综合时间更快代码可读性也高很多。3.2 生成CMAC IP核并打开TUSER通路在Vivado里生成100G CMAC IP需要注意几个选项。首先是线速率确认是100GBASE-R还是100GBASE-KR板卡上如果是QSFP28光口通常是100GBASE-R。其次是用户接口位宽这里选512bit和前面算的时钟对应。还有就是TUSER信号CMAC的AXI-Stream接口上会带TUSER来指示错误状态、帧起始等信息但这个信号如果不留意很容易在连接时被忽略。我这里建议在生成IP时把TUSER显式打开并且在顶层把它接出来即便当前不开源栈不用它后面调试时也能够通过ILA观察TUSER来判断CMAC是否报错。很多移植后“收发不通”的案例最后都是靠观察TUSER定位到物理层或者PCS阶段就出错了的。3.3 顶层实例化的信号映射与参数核对接下来是把这个开源UDP栈的例化和CMAC的AXI-Stream接口对接起来。开源代码顶层对外一般是标准的AXI-Stream收发接口方向和CMAC正好对应CMAC接收方向出来的AXI-Stream是rx_axisUDP栈的输入就对到这个接口CMAC发送方向的tx_axis来自UDP栈的输出。这里的信号映射不难真正难的是理解握手信号。AXI-Stream的tvalid/tready是核心tvalid拉高表示数据有效tready由对端拉高表示可以接收只有两者同时为高才算一拍有效数据。移植后最容易出现的错误是ready/valid时序处理不对导致死锁或者丢拍。比如CMAC在复位期间tready是拉低的如果UDP栈不等tready就继续发送数据就会丢。我在顶层例化时核对了几个关键参数数据位宽是否一致、FIFO深度是否足够、校验和计算是否开启、MAC地址和IP地址的初始值是否配置正确。这些参数分散在开源代码的generic/parameter里没有统一文档只能逐个打开源码核对。建议第一次移植时把每个参数的含义注释到自己的顶层文件里后续维护会轻松很多。3.4 约束文件里的三个注意点约束文件在移植中容易被忽略但它对上板成功率影响很大。我这次在XDC里主要做了三件事第一把195.3MHz的用户逻辑时钟在约束里显式创建并和CMAC输出的usr_clk绑定第二给QSFP28相关引脚加上合适的电平标准和差分约束第三给所有异步FIFO的跨时钟路径设置set_clock_groups避免时序分析器在两个异步时钟域之间产生虚假的违例报告。第三个点尤其重要。如果不在约束里声明时钟组是异步的Vivado会默认它们有关联然后在两个时钟域之间报出大量时序违例。这些违例很多是虚假的因为数据经过了异步FIFO或同步器处理但如果你不管它时序报告中会出现一堆红色真正的问题反而被淹没。4. 上板之前最容易翻车的三个平台相关点4.1 复位顺序GT、CMAC、用户逻辑的先后关系100G CMAC的复位不是一个简单的全局复位就能解决的。CMAC内部有完整的复位状态机GT的复位、PCS对齐、MAC初始化都有先后顺序。如果用同一个全局复位信号同时复位CMAC和用户逻辑很容易出现CMAC还在初始化时用户逻辑就开始发数据结果所有包全部丢失而且这种问题非常难查。我这次的做法是CMAC IP的复位由它自己的管理接口控制等CMAC输出指示信号如tx/rx ready拉高之后再释放用户逻辑的复位。也就是说用户逻辑的复位信号实际上是CMAC ready信号的延迟释放版本。这样能保证在UDP栈开始工作之前底层物理链路已经稳定。从硬件工程师的角度说这相当于一个复位链上电复位 → GT初始化 → CMAC对齐 → 用户逻辑运行。任何一步提前都会导致链路通了但数据收不到。4.2 跨时钟域处理别把FIFO当成万能药100G系统里几乎不可避免会遇到跨时钟域问题最典型的是用户逻辑工作在某个时钟域而CMAC用户接口工作在另一个时钟域。我这次用户逻辑的时钟和CMAC用户接口时钟其实是同一个源但还是通过异步FIFO做了隔离主要是因为后续可能要换平台、换时钟架构留一层缓冲会安全很多。这里要提醒的是异步FIFO虽然能解决数据跨时钟域但你得正确配置FIFO的读写位宽和深度。100G下帧间隔很小如果FIFO深度不够瞬间突发流量就会导致溢出丢包。深度的估算方式是最坏情况下上游能在多长时间内连续发送数据这个时间乘以带宽就是FIFO需要容纳的数据量。我这次给收发各配了32KB深度实测下来突发冲击能扛住。4.3 不同FPGA厂商的移植差异先认清再动手开源代码里很多模块是针对Xilinx平台适配的比如cmac_pkt模块直接对接Xilinx CMAC的接口时序。如果目标平台换成了Intel或者国产FPGA接口时序、复位机制、时钟结构都会有差异不可能直接无缝移植。我的经验是先做一个接口时序对照表把Xilinx CMAC和自家平台高速MAC硬核的每个信号逐项对应凡是语义不一致的地方都单独写一个适配层。适配层不参与协议逻辑只做时钟域、信号极性、背压使能的转换这样后续换平台时只需要改适配层UDP协议栈主体不用动。5. 先过仿真再上板最小回环用例和时序收敛检查5.1 最小回环用例的设计思路上板之前一定要做仿真哪怕只是简单的功能仿真。我这次没有去跑完整的以太网帧仿真而是搭了一个最小回环用例在测试顶层里产生一个固定内容的UDP帧从发送端注入协议栈然后直接把发送端的数据回环到接收端观察接收端能不能完整地恢复出帧内容。这个用例的好处是它不依赖外部PC和物理层能快速验证UDP协议栈本身的收发逻辑、FIFO读写、校验和计算是否正确。我在仿真里发现了一个很典型的bug发送FIFO的写使能信号在满标志出现后还拉高了一个周期导致数据被写入了一个已经“满”的FIFO造成数据顺序错乱。这类问题如果不仿真上板后会非常难定位因为数据在板子上不可见。仿真时建议配合ILA一起设计在板级调试之前就把内部信号引出到几个关键观测点UDP栈的收发tvalid、tready、FIFO的空满标志、以及CMAC的链路状态信号。这些观测点对后续上板测试至关重要。5.2 时序收敛WNS为负时的常见元凶移植开源代码后第一次综合时序违例是大概率事件。最常见的元凶有三个一是逻辑路径太长比如一个组合逻辑链跨了多个模块二是跨时钟域路径没有正确设置异步约束三是复位信号扇出太高导致复位网络的时序特别差。我这次遇到的WNS为负问题最后定位到是复位信号扇出太高。解决办法是给全局复位加了一级寄存器树做扇出或者在约束里把复位网络设置为global clock资源。这里要特别提醒不要一看到时序违例就随手加流水寄存器先看违例路径是跨时钟域还是同频同相如果是跨时钟域的虚假违例加流水寄存器反而会把设计搞乱。6. 上板实测从链路建立到iperf3 UDP打流6.1 光口和端口状态检查上板测试的第一步不是跑带宽而是确认链路物理层是通的。插上QSFP28光模块后用光功率计确认接收光功率在正常范围内然后在FPGA内部观察CMAC的对齐状态和链路up信号。很多“板上测不通”的问题最后都卡在光模块没插紧、光纤RX/TX接反、光功率过低这些物理层面的问题上。这一步如果出了问题后面所有协议层的排查都是徒劳。我自己的习惯是链路up信号没有稳定拉高之前不做任何数据通路测试。6.2 IBERT先把物理层误码控制在1e-12以下正式的误码测试我推荐用Vivado的IBERT工具它不需要你写任何用户逻辑直接在Hardware Manager里生成测试工程然后通过GT收发器的回环模式来测误码率。100G链路对误码率的要求通常要低于1e-12否则上层协议会频繁重传或者丢包。IBERT测试还能顺手看一下眼图如果眼图张度不够或者信号质量差可以先尝试调整TX的预加重参数。这一步做完且通过之后再回到自己的UDP工程把物理层因素排除掉后面排查问题时可以少一半变量。6.3 iperf3 UDP实测与结果解读链路正常后我用的带宽测试工具是iperf3。PC端装的是100G网卡和FPGA通过光模块直连。测试命令大概是这样# PC端作为接收端 iperf3 -s -u # 或者用另一台机器作为发送端 iperf3 -c 192.168.1.10 -u -b 40G -t 30 -l 1400这里有几个值得注意的点-b参数指定目标带宽-l指定UDP payload长度长度不要超过MTU对应的1472字节否则会触发分片。-t是测试时长。100G下如果要测接近线速的吞吐单线程iperf3往往打不满因为PC侧CPU会成为瓶颈这时候需要多开几个iperf3进程或者用DPDK打流工具。我这次实际测下来的结果是单进程iperf3 UDP大约能到35Gbps左右多进程并行打流时能达到88Gbps净吞吐这个时候PC网卡本身开始丢包已经不是FPGA侧的问题了。这个数据也从侧面说明FPGA侧协议栈处理100G线速是能扛住的瓶颈在测试主机侧。对于下面这个表格是我记录的一次典型测试数据可以直接作为参考测试项配置结果单进程UDP打流iperf3 -c 192.168.1.10 -u -b 40G -l 140035Gbps接收无丢包4进程UDP打流4×iperf3各打20Gbps88Gbps接收FPGA无丢包大包吞吐payload 8000字节jumbo frame92Gbps但PC侧MTU需提前设置6.4 Wireshark抓包与UDP统计的细节打流之后我一般会再用Wireshark抓包确认帧内容是否正确。这里有一个常见困惑抓包过滤器明明写了udp为什么还看到ICMP包其实这不一定是抓包规则配置错了很多情况下是UDP端口不可达时PC端会主动产生ICMP Port Unreachable报文作为响应。这类ICMP报文虽然协议号不是UDP但它是UDP协议栈工作的一部分所以Wireshark里会一并显示出来。如果你在测试时看到大量这类ICMP包通常说明PC端的UDP端口没有监听需要检查接收进程是否正常启动。另外Wireshark里“packets received”和“packets to unknown port”这两类统计很值得关注。前者是本机网卡实际收到的UDP包总数后者是那些目标端口没有应用监听的包。我在一次测试中FPGA明明发出了大量UDP包但PC端应用收到的包很少打开Wireshark一看大量包都落在了“packets to unknown port”上原因就是iperf3接收进程挂掉了PC内核收到包之后发现没人监听这个端口直接丢弃。这不是FPGA的问题但如果不看统计很容易误判成传输链路故障。7. 复盘100G吞吐上不去的两个典型病根7.1 FPGA侧FIFO水位打不满导致背压第一次打流时我发现吞吐率只能到60Gbps左右继续上不去而且FPGA侧的发送FIFO经常处于非满但也不空的状态。这个现象很典型说明发送数据通路的吞吐能力不足而不是链路拥塞。排查下来发现协议栈内部的发送调度逻辑存在一个“每帧处理完才发下一帧”的状态机瓶颈。具体来说FIFO的读侧在帧与帧之间的间隔里启动太慢导致链路在帧间隙出现空闲周期。100G链路对帧间隙的要求很严格哪怕每个帧之间多出几十个周期的空闲整体吞吐率也会明显下降。解决办法是优化发送状态机让它在当前帧还没发完时就预取下一帧的帧头做到流水线处理。我通过分析FIFO的水位变化发现FIFO深度足够但读侧的预取逻辑没有提前量加了一段prefetch逻辑之后吞吐率从60Gbps提升到了接近线速。7.2 PC侧UDP缓存与多队列问题反杀带宽另一个让吞吐率上不去的场景是FPGA发出来的数据已经到了链路满速但PC应用收到的速率只有20多Gbps而且CPU占用率已经跑满了一个核。这是典型的单队列中断处理瓶颈。默认情况下Linux或者Windows收到100G网卡的UDP包都把所有中断放到CPU0上处理单核能力跟不上100G的包速率。解决办法有两个方向一是开启网卡的RSS多队列让不同流的UDP包分散到多个CPU核上处理二是调大UDP接收缓冲区减少因缓冲区溢出导致的丢包。我在Windows上测试时还踩过一个大坑系统全局UDP接收缓冲区默认很小即使应用层使用了很大的recv buffer内核也会截断到系统默认值。需要修改注册表或者用管理员权限调整全局UDP缓存大小才能显著改善。这个问题在Linux下相对简单一个sysctl -w net.core.rmem_max...就能解决。经过这两轮优化之后实测结果才算稳定在88Gbps以上。从这个案例也能看出来100G UDP调优是一个全链路的问题FPGA侧只是其中一环PC主机的协议栈、驱动、多队列配置都会成为瓶颈。这次移植上板测试做下来我最大的感受是开源UDP协议栈在100G这个量级上的成熟度其实比很多人想象中要高但它对使用者有一个隐性要求你必须真正理解以太网协议和AXI-Stream时序才能在上板后快速定位问题。否则一次简单的“不转发”就足够让你排查好几天。最后分享一个小经验任何时候怀疑链路层有问题先用IBERT和自环测试把物理层排除掉再回到协议栈任何时候怀疑吞吐率上不去先看CPU占用和中断分布再看FPGA侧FIFO水位。按照这个顺序排查大部分100G UDP问题都能在半个小时内定位到根因。