
做任何高速接口项目第一步永远不是写代码而是先把数据通路画出来。100G UDP这个项目我去年在一个数据采集卡上完整跑过一遍从拿到开源代码到上板打流通过前后大概折腾了三周半。这篇就把移植和上板测试过程中最关键的环节、最容易踩的坑以及最终实测的数据整理出来给正准备做类似项目的朋友一个参考。1. 整体设计与方案选型为什么用开源UDP而不是商业IP1.1 项目目标和应用场景这个项目的本质是在FPGA上实现一个单端口100Gbps的UDP数据传输链路。听起来就是把协议栈跑起来但实际牵涉到的东西远不止UDP那几个字段。100G UDP在真实项目里的用途非常明确高速数据采集、网络流量回放、数据中心内部节点间的大块数据搬移。因为UDP无连接、头部开销小做纯数据搬运时比TCP友好得多尤其适合FPGA这种擅长流式处理、不适合做复杂状态机的硬件。我当时的场景是前端ADC持续产生高速数据流经过FPGA打包成UDP报文从QSFP28光口发出去。接收端是服务器上的100G网卡。数据的实时性要求高允许少量丢包但不能出现链路中断时延要低。1.2 开源协议栈的选型对比市面上能做100G UDP的FPGA开源方案主流就那几个。我用过的有Alex Forencich的verilog-ethernet、Corundum还有OpenCores上的一些老古董。最终选了verilog-ethernet原因很直接它对AXI-Stream接口的封装非常干净MAC、IP、UDP、ARP各层模块解耦代码风格清晰而且作者长期维护对Xilinx和Intel两家器件都有适配。Corundum的优势在于它是一套完整的高性能网卡方案包含了PCIe DMA、多队列、卸载引擎如果你要做的是一个真正的100G智能网卡Corundum是更好的起点。但如果只是需要一个轻量级的UDP数据通道Corundum的复杂度反而会成为移植负担。老古董那些OpenCores项目大多数停在10G/25G级别100G的支持基本是残缺的。1.3 为什么“移植”这件事不是复制粘贴很多第一次做的人会以为开源代码拿来就能跑实际上完全不是这么回事。verilog-ethernet的demo工程是针对特定板卡和特定FPGA型号的你拿到的是一堆通用RTL要做到自己的板子上至少需要改这几类东西时钟方案100G以太网的线速率是固定的但用户侧逻辑时钟可以根据位宽设计选择不同频率SerDes收发器配置Xilinx的GTY/GTH、Intel的E-tile底层都不一样复位与初始化时序每个平台的复位要求不同尤其在高速收发器上与厂商IP的对接比如Xilinx CMAC硬核100G MAC的使用方式所以这个项目的核心工作一半在理解协议栈逻辑一半在硬件适配。2. 时钟、复位与位宽转换移植中最容易翻车的三个基础点2.1 100G MAC的时钟域设计100G以太网在FPGA内部的数据位宽和时钟频率是有讲究的。标准100GbE的PCS层在64B/66B编码之后线路速率是103.125Gbps。如果MAC层对外数据接口采用1024bit位宽那么用户侧时钟就是103.125G / 66 × 64 / 1024约等于97.7MHz这个频率太低了逻辑好跑但很多外部接口模块比如PCIe DMA、高速ADC接口不喜欢这么低的频率。实际工程中常见的做法是用512bit 195.3MHz或者322.265625MHz 320bit。verilog-ethernet里的eth_mac_100g模块数据接口就是独立的AXI-Stream位宽可参数化。官方demo用的是1024bit位宽配大概97.7MHz或者156.25MHz的时钟取决于是否启用FEC。我在移植时为了和DMA模块的时钟域对齐用了512bit 195.3MHz。这里有个容易绕进去的点PCS层的时钟和MAC用户侧时钟不是一个东西。CMAC硬核内部有自己的时钟方案你外部需要做的是在CMAC的用户侧接口和你自己的逻辑之间保证时序收敛。如果硬核支持内部时钟分频可以省不少事。2.2 复位时序的坑这是移植过程中让人掉头发最多的环节。Xilinx的CMAC参考设计里复位信号的处理非常讲究GTY收发器需要先完成复位然后PCS的RX侧还需要等待复位完成信号最后才能释放MAC层复位。如果你一股脑把所有模块的复位信号接到同一个全局复位上大概率出现的问题是——链路能起来但偶尔不工作或者收发时钟恢复不稳定。我当时的做法是按照官方example design里的复位时序做一个简单的复位状态机上电后先复位GTY的TX和RX等待txresetdone和rxresetdone拉高然后释放CMAC的复位再等待cmac的rx_axis_reset_done信号。用户侧逻辑的复位放在最后。2.3 AXI-Stream位宽变换和tkeep处理开源协议栈的UDP接口通常有一个参数化的数据宽度但用户业务逻辑的数据粒度不一定匹配。比如我这边ADC数据是256bit位宽而UDP发送模块是512bit中间必须加异步FIFO做位宽转换和时钟域转换。这里有个细节AXI-Stream的tkeep信号在处理非整帧对齐的数据时特别容易出错。以太网帧最小64字节最大1518字节巨型帧可以到9K你打包数据时最后一个beat的tkeep必须精确对应有效字节数否则对端网卡会丢弃报文。我在调试时遇到过一种情况帧能发出去Wireshark也能抓到但接收端的socket收不到排查了半天最后发现是tkeep在最后一拍没有正确拉低导致对端校验字节长度时出错。注意AXI-Stream的tlast信号必须与最后一拍数据对齐而tkeep在最后一拍必须表示出真实有效的字节数。很多开源代码在数据宽度是8的整数倍时会默认tkeep全1如果你的业务数据不是正好填满最后一拍这个默认值就要改。3. 开源UDP协议栈的修改与适配过程3.1 协议栈模块结构梳理在使用verilog-ethernet之前我先把它的目录结构完整走查了一遍。这个项目核心模块分成几层数据链路层eth_mac_10g/25g/40g/100g负责MAC帧的收发、FCS校验、 preamble处理网络层ip_arbiter、ip_in、ip_out、ip_arp负责IP包的路由和ARP缓存更新传输层udp_64、udp_1024、udp_axis_tx/rx负责UDP头部解析和校验和计算公共模块axis_fifo、axis_async_fifo、lfsr等移植时不需要从头读每一个模块的每一行代码但一定要把数据流方向搞清楚。我的顺序是先从udp_axis_tx和udp_axis_rx看起理解用户数据如何被封成UDP/IP帧再回头去看MAC侧的信号接口。3.2 修改MAC与PCS对接层verilog-ethernet自带的MAC模块是纯RTL的但如果你的FPGA有硬核MACUltraScale的CMAC直接用硬核是更稳的选择。硬核的资源占用低、时序好、支持FEC和链路训练。问题在于硬核的接口时序和开源MAC模块不一样你需要写一个桥接层把开源代码里的MAC接口信号映射到CMAC的AXI-Stream接口上。这个桥接层看起来只是信号重命名实际上有两个难点一是CMAC的axis接口有严格的复位时序要求二是CMAC的tuser信号中包含错误标记接收方向要处理error、bad_fcs等状态而开源代码在某些版本里忽略了tuser导致坏帧没有被过滤。我当时写了一个接收方向的过滤逻辑当tuser标记该帧CRC错误时直接丢弃整帧并且不回传用户数据。这个过滤逻辑大约二十行但价值非常大因为100G链路上偶尔出现的错误帧如果直接进入UDP层会造成上层业务解析错误。3.3 ARP和ICMP的处理如果你希望服务器ping通FPGA板卡ARP和ICMP是必须的。verilog-ethernet里有完整的arp_cache和icmp模块但默认配置下ARP缓存表深度是有限制的如果你要长时间大流量通信ARP表项老化或者溢出会导致链路中断。我实际遇到的情况是用iperf3打流时前几分钟一切正常跑一段时间后突然断流重新ping又通。排查了很久发现是ARP缓存表老化刷新机制和业务流量的交互出了问题。解决办法是把arp_cache的缓存时间调长同时设置了静态ARP表项。经验如果通信对象是固定的几台服务器强烈建议在FPGA内部做静态ARP映射不要在业务运行时依赖动态ARP刷新。动态ARP一旦缓存失效会产生大量广播请求在100G链路上影响非常明显。4. 上板测试全流程实录4.1 测试环境与工具链硬件环境这样搭的FPGA板卡UltraScale VU9P板载QSFP28接口光模块100G QSFP28 SR4配多模光纤服务器双路Intel Xeon操作系统Ubuntu 20.04网卡Mellanox ConnectX-5双口100G软件工具用得最多的是iperf3、scapy和tcpdump。iperf3测带宽和丢包率scapy用来构造特定格式的UDP报文测试协议栈对异常包的响应tcpdump在服务器侧抓包验证FPGA发送的报文格式是否正确。4.2 底层检测IBERT与链路协商正式跑UDP之前必须先确认物理层没问题。这一步很多人会跳过但我觉得是最省时间的步骤。用Vivado的IBERT IP做一次误码测试让GTY收发器工作在回环模式连续跑几分钟如果误码率不是0说明PCB布线、电源或者光模块有问题这时候去调上层代码没有意义。我的实测结果是在VU9P上GTY跑103.125Gbps线速率IBERT误码率为0眼图裕量在合理范围内。确认无误之后才把CMAC配置进去看link状态是否能拉高。第一次上电时link状态一直在down和up之间跳变后来发现是光模块的信号完整性参数没有匹配调整了TX的预加重设置后稳定下来。4.3 MAC层测试从loopback到真实链路链路稳定后先用板载回环测试MAC层收发通路。有些板卡支持外部光纤环回把光模块的TX和RX短接这样验证的是完整的SerDes和光模块通路。回环测试能通之后再连接服务器网卡。连接真实链路时第一次ping不通排查步骤是在FPGA侧加一个ILA抓CMAC接收方向的信号看是否有preamble和SFD在服务器侧用ethtool看link状态确认物理层正常在服务器上tcpdump抓包确认ARP请求是否到达最后的结论很简单FPGA侧的ARP响应模块没有使能ICMP的echo响应也没有打开。开源代码里这两个模块默认有但我在前面“精简工程”的时候误删了一个例化。这也算是个教训——移植时删代码要谨慎尤其是网络层以下的功能模块。4.4 UDP端到端打流与性能测试链路和IP层都正常后就进入最核心的环节性能测试。先测试的是FPGA发送、服务器接收方向。在FPGA内部生成了一个固定载荷的UDP流用计数器模拟业务数据打包成标准的UDP报文连续发送。服务器侧用iperf3的UDP模式收流iperf3 -s -u -i 1FPGA侧的发送模块跑起来后iperf3显示的接收速率稳定在99.2Gbps左右丢包率在1e-7以下。这个结果说明协议栈本身没有成为瓶颈。然后是服务器发送、FPGA接收方向。服务器侧用iperf3打流到FPGA的IP地址FPGA内部做了一个简单的回环计数把收到的UDP报文数量统计后再发回来。这一步主要验证的是FPGA接收路径的吞吐能力。实测下来接收方向同样能跑到接近100Gbps。但这个环节出了一个有意思的问题当UDPrx侧的业务FIFO发生背压时由于上游IP模块没有流控机制FIFO溢出导致丢包。这其实是开源协议栈的一个特点——内部没有端到端的流控如果你的业务消费速率跟不上接收速率丢包是必然的。4.5 小包性能与大包性能的差异吞吐测试还有一个值得记录的指标不同报文长度下的速率。以太网物理层速率固定100Gbps但帧数pps是有限制的。64字节小包的最高速率大约是1.4881Mpps数据有效载荷只有约6.67Gbps而1518字节大包的有效吞吐可以到99Gbps以上。我在测试时专门对比了128字节、512字节、1024字节和1518字节四种帧长结果如下帧长字节理论最大有效载荷Gbps实测有效吞吐Gbps丢包率128约7.877.82 1e-6512约31.531.4 1e-61024约63.162.901518约98.798.30这个结果符合预期也说明了一个现实如果你的业务想跑满100G尽量用大包。设计的UDP包长如果在128字节以内再好的协议栈跑出来也就七八个G没有意义。5. 常见问题与调试经验记录5.1 链路不稳定link状态反复跳变反复跳变最常见的原因是物理层的信号完整性或GTY配置问题。除了IBERT验证之外还要检查光模块的FEC配置。100G SR4光模块在某些板卡上需要打开RS-FEC才能稳定工作否则误码率超标会导致链路反复重启。我在测试中开了FEC之后误码率直接降了几个数量级链路状态也稳定了。提示FEC开与不开直接影响链路的稳定性但会增加约数个纳秒的时延。对时延敏感的场景需要在稳定性和时延之间做权衡。5.2 能ping通但UDP业务不通这种问题通常是ARP或UDP端口配置导致的。ARP缓存表项老化后重新发起ARP请求如果请求是广播帧在100G链路上占用带宽很小不会造成断流。但如果板卡上有防火墙性质的分包过滤逻辑需要确认UDP目标端口号是否在过滤白名单里。还有一种隐蔽情况板卡的UDP协议栈只处理目标MAC是本机MAC的帧如果对端用不同VLAN ID发送MAC地址可能不匹配导致帧被丢弃。5.3 丢包率过高丢包率高的排查顺序我总结为先看FIFO深度再看背压机制最后看校验和。业务侧FIFO深度不足是最常见的原因。UDP接收模块瞬间收到大块数据时业务逻辑来不及消费FIFO溢出就会丢包。解决办法一个是加大FIFO更深层的方案是让业务消费逻辑能够随机应变地调整消费速率。另一个容易忽视的点是UDP校验和。部分软件工具发送UDP包时校验和为0表示不校验但如果FPGA侧的校验和检查模块强制将校验和0当作错误处理所有这样的包都会被丢弃。很多开源代码里这种边界情况处理得不够好需要自己修改。5.4 逻辑利用率过高导致时序不过当你把整个协议栈加进一个大工程时资源占用可能并不高但时序收敛会很痛苦。原因在于MAC层512bit的数据通路跨了太多组合逻辑。我的经验是模块之间全部插AXI-Stream寄存器级也就是SRL/FF顶层做pipeline不要省那几级寄存器。在100G这种速率下把时序约束做死在第一位逻辑简洁性是第二位。6. 一点个人经验总结整个项目做完后最大的体会是100G UDP的代码本身并不复杂复杂的是它和硬件平台之间千丝万缕的耦合关系。开源的协议栈给了你一条捷径但理解每一层为什么这样设计才能做好移植和调优。另外一个建议是测试一定不能省。IBERT、回环、ping、小包打流、大包打流每一步都有它存在的意义跳过去直接跑业务出了问题再回头查时间成本翻倍。最后再分享一个小技巧在板卡上留一组计数器统计发送帧数、接收帧数、丢弃帧数通过串口或者DDR刷新出来。这一组计数器的价值比任何逻辑分析仪都大因为你不用暂停系统就能实时看到数据通路的健康状态。可以说调ぜん了它这个项目就算真正收工了。