100G UDP协议栈FPGA移植实战:开源方案与上板测试指南
1. 整体设计与思路拆解1.1 为什么一定要上 100G UDP做过高速数据采集或者图像传输的工程师应该都有同感当数据量到几十 Gbps 之后TCP 那套拥塞控制、连接管理、重传机制在 FPGA 里实现起来非常痛苦资源占用大不说逻辑时序也容易崩。UDP 无连接、无状态、处理逻辑简单天然适合硬件化。100G 以太网在单根光纤上就能提供 100Gbps 的线速带宽配合 UDP 协议栈可以做到极低延迟、极高吞吐的数据传输非常适合做数据中心互联、高速存储、多通道图像汇聚、雷达信号采集这类场景。我自己做这个移植项目的时候目标很明确把一套开源以太网协议栈里带 100G UDP 完整收发路径的 RTL 代码从参考工程移植到自己的板卡工程里跑通上板测试用工具验证收发包的正确性和吞吐率。这个过程里踩了不少坑也总结了一些可复现的步骤。这篇文章会直接把关键文件、关键参数、测试命令和排障方法都写出来适合两种人看一种是想快速体验 100G UDP 的 FPGA 工程师另一种是已经跑过 10G/25G 以太网、想升级到 100G 的同学。1.2 选开源方案的三个理由刚开始我也纠结过要不要直接用 Vivado 自带的 100G Ethernet IP但对比之后还是选了开源方案主要有三个原因。第一100G MAC 本身复杂度很高。100G 以太网用的是 64B/66B 编码数据通路上要处理 20 条 lane 的对齐、同步头提取、FCS 校验、PCS 层状态机。自己从零写一个能跑到线速的 100G MAC没有几个月时间根本下不来而且出了 bug 极难排查。开源的成熟实现已经经历过多轮迭代时序收敛、异常处理都做得比较稳。第二开源协议栈的接口通常很统一。以社区常用的 verilog-ethernet 为例内部全部基于 AXI-Stream 接口用户数据通路只要跟 axis 总线打交道就行不需要理解底层 GTY 收发器和 64B/66B 编码的细节。这种模块化设计非常方便做二次开发。第三代码体积小、可读性好。开源实现往往没有太多复杂的配置寄存器改动起来很透明调试的时候直接抓内部信号就行。相比之下闭源 IP 核内部是个黑盒子出了问题你只能干瞪眼。1.3 整体架构长什么样整个 100G UDP 移植工程的架构一句话就能说清楚上位机或者对端网卡通过光纤收发 UDP 包FPGA 内部完成 MAC/网络层解封装后把用户数据交给业务逻辑反向则把业务逻辑产生的数据封装成 UDP 包发出去。发送方向的数据流是用户 FIFO 或者 DMA 读出的数据 → UDP TX 模块加 UDP 头、IP 头、MAC 地址→ 100G MAC加前导码、FCS 校验→ GTY 高速收发器64B/66B 编码、并串转换→ QSFP28 光模块 → 光纤。接收方向则完全对称光纤 → QSFP28 → GTY 收发器串并转换、64B/66B 解码→ 100G MACFCS 校验、剥前导码→ UDP RX 模块校验 IP 头、解析 UDP 端口、剥头→ 用户数据 FIFO。这个架构看起来简单但真正移植的时候用户时钟频率、GTY 参考时钟、复位时序、跨时钟域处理、时序收敛每一个点都可能让你卡上几天。2. 开源方案与核心模块拆解2.1 几个主流开源方案怎么选我在调研阶段翻了几个开源项目简单对比一下供你参考。方案特点适用场景verilog-ethernet模块化设计AXI-Stream 接口支持 10G/25G/40G/100G带完整 UDP/TCP/IP 协议栈自带 100G 参考工程多数人的首选从 10G 升级到 100G 的路径平滑Open-NIC偏向智能网卡方向带 PCIe DMA、eBPF 数据通路支持多队列做 RDMA、可编程网卡、数据中心卸载的场景Xilinx 官方 100G IP闭源 IP需要 license带 AXI-Stream 接口配置丰富要求官方支持、不差 license、不想折腾开源代码的商用项目自己写 UDP MAC完全可控学习用可以产品化不推荐如果你只是想快速跑通 100G UDP 收发最省力的路径是选 verilog-ethernet。它的 100G 参考工程把eth_mac_100g、udp_complete_100g这些模块已经整合好了你主要做的是把它接到自己的用户逻辑里然后上板测试。2.2 UDP 协议栈内部结构解析拿 verilog-ethernet 的 100G 路径来说核心文件大致有这么几个eth_mac_100g.v是 100G MAC 的主模块内部调用ssio_100g.v做 64B/66B PCS 层处理最终接到 GTY 收发器上。udp_complete_100g.v则把 MAC 层、IP 层、UDP 层打包封装成一个整体对外只暴露 AXI-Stream 接口和少量控制信号对用户来说最友好。发送端udp_100g_tx.v做的事情是从 AXI-Stream 接口接收用户数据自动添加以太网头部目的 MAC、源 MAC、EtherType、IPv4 头部版本、长度、校验和、UDP 头部源端口、目的端口、长度最后把完整的帧交给 MAC 层加上 FCS。接收端udp_100g_rx.v则做反向操作剥掉 MAC 头、校验 IP 头、检查 UDP 端口号最后只把用户 payload 通过 AXI-Stream 输出。值得注意的是这些模块内部大量使用了 AXI-Stream 总线的tkeep、tuser、tlast信号。100G 数据位宽是 512bit64 字节一包数据可能跨多个周期传输tuser信号用来指示错误帧和包边界。如果你之前只做过 AXI-Lite 或者 AXI-MM上手 AXI-Stream 需要花点时间看清楚每个信号在每个时钟周期里到底长什么样。除了协议栈本体这套开源工程里还带了一堆实用库axis_fifo.v用于跨时钟域 FIFOaxis_adapter.v用于位宽转换eth_phy_gt_100g.v负责例化 GTY 收发器。这些库文件是官方参考工程的重要组成部分移植的时候不要漏掉。2.3 100G 内核必须理解的关键细节首先是 64B/66B 编码。100G 以太网把数据切分成 64bit 的块加上 2bit 同步头01表示数据块10表示控制块所以物理链路速率是 103.125Gbps其中只有 100Gbps 是有效载荷。GTY 收发器在 100G 模式下会实例化 20 条收发 lane每条 lane 约 5.15625Gbps数据通路时钟是 322.265625MHz数据位宽 512bit。这个 322MHz 时钟是全工程性能的基础所有用户逻辑时序都要在这个时钟频率下收敛难度比 10G156.25MHz 用户时钟高不少。其次是 FCS 校验。以太网帧尾部 4 字节是 CRC32 校验值由发送端 MAC 自动生成接收端 MAC 自动检查。有些开源实现里有个cfg_mac_remove_fcs选项如果两端配置不一致接收端会一直报 CRC 错误。我测试的时候曾经被这个配置坑过后面会专门讲。第三是 GTY 收发器的参考时钟。100G 以太网对参考时钟要求很高需要 156.25MHz 高质量低抖动时钟。如果参考时钟质量不行GTY 的gtpowergood会不定时拉低或者 CDR 锁定后很快失锁。上板之前建议先用频谱仪测一下参考时钟的相位噪声或者至少确认晶振型号和 PCB 走线是没问题的。3. 移植过程与上板步骤3.1 移植前需要准备的东西硬件方面我用的是一块带 XCKU115 FPGA 的板卡板载 QSFP28 光口GTY 收发器全部引出到光模块。你可以用 VCU118、VCU1525 或者任何带 100G 光口的板卡核心要求是 FPGA 要有足够的 GTY 收发器资源。工具方面Vivado 版本建议 2019.2 以上因为老版本对 100G 以太网和 GTY 的原语支持不完整。软件准备方面我强烈建议提前装好 iperf3 和 Wireshark后面性能测试和抓包排查都得用它们。代码获取直接从开源代码托管平台搜索 verilog-ethernet下载或者 clone 下来即可。注意这套代码里的 100G 参考工程默认是给 Xilinx 的特定板卡写的你要做的是把核心 RTL 文件挑出来放到自己的工程里而不是直接把整个参考工程拿来用。3.2 工程搭建与文件清单我自己建工程的经验是不要图省事把整个仓库所有文件都加进 Vivado而是只加 100G UDP 路径真正需要的文件否则综合时间会莫名变长还有可能引入不必要的依赖冲突。核心文件列表大致如下lib/eth/eth_mac_100g.vlib/eth/udp_complete_100g.vlib/eth/udp_100g_rx.vlib/eth/udp_100g_tx.vlib/eth/eth_phy_100g.vlib/eth/eth_phy_gt_100g.vlib/eth/ssio_100g.vlib/axis/axis_fifo.vlib/axis/axis_adapter.vlib/axis/priority_encoder.v部分模块依赖如果 FPGA 引脚分配和参考工程不一样还需要自己写一个引脚约束 XDC 文件。这里有个比较容易踩的坑ssio_100g.v里例化了很多 Xilinx 原语比如gt_quad_base、gt_channel如果你用其他厂商的 FPGA这套代码是没法直接移植的。所以准确说这个开源方案是 Xilinx FPGA 专用。3.3 约束要点与综合布线100G 工程的约束主要是时钟约束和引脚约束两部分。时钟约束方面给 GTY 参考时钟创建 156.25MHz 的输入时钟约束同时要给 322MHz 的用户逻辑时钟创建时序约束。如果你的工程里还有 10G 以太网路径或者其他外设时钟也要一并约束清楚否则综合工具不知道时钟关系时序分析结果会非常不靠谱。引脚约束方面QSFP28 光模块的 txp/txn 和 rxp/rxn 是差分对需要在 XDC 里指定PACKAGE_PIN和IOSTANDARD比如LVDS。这里有个关键细节100G 光模块的 RX 侧没有参考时钟引脚GTY 通过 CDR 从接收数据流里恢复时钟所以你要保证对端设备发送的信号质量在规范范围内否则光模块的 RX 信号会不稳定。综合和布线的时候我建议开启retiming然后把综合策略设为PerformanceExtraNetDelay。直接跑默认策略也能过但 322MHz 的数据通路经常会出现 setup 违例开着 retiming 能省去很多手动优化的时间。跑 implementation 的时候如果时序报告还有violation优先检查是不是跨时钟域 FIFO 深度不够或者某个组合逻辑路径太大了。3.4 上板环境搭建板卡程序烧录之后要想做 UDP 收发测试还需要搭一套测试环境。最简单的方式是 FPGA 板卡和一台有 100G 网卡的服务器直连中间用 QSFP28 光模块和光纤跳线对接。对端网卡我用的 Mellanox ConnectX-5/6驱动装好之后能自动识别 100G 链路。如果你的板卡有两个光口也可以做板卡内部自环测试把一个口的 TX 直接对到另一个口的 RX这样不用依赖外部设备。IP 地址规划随意比如 FPGA 内固定 192.168.1.10服务器网卡设 192.168.1.20掩码 255.255.255.0两端能互相 ping 通就说明链路建立起来了。注意 FPGA 内部实现的 UDP 协议栈能不能响应 ICMP 取决于代码实现有些开源实现只处理 UDPICMP 会被直接丢弃ping 不通不代表链路不通最好直接收发 UDP 包验证。4. 上板测试与性能验证4.1 用 iperf3 打流测试链路起来之后第一件事就是用 iperf3 做 UDP 打流测试。这里有个常见的困惑到底看发送端还是接收端的数据实测下来UDP 打流时两台机器如果只跑一个方向的流量应该看接收端的统计。因为发送端从网卡驱动发出数据后就不管了接收端收到的包数量、丢包、带宽才反映了真实链路情况。如果对端是普通 PC 或者服务器典型打流命令如下# 服务器端接收 iperf3 -s -i 1 # 客户端发送 iperf3 -c 192.168.1.20 -u -b 80G -l 1472 -t 30 -i 1这里-b 80G是目标带宽-l 1472是 UDP payload 大小1500 MTU 减去 20 字节 IP 头再减去 8 字节 UDP 头-t 30是测试时长。如果你把-b设成 100G 以上实际发送速率也会接近 100G但链路很难打满大部分时间会卡在对端网卡驱动或者 PCIe 带宽上。iPerf3 跑完之后接收端会输出类似这样的统计[ ID] Interval Transfer Bitrate Jitter Lost/Total Datagrams [ 5] 0.00-30.00 sec 279 GBytes 79.8 Gbits/sec 0.000 ms 0/204944857 (0%)看到 Lost 为 0Bitrate 接近 80Gbps说明这条 100G UDP 通路在普通大包场景下工作正常。如果丢包率居高不下问题大概率不在 FPGA而是对端网卡驱动的 UDP 接收缓冲区太小或者 iperf3 所在的 CPU 核心接收中断处理不过来。4.2 板级调试观测光靠 iperf3 统计还不够FPGA 内部的实际收发行为一定要用逻辑分析仪抓一下不然你不知道数据到底有没有走到预期位置。建议把 ILA 核挂在udp_complete_100g的 AXI-Stream 接口上观察axis_rx_tdata、axis_rx_tvalid、axis_rx_tlast、axis_rx_tuser这四个关键信号。触发条件可以设为tvalid tuser这样能在有错误帧时抓一拍。更实用的做法是在 RTL 里加几个计数器收包总计数、CRC 错误计数、UDP 端口匹配计数、用户数据写入 FIFO 计数。上板后通过串口打印或者 JTAG 读取这些计数器的值一分钟就能定位是 MAC 层、IP 层还是 UDP 层出了问题。比如收包总数在涨、CRC 错误计数为零、UDP 匹配计数为零那说明包是被 IP 校验或者 UDP 端口过滤丢掉了直接查udp_100g_rx.v里的过滤逻辑就行。4.3 性能结果怎么验收我测试完的实际结果是1472 字节大包纯 UDP 收发可以做到线速 99Gbps 以上丢包率 0.001% 以下延迟在微秒级。但在 64 字节小包场景下性能会明显下跌因为线速下每秒要处理约 1.48 亿个包包间隔太小MAC 层和 GTY 都要花大量周期做前导码对齐和帧状态机跳转不少开源实现的小包吞吐率只有线速的 60% 到 80%。如果你的业务场景以小包为主建议做两件事一是把axis_fifo深度加大减小背压概率二是把发送端多个用户通道做仲裁合并避免 IDLE 空隙太大。如果业务场景是大包为主开源实现的性能完全够用。5. 常见问题与排查技巧实录5.1 CRC 错误一直涨怎么查现象接收端bad_crc_count持续增加上位机 Wireshark 也能看到很多 bad checksum 的包。排查思路从两端入手。如果是 FPGA 接收方向报 CRC 错误先确认对端网卡发出的帧格式没有开启 VLAN tag有些网卡驱动默认加了 802.1Q 头而 FPGA 的 MAC 实现没有解析 VLAN就会把 VLAN 头后面的数据当错位数据校验。如果是 FPGA 发送方向导致对端报 CRC 错误检查发送端是否开启了cfg_mac_remove_fcs。这个配置位是控制 MAC 层要不要自动加 FCS 的如果 FPGA 发送路径最后一段又手动加了一遍 FCS就会造成帧尾多 4 字节。还有一个很隐蔽的点开源代码的 UDP 校验和在udp_100g_tx.v里计算如果你改了 IP 地址或者端口号的配置又没有重新计算校验和接收端会把整个包丢弃。这类错误通过计数器看是“收包数量在涨但用户 payload 写入计数不变”。5.2 GTY 锁不上或者锁上又掉了GTY 的tx_reset和rx_reset释放顺序是硬条件。正确的时序是先等 GTY 的gtpowergood拉高再释放tx_reset等tx_resetdone拉高后再释放rx_reset等rx_resetdone拉高后还要等rx_byteisaligned和rx_commadet稳定。如果复位顺序搞反了CDR 大概率锁不上。如果复位都正确但依然棒不住重点检查参考时钟。156.25MHz 参考时钟的 ppm 偏差要小于 100ppm抖动要足够低。我曾经因为硬件上参考时钟少焊了去耦电容导致 GTY 频繁失锁换了块板子后问题直接消失。还有个实用技巧在 Vivado 里打开 GTY 的 DRP 接口通过串口读取eyescan.data寄存器值可以判断接收眼图的余量。5.3 时序违例setup violation 总是消不掉100G 用户时钟 322MHz周期只有 3.1ns稍微复杂点的组合逻辑就可能超出一个时钟周期。我的处理顺序是先开 retiming重新跑综合如果还不行就把eth_mac_100g和udp_100g_rx之间插入两级寄存器把组合逻辑拆开再不行检查是不是有地方把 512bit 数据拆分成了多个小位宽处理导致逻辑层级过深。还有一个容易忽略的地方axis_adapter在做位宽转换时内部会有比较复杂的valid/ready握手逻辑这个逻辑在 322MHz 下经常成为 critical path。如果不需要位宽转换直接用配套的axis_fifo做跨时钟域不要让 adapter 出现在 100G 路径上。5.4 Wireshark 抓包为什么抓到 ICMP网上经常有人问我在 Wireshark 里加了过滤条件udp为什么还是抓到了 ICMP 包这里其实涉及 Wireshark 两个不同的过滤位置。udp写在 Wireshark 工具栏的 Filter 里是显示过滤器只会影响帧列表里显示的内容不影响实际捕获。捕获过程始终会把网卡收到的所有包包括 ICMP、ARP、TCP抓下来只是没显示出来。如果你在抓包选项里用捕获过滤器Capture Filter写udp那才是真正在驱动层面就过滤掉非 UDP 的包。更坑的是某些网卡驱动在混杂模式下会把发往其他 MAC 地址的包也收上来比如 ARP 广播、ICMP 广播这些包即使在显示过滤器里设了udp也不该显示但如果你的过滤语法写错了比如在显示过滤器里写成了抓包过滤语法结果就会很混乱。我的建议是先不写任何过滤条件抓一轮确认包的来源和类型再加显示过滤器过滤不要指望捕获过滤器能帮你省事。如果你在做 100G UDP 测试时发现上位机抓包有 ICMP 包大概率是你板卡侧或者网卡侧在发 ARP/ICMP 协议报文。很多开源 UDP 协议栈没有实现 ICMP 应答但如果对端网卡持续发 ARP 请求某些 MAC 实现会回复这属于正常现象不影响 UDP 数据传输。5.5 大流量下上位机丢包这个问题不在 FPGA却经常被 FPGA 工程师误判。100G 打流时Windows 或者 Linux 默认的 UDP 接收缓冲区非常小瞬间突发就能把缓冲区灌满系统直接丢弃后来的包。Linux 上调大接收缓冲区sysctl -w net.core.rmem_max134217728 sysctl -w net.core.rmem_default134217728Windows 上则需要改注册表HKLM\SYSTEM\CurrentControlSet\Services\AFD\Parameters里新增DefaultReceiveWindow和DefaultSendWindow设成较大的值然后重启系统。还有一种更推荐的做法是不要让上位机 CPU 直接处理 100G UDP 流量而是用支持 RSS接收端缩放的网卡把中断分散到多个 CPU 核心上。我测试时用 Mellanox 网卡开启多队列后上位机接收能力提升非常明显。5.6 DDR 带宽不够导致的隐性问题如果你的业务逻辑需要从 DDR 读数据再通过 UDP 发送这里有个很隐蔽的坑DDR4 的理论带宽虽然足够但 AXI 总线在随机小粒度访问时的效率只有理论值的 60% 到 80%。100G 线速意味着每秒约 12.5GB 的数据吞吐如果你的 AXI 读 burst 长度很小DDR 控制器大部分时间都花在行切换和列切换上带宽根本喂不饱 UDP 发送端表现出来就是丢包或者吞吐率波动。解决办法是让 AXI 读请求尽量 burst 对齐一次读 512B 或 1KB并且加大读 FIFO 深度。如果业务逻辑拿到的数据本身就是离散的最好先经过一个 DMA 引擎把数据整理成连续块再喂给 UDP 发送端。最后再分享一点个人的实际体会100G UDP 移植这事儿难不在“UDP”而在“100G”这三个字。UDP 协议本身很简单任何一个会写状态机的工程师都能实现但 100G 下的时钟频率、GTY 收发器配置、64B/66B 编码细节、时序收敛和测试环境搭建才是真正耗时间的地方。如果让我给后来者一个最重要的建议那就是不要在 64B/66B 编码或者 MAC 层上自己造轮子用成熟的 MAC 内核是节省时间的唯一正确选择。另一个容易被低估的是测试环节。iqerf3 的统计结果、Wireshark 的抓包记录、FPGA 内部的计数器这三样东西必须能互相对应起来你才能确信整个链路真的没问题。我见过不少同行FPGA 侧计数显示发包成功但对端根本没收到最后查出来是光纤衰减太大导致信号质量差MAC 层一直在重同步。遇到链路问题时先看光模块的 RX power再看 GTY 的误码率寄存器最后才去翻 RTL 逻辑这个顺序能帮你少走很多弯路。这套内核跑通之后后面值得扩展的方向挺多PCIe DMA 桥接 100G UDP 实现高性能网络接口、多通道数据流仲裁合并、FPGA 板卡之间的自环测试脚本化。如果你手头正好有一块 100G 板卡建议直接拉一套开源代码按上面的步骤走一遍相信很快就能看到自己的板卡和对端机器之间跑满带宽的那一刻。