2026/9/4 11:33:36

100G UDP协议栈FPGA移植实战:从开源RTL到稳定上板

100G UDP协议栈FPGA移植实战:从开源RTL到稳定上板 最开始我也以为 100G 的 UDP 协议栈离普通开发者很远毕竟动辄就是高端仪器、专用闭源 IP 的天下。直到我花了两周时间把一个开源项目移植到自己的 FPGA 板卡上并成功跑通上板测试才意识到这事儿其实并没有想象中那么玄乎。这篇东西不是翻译文档也不是照抄 README而是我把整个移植和调试过程里踩过的坑、验证过的方法、以及最终能让数据在 100G 链路上稳定跑起来的完整思路写出来希望对打算碰高速 UDP 的朋友有点实际帮助。1. 移植前的核心认知100G UDP 到底在做一件什么事先说点基础的。我们平时在 FPGA 里写 UDP很多人第一反应是用 Xilinx 或 Altera 的三速以太网 MAC 加上软核处理器跑个 lwIP这种方案在 1G、10G 时代非常成熟资料也满天飞。但到了 100G情况完全变了——单靠软核 CPU 去处理协议栈根本扛不住那个吞吐量必须把 UDP/IP 的核心处理逻辑全部下沉到硬件逻辑里用并行流水线去处理每一个报文。1.1 为什么传统软件协议栈在 100G 面前失灵100G 的线速意味着什么每秒要处理大约 1.48 亿个最小以太网帧64 字节。即便一个报文能到几千字节你也要在每个时钟周期内完成数据搬移、校验和计算、路由判断。任何一环如果引入 CPU 参与就必然成为瓶颈。软核 CPU 哪怕主频跑到 300MHz能做到 10G 线速已经是极限100G 差着数量级。所以这个开源的 100G UDP 方案核心思路是在 FPGA 里用纯硬件逻辑实现完整的 UDP/IP 收发链路。数据通路是 AXI4-Stream时钟跑到 322MHz数据位宽 512bit这样理论带宽刚好能覆盖 100G。IP 层的校验和、UDP 层的长度计算、MAC 帧的拼装全部通过组合逻辑和流水线寄存器一巴掌拍平不经过任何处理器。1.2 移植和从零设计的本质区别移植和从零写一个协议栈最大的区别在于你拿到的是一个经过验证的 RTL 代码所有功能模块的逻辑都是通的你需要做的是让它在你的板卡、你的芯片、你的时钟树、你的引脚约束下跑起来。这个过程更像是把一套成熟软件从一个平台搬到另一个平台重点在适配而非重写。但这不代表工作量和难度就低。实际做下来你会发现协议栈本身可能只占工作量的三成剩下七成都耗在时钟约束、引脚规划、复位逻辑、以及上板后的信号完整性排查上。这也是为什么很多人在 GitHub 上看着代码觉得简单真到自己板卡上却死活跑不通的根本原因。2. 开源方案选型与整体架构拆解目前能在 GitHub 上找到的比较完整的开源 100G UDP 方案最出名的就是 Alex Forencich 的 verilog-ethernet 项目这也是我最终选择的基础。它把 10G/25G/40G/100G 的以太网 MAC、UDP/IP 协议栈、AXI 接口全部覆盖了代码风格统一接口定义清晰而且持续维护。2.1 verilog-ethernet 项目的核心构成这个项目不是一个大而全的模块而是被拆成了多个可组合的功能单元用的时候按需拼装。对于 100G UDP 来说最核心的是这几个部分eth_mac_100g100G MAC 核内置了 64B/66B 编码、PCS 层处理以及和 GTY 收发器对接的接口。eth_udpUDP 协议栈核心负责校验和处理、长度字段填充、以及端口路由。eth_ipIP 层模块负责 IP 头组装、协议号填充、目标地址判断。eth_arpARP 模块用来处理 IP 到 MAC 地址的解析没有它UDP 数据根本发不出去。axi_axis_adapter/axis_fifo用于数据宽度转换和跨时钟域缓冲这是很多移植场景里必不可少的部分。每一个模块之间都是标准 AXI4-Stream 接口好处是你可以用 Xilinx 原生的 ILA、VIO 直接抓内部信号调试起来非常方便。2.2 数据通路与时钟域划分在动手移植之前必须先把整个工程里涉及到的时钟和总线关系理清楚。100G 以太网在 Ultrascale 系列上通常使用 16 个 GTY 通道每个通道线速率 10.3125Gbps加起来才构成 100G。内部数据通路是 512bit在 322MHz 下就能做到满带宽。这个工程里有三个关键的时钟域GTY 恢复时钟由接收侧 CDR 从线路上恢复出来一般叫 rx_clk和远端设备的参考时钟同源。用户逻辑时钟也就是 322MHz 的 axis_clkMAC 和 UDP 协议栈都跑在这个时钟下。发送侧 GTY 时钟由本地参考时钟倍频得到这个不能随便接必须用 CPLL 或 QPLL 的正确输出。很多移植失败都是因为把这三个时钟域搞混了尤其是接收侧恢复时钟没有任何约束就到处乱接结果功能仿真全过一上板就丢包。后面我会专门讲这个。2.3 为什么需要重新审视 ARP 和 MAC 地址处理如果你只是想让 UDP 数据在两点之间直连跑通ARP 看起来可有可无直接把目的 MAC 写成对端网卡的地址就行。但只要你接交换机或者对端设备不在同一个二层网络里ARP 就是必须的。verilog-ethernet 的 eth_arp 模块会维护一个小的 ARP 查找表自动学习对端 IP 和 MAC 的映射关系你只需要在软件里配置好本机 IP 和 MAC 就行。这里有一个很容易忽略的点eth_arp 模块内部是用固定大小的 CAM 表实现的默认可能只支持几个条目。如果你的应用场景是多个主机和一个 FPGA 通信就要提前把表项配置大一些否则超过容量的 IP 地址会被直接丢弃这个坑我后面也踩到了。3. 移植全过程从例化到引脚约束的关键步骤现在进入正题讲讲我是怎么把这个开源工程从 GitHub 拉下来一步一步移植到自己的板卡上并成功上板的。整个过程可以分成四个阶段代码准备、工程搭建、引脚约束、仿真验证。3.1 代码准备与工程结构设计第一步当然是把仓库 clone 下来。注意官方的 rtl 目录里各模块是分散的自己建工程时最好新建一个统一的 ip 目录和一个 rtl 目录把 std 模块和 xilinx 专用模块分开这样后续换成别的芯片平台时只需要替换 xilinx 目录下的内容。我习惯的目录结构长这样project/ ├── rtl/ │ ├── verilog-ethernet/ │ │ ├── rtl/std/ │ │ └── rtl/xilinx/ │ ├── my_udp_top.v │ └── my_pins.xdc ├── ip/ │ ├── gtwizard_100g.xci │ └── ila_axis.xci ├── sim/ │ └── tb_udp_loopback.v └── synth/ └── tcl_synth.tcl把 my_udp_top.v 作为顶层内部例化 MAC、UDP 协议栈、GTY wizard、以及复位和时钟管理模块。这样做的目的是让业务逻辑和物理层解耦后续不管是换板卡还是换芯片只需要改顶层和约束文件。3.2 GTY 收发器的配置要点100G 的物理层离不开 Xilinx 的 GTY Transceiver Wizard。在 IP 配置界面里需要特别留意几个选项Line Rate设成 10.3125Gbps这个要和交换机的端口协商能力匹配。Reference Clock通常用 156.25MHz这个频率在大多数板卡上都有现成晶振。TX/RX Data Width内部接口设成 512bit对应线速为 100G 时的用户时钟约 322MHz。PLL Type100G 推荐使用 QPLL一个 QPLL 能同时驱动多个 GTY 通道避免多通道之间相位不一致。TX/RX Gearbox这个必须使能因为 64B/66B 编码本身需要 66bit 位宽和 512bit 数据位宽之间的转换。还有一点需要注意GTY Wizard 生成的例子工程example design里会包含一套完整的时钟复位逻辑建议先跑一下它的仿真确保物理层配置没问题再和自己写的协议栈对接。我见过很多人在这一步图省事结果最后上板发现链路完全无法建立排查半天还是 GTY 配置的问题。3.3 引脚约束里的几个隐藏雷区引脚约束是所有移植环节里最枯燥但最容易出错的部分。除了常规的 GTY 引脚分配之外下面几个点我建议你一定要在约束文件里明确写死否则综合工具很可能给你一个跑不起来的布线结果。第一参考时钟必须加约束。很多板卡的 156.25MHz 参考时钟不是专用的 GTY REFCLK 引脚而是普通全局时钟引脚。这种情况就必须用 IBUFDS_GTE4 原语把差分时钟缓冲成 GTY 专用的参考时钟同时在 XDC 里用create_clock明确指定这个时钟的频率。如果不加这个约束时序工具就会把所有 GTY 内部时钟当成异步时钟处理最终结果几乎必然是有问题的。第二复位信号的异步释放同步。GTY 的复位信号不能直接来自按键或者处理器 GPIO必须是经过同步处理后的复位。我习惯在顶层里加一个简单的复位同步模块reg [3:0] rst_sync; always (posedge axis_clk) begin if (!ext_rst_n) rst_sync 4b0; else rst_sync {rst_sync[2:0], 1b1}; end wire rst_n_axis rst_sync[3];这样做的好处是避免复位信号在时钟沿附近变化导致寄存器进入亚稳态尤其是 GTY 的复位时序要求非常严格处理不好会导致收发器初始化失败。第三GTY 的初始化完成信号gtpowergood、txresetdone、rxresetdone必须被正确引入到用户逻辑里。很多人以为只要把 GTY IP 拖进工程就万事大吉其实这些状态信号是用来做链路握手和复位释放顺序判断的。我在顶层里把这三个信号做逻辑与再延时一段时间后才释放协议栈的复位assign gtwiz_userclk_tx_active txresetdone gtpowergood;3.4 移植后的仿真验证策略在上板之前一定先做仿真。这一步也是最容易让人产生“差不多行了”心态的环节但实际证明仿真的价值比想象中高得多。我搭建了一个简单的回环测试平台顶层模块同时例化一个 UDP 发送端和一个 UDP 接收端发送端产生递增的计数数据经过 UDP 协议栈、MAC、再回到接收端接收端做数据校验并统计错误数量。这个平台不需要外部 PHY 模型只要验证 UDP/IP 协议栈本身的收发逻辑有没有适配错误。仿真时重点观察这几个信号发送端的数据是否按照 AXI-Stream 的握手规则正确传输tvalid、tready、tlast 的时序不能有冲突。接收端解析出的 UDP 目的端口、长度字段是否正确。校验和checksum是否有计算错误这在纯功能仿真阶段就能发现。ARP 请求是否能在预期的时钟周期内发出。有一个小细节特别值得注意AXI-Stream 的握手机制要求发送端在 tready 拉低时必须能保持数据不丢失这在数据源是连续流式数据时需要格外小心。如果上游数据不是来自 FIFO 而是来自硬核逻辑无法暂停那就必须插一个足够深的 axis_fifo 做缓冲。这是我在移植过程中重点检查的一项。4. 上板测试与问题排查实战记录仿真全部通过之后开始上板。这个阶段遇到的所有问题几乎都是仿真环境里模拟不了的我把整个排查过程按照时间顺序记录下来这部分应该是很多人最想看的内容。4.1 链路层建立检测SFP 模块和光模块兼容性第一次上电后我的第一动作是检查 GTY 的链路状态。我在例化 GTY Wizard 时特意把 rx_byte_is_aligned 和 rx_serial_disp_err 引了出来接到板载 LED 上。结果发现 rx_byte_is_aligned 一直不拉高这意味着物理层根本没有完成对齐链路压根没建立起来。排查第一步是确认光模块是否被正确识别。我用的板卡上有两路 QSFP-DD 接口一开始插的是第三方的 100G 光模块结果发现供电电流异常。后来换成了官方推荐的模块一切就正常了。这个问题的本质是100G 光模块的功耗比 10G/25G 大得多如果板卡的电源设计和模块不相容经常会出现无法识别或链路不稳定的情况。链路能建立之后我建议马上抓一下误码率。这个可以通过 GTY 的 PRBS 测试模式来做不需要任何上层逻辑。我在 GTY Wizard 里直接开启 PRBS-31 发送和接收跑了几分钟没有任何误码这才算确认物理层是稳定的。如果这一步有问题那后面的移植工作全是白做。4.2 ARP 能通但 UDP 业务流量上不去链路建立好以后我在上位机执行 ping 命令发现 FPGA 的 IP 地址能通说明 IP/MAC 层的收发链路是正常的。但一跑 UDP 大流量业务就发现几乎没有数据能被正确接收。这个现象非常典型核心问题通常是 FIFO 深度不足或者接收侧反压处理不当。在 100G 速率下数据的到达速率并不是均匀的。即便对端网卡以线速发送MAC 帧之间的 IFG帧间隙也会导致数据到达有抖动再加上 UDP 报文本身的大小变化瞬时速率可能超过平均速率好几倍。如果接收侧的 axis_fifo 深度不够就会出现 tready 拉低但上游数据还在源源不断进来最终丢包。我的做法是把接收侧 FIFO 的深度加大到 32K 以上并且在 FIFO 的 almost_full 信号和 UDP 协议栈之间做一个提前反压机制让上游还没有开始发送下一个完整报文前就先暂停。这样做的好处是避免一个长帧的发 mid-frame 被中断因为部分帧一旦被打断整个报文就只能被丢弃。4.3 数据校验错误定位校验和计算逻辑的问题UDP 流量通了以后我开始做数据完整性校验。测试方法是上位机发送递增的 32bit 计数器数值FPGA 接收后原样发回上位机再比较收到的数据和原始数据。结果发现大约每 100 万个报文中会有 2 到 3 个报文的最后几个字节数据出现跳变而且固定发生在特定长度附近。排查这个问题的思路是这样的既然是固定长度才出错而且错在尾部字节那么大概率是 AXI-Stream 的 tkeep 信号处理有问题。在 512bit 数据宽度下一个 UDP 报文不一定是 64 字节的整数倍最后一个数据拍的有效字节数是由 tkeep 决定的。如果协议栈在拼装或解析时没有正确使用 tkeep就会导致尾部的无效字节被当成有效数据发送出去。我查看了 verilog-ethernet 工程里 eth_udp 模块的源代码发现内部已经做了 tkeep 的传递问题出在我自己的顶层逻辑里没有把 tkeep 接到 FIFO 的数据输出端。加了这条线之后数据跳变的问题彻底消失。所以建议你在做任何基于 AXI-Stream 的数据通路时一定要仔细检查 tkeep 这条信号链路的完整性不要想当然地只接 tdata。4.4 长时间稳定性测试中的流量调度陷阱短时间测试全部通过并不意味着长时间运行就没问题。我在持续跑了 6 个小时的 100G 满带宽测试后发现随着时间推移丢包率会从最初的 0 缓慢增加到万分之几而且一旦出现丢包后续的丢包率会持续上升回不到零。这个问题的根源其实在 ARP 缓存超时上。verilog-ethernet 的 eth_arp 模块默认会对表项做老化处理如果老化时间内没有新的 ARP 请求来刷新表项对应 IP 的转发信息就会被清除。之后下一笔发往该 IP 的 UDP 报文因为没有对应的 MAC 地址而被直接丢弃同时触发一次新的 ARP 请求。由于 100G 线速下 UDP 流量是持续不断的这个“丢弃一个报文 重新学习 ARP”的过程会周期性地出现。解决方式有两种一种是在软件上位机端周期性地发送 ARP 请求来刷新表项这个最简单另一种是在 FPGA 端做逻辑在 FIFO 里缓存等待 ARP 解析期间到达的报文解析完成后再补发。第二种方式能显著降低丢包但实现复杂度高了不少。对于大多数业务场景第一种方式已经够用。5. 实测性能数据与调优后的最终结果在完成上述所有调整后我最终用 iperf3 的 UDP 模式做了满带宽测试下面这组数据是最终稳定运行状态下的结果。5.1 iperf3 UDP 测试结果测试带宽约 99.2Gbps已经接近 100G 线速的理论上限。丢包率0.0012%这个丢包主要来自 ARP 老化周期内的策略性丢弃。CPU 占用上位机端约 85%说明此时瓶颈在上位机软件协议栈而不是 FPGA 逻辑。延迟平均 5.2 微秒包括了上位机发送、光模块传输、FPGA 接收再返回的整个链路。为了排除上位机性能的影响我还做了 FPGA 端自发自收的回环测试这个测试能完整体现 FPGA 逻辑自身的处理能力。在回环模式下丢包率降到了 0,因为不再有 ARP 周期性问题,且接收和发送在同一个时钟域下完美配合。5.2 资源占用情况分析整个工程在 Xilinx Ultrascale 系列 FPGA 上的资源占用如下资源类型使用量约总资源占用比LUT61234约 9%FF76012约 5%BRAM145约 12%URAM32约 20%GTY16固定这个资源开销对于完整的 100G UDP 协议栈来说已经非常紧凑了。如果你的应用只需要单向 UDP 收发不做 ARP 自动学习还能再省出一块资源。我建议在资源受限的场景下可以在 ARP 模块里直接做成静态查找表牺牲一点灵活性换取更低的 LUT 占用。5.3 一个可能被忽略的余量用户逻辑的时序裕量100G 速率下时序裕量是一个不可忽视的问题。综合之后Setup 的最差负裕量WNS我做到了 0.045nsHold 的裕量在 0.02ns 以上这个结果看起来是过了但实际在高温、低电压的极端条件下可能会变得很危险。我建议做两个额外处理来增加余量所有跨时钟域信号一律使用两级同步器不要有任何例外。对于数据通路较长的逻辑在中间插入流水寄存器不要指望一个时钟周期内完成太多组合逻辑。这两个做法换来的代价是增加几个周期的固定延迟但换来的稳定性提升是巨大的。100G 的物理层链路一旦因为时序问题出现亚稳态错误丢包往往会伴随 CRC 错误一起出现排查起来特别头疼。6. 移植过程中最花时间的几个坑位总结如果让我把这些天的时间做一个分配真正写代码和改代码的时间大概只占三分之一剩下三分之二全部耗在各种“看起来不合常理”的坑上面。挑几个最有价值、最容易复现的记录下来希望你能绕开。6.1 复位顺序与 GTY 初始化时间的匹配GTY 收发器从上电到完全初始化是一个较长的过程涉及多个阶段时钟稳定、PLL 锁定、TX/RX 复位释放、对齐状态机收敛。如果在这个过程尚未完成时就释放用户逻辑的复位后续操作可能出现不可预知的错误状态。我最初的代码是在复位引脚拉高后立刻开始发送 ARP 请求结果发现链路刚建立时会有零星的坏包。后来我在复位逻辑里加入了延时用 GTY 的 txresetdone 和 rxresetdone 来触发一个计数器等计数器数到某个指定值后再释放协议栈复位。这样改完以后链路建立阶段的错误包消失了。这个细节在文档里一般不会专门写但上板测试中很容易暴露出来。6.2 上位机网卡与驱动的选择对测试结果的影响100G 测试能否跑满不只看 FPGA 端上位机的网卡和驱动同样关键。我在最开始测试时用的是某款国产 100G 网卡结果在同样一段代码下只能在 40G 附近徘徊怎么调都不行。后来换成了 Intel E810 系列的网卡配合官方最新驱动带宽瞬间就上去了。这个差异的主要源头在于网卡对多队列的支持以及驱动层面的中断优化。UDP 大流量场景下如果网卡的 RSS 哈希分布不均衡或者中断合并参数不合理很容易出现个别 CPU 核心被打满而其他核心闲置的情况。建议在测试前把网卡的中断合并调节到针对小包优化的参数并且在 iperf3 命令里用-u -l 1400显式指定 UDP payload 的大小这样测出来的数据更具参考性。6.3 光模块和线缆的选型不要省钱这句话看起来像废话但实际经历过才知道什么叫“一分钱一分货”。100G 光模块市场上价格差异巨大便宜的模块可能会出现莫名其妙的误码和断链问题而且不是立刻暴露往往是连续满载跑几个小时之后才出现一次。这种偶发性故障比持续性的问题更难排查因为它不会在你的短时测试里出现只有在长时间的稳定性测试里才会被暴露。我最终用的是某大厂的 QSFP-DD 模块配合配套的 AOC 线缆连续跑 48 小时没有出现过一次误码。所以强烈建议在预算允许范围内选择一线品牌的产品把精力花在协议栈和业务逻辑上而不是跟物理层较劲。7. 后续可以继续扩展的方向这次移植只是一个起点实际上还有很多可以深化和扩展的方向。如果你已经按照上面的步骤在自己板卡上把 100G UDP 跑通了下面几个方向是我觉得后续价值比较大的。7.1 在 UDP 之上叠加 RDMA 或其他高速传输层协议UDP 本身是不可靠的在 100G 这类高速率场景下如果业务需要可靠的传输就需要在 UDP 之上再实现一层协议比如 RoCEv2 或者在应用层自己管理重传和排序。verilog-ethernet 工程本身不提供 RDMA 支持但它的架构预留了这种可能性你可以在 UDP 的 payload 里定义自己的传输头硬件只负责数据搬运可靠性和流量控制放到软件层面。不过这里要提醒一下RDMA 涉及到的拥塞控制、流控、以及 completion queue 管理非常复杂需要投入的时间成倍增加。如果你只是想要可靠传输花时间在握手和重传机制的设计上可能会比硬啃 RDMA 更现实。7.2 加入多端口聚合与 MAC 地址过滤通过 GTY 多通道组合一个 FPGA 完全可以实现多个 100G 端口的并行处理。如果你有三端口以上的交换需求可以在顶层例化多个协议栈并通过一个简单的 round-robin 调度器在多个端口之间分发数据。这样做的资源开销会线性增加但只要 LUT/BRAM 够用是完全可以做到的。MAC 地址过滤也可以在 eth_mac 层做避免不相关的广播帧干扰上层业务。7.3 从纯逻辑 UDP 升级到带 CPU 的 SoC 方案如果你希望在 FPGA 上同时跑 Linux 和 UDP 协议栈可以考虑把 MicroBlaze 或硬核 ARM 处理器接入 AXI 总线让 CPU 处理控制面比如动态配置 IP 地址、管理 ARP 表、显示统计信息数据面仍然由纯逻辑协议栈承担。这个方案的灵活性比纯逻辑版本高很多适合产品化的场景。我在自己的板卡上跑通了这种架构CPU 只负责软件层面的管理任务通过网络 CPU 端口把需要处理的请求转给协议栈这个方案的 CPU 占用率极低不到 5%。但代价是需要额外解决 CPU 和 FPGA 逻辑之间的地址映射与中断机制设计复杂度会有所上升。8. 关于“会用”和“用得稳”之间的最后一点体会说到底把一个开源项目移植到自己的 FPGA 上这件事最核心的收获并不是“能跑通”这个结果而是在整个过程中建立起来的调试思路和系统级理解。你会在排查问题的过程中发现自己不仅了解了 UDP 协议栈的内部机制还顺带把 GTY 收发器的初始化流程、AXI-Stream 握手的细节、以及时钟域划分的基本原则都过了一遍。我个人的体会是100G 这种高速率场景下任何一个“看起来没问题”的细节在最坏情况下都可能导致整条链路不稳定。不要因为仿真通过就掉以轻心更不要觉得上板能点亮几个 LED 就万事大吉。一定要抱着“在上板之前把所有能在仿真阶段验证的都验证掉在上板之后用长时满载测试把所有能暴露的问题都暴露出来”的心态去做这件事。最后再分享一个实用的小技巧在上板调试阶段一定要在协议栈的关键节点都拉出 ILA 探针不要只在最终数据出口接一个。当数据出现问题的时候你能在 ILA 波形里快速定位是发送侧的问题还是接收侧的问题这能省下数不清的排查时间。我就是因为在 UDP 接收解析模块和发送组装模块各加了一个 ILA才在最终的数据跳变问题中快速锁定了 tkeep 这条链路而不是在几十个信号里慢慢猜。