开源100G FPGA UDP协议栈移植上板测试实战
1. 内容整体设计与思路拆解做 FPGA 网络通信的人应该都有过这种经历在 PC 上写过的 UDP 程序、调通的 TCP 协议栈到了 FPGA 世界里一切都得重新“翻译”成硬件逻辑。尤其是当带宽从千兆跳到 100G 的时候很多软件年代形成的惯性思维会立刻失效。这次做的“开源 100G FPGA UDP 移植上板测试”就是把一套开源的高性能 UDP 协议栈从仿真环境搬到真实板卡上用光模块和 PC 端 iperf3 实测打流验证链路、带宽和稳定性。整个过程踩了不少坑这里把思路、步骤和排查过程完整记录下来。先说为什么要选 UDP 而不是 TCP。TCP 有状态机、滑动窗口、拥塞控制、重传机制这些在 FPGA 里实现并不是不可能但复杂度会直线上升。UDP 无连接、无状态、处理逻辑简单配合硬件逻辑可以实现线速收发这也是很多高速网络测量、数据采集、AI 推理集群中节点间通信选择 UDP 的原因。100G 场景下UDP 的“简单”反而是最大优势因为数据面吞吐是硬指标控制面交给 CPU 去管就好。1.1 为什么选“移植”而不是“从零写”这次项目其实有个关键背景不是从零写协议栈而是把一套在别的板卡或仿真环境里已经跑通的开源 UDP IP 核移植到我们手头的 FPGA 板卡上。听起来好像轻松不少实际动手后才发现移植的难点根本不在于“IP 核怎么工作”而在于“IP 核和我的板卡怎么对接”。开源 UDP 协议栈和真实硬件之间隔着好几层MAC 层可能用 Xilinx 的 100G CMAC 硬核也可能用第三方软核MAC 外面的 PHY 可能是光模块可能是 DAC 线缆还可能是 AoC 光纤。协议栈输出的 AXI4-Stream 接口要和这些硬核的信号命名、数据位宽、时钟频率、复位时序一一对上中间任何一个环节不匹配上板后就是黑屏——不对是“黑链”。我这次板卡用的是 Xilinx UltraScale 系列的 FPGA带 GTY 高速收发器支持 100G 以太网。100G 的实现方式通常有两种4×25G 和 1×100G PAM4。前者用四路 GTY后者用硬核 PCS/PMA 加 PAM4 调制。我们选的是 4×25G 方案四个通道独立收发在 MAC 层汇聚成 100G 逻辑链路。这样做的好处是 GTY 的参考时钟可以共用布线压力小而且 25G 单通道的调试难度远低于 100G PAM4。1.2 协议栈选型与功能边界开源的 FPGA UDP 协议栈不少但大多数只支持 10G 以下速率。100G 级别的开源方案相对少而且质量参差不齐。选型的时候重点看这几个方面是否原生支持 AXI4-Stream 接口数据位宽能否直接对接 512bit这是 100G 场景下最常见的位宽。是否自带 MAC 层控制比如 PFC、pause 帧处理还是只负责 IP/UDP 封装。ARP 处理是怎么做的有没有独立的响应模块。CRC32 校验是软件还是硬件逻辑完成100G 线速下绝对不能用软件代替。我们最终选定的是一个以 AXI4-Stream 为核心的轻量级 UDP 协议栈数据通路干净寄存器配置简单源码可读性也还行。它把 ARP 响应、IP 校验和、UDP 校验和、以太网帧封装都放在了硬件里完成用户只需要关心 payload 的收发和配置管理寄存器。2. 核心细节解析与实操要点移植的第一步是搞清楚协议栈的内部架构。很多开源 IP 核的问题不是功能不行而是文档太少只能靠读代码理解。我花了一天时间把数据通路的关键模块画了一遍才敢动手改。2.1 协议栈的数据通路架构从接收方向看数据路径是这样的光模块 → GTY 高速收发器 → 100G MACPCS/PMA → AXI4-Stream 数据 → UDP/IP 接收引擎 → 用户 FIFO → 用户逻辑。发送方向刚好反过来用户逻辑 → 用户 FIFO → UDP/IP 发送引擎 → AXI4-Stream → 100G MAC → GTY → 光模块。这里面最容易出问题的是“数据位宽”和“时钟频率”的匹配。100G 以太网在 512bit 数据位宽下用户时钟是 322.265625MHz100G/512bit考虑 64b/66b 编码开销后实际速率略有不同。这个频率在 UltraScale 上完全跑得动但如果协议栈默认的位宽是 256bit那时钟就得翻倍到 644MHz时序收敛的难度会急剧上升。我这次移植时第一件事就是把数据通路全部统一成 512bit然后配套调整 FIFO 的读写位宽和所有握手信号。这个改动涉及的地方很多帧起始/结束信号的位宽标记、字节使能信号的处理、CRC 的输入位宽。哪一个没改对上板后就会出现“看起来通了、其实发出去的全是错包”的情况。2.2 时钟复位域的处理100G 协议栈的另一个坑在时钟和复位。以太网 PCS 层恢复出来的时钟和用户逻辑时钟严格来说不是同一个时钟域中间必须有异步 FIFO 或者同步逻辑做缓冲。很多开源代码默认“所有逻辑都跑在同一个时钟下”这在仿真里完全没问题因为仿真器里一切都是理想的。但真实芯片里跨时钟域的处理不到位就会出现偶发的数据丢失、状态机卡死。我移植时的做法是把协议栈的数据通路时钟user_clk和 MAC 接口时钟mac_clk完全分开中间用 Xilinx 的原语比如 XPM_FIFO 异步 FIFO做缓冲。状态机则统一跑在 user_clk 下避免多个状态机在不同时钟域里互相咬合。复位这块也值得单独说。100G MAC 硬核的复位时序是有讲究的复位释放后需要等待 PCS 完成对齐、链路训练通过后数据通路才能开始收发。如果复位逻辑只是简单拉低然后放开很可能会在 MAC 还没准备好的时候就开始发包前几个包直接丢掉而且很难排查。2.3 AXI4-Stream 接口的关键细节AXI4-Stream 看似简单tvalid、tready、tdata、tkeep、tlast、tuser。但 100G 场景下这些信号的时序约束比 10G 严格得多。首先是 tkeep 信号它在 512bit 位宽下是一个 64bit 的向量每一位对应一个字节的有效性。帧尾最后一个周期tkeep 的值决定了这个包的实际长度处理不好就会出现尾包多传几个字节或者少传几个字节的问题。其次tuser 信号在不同模块里代表的意义可能不一样。有些协议栈把 tuser 当作错误标记比如 CRC 错误、帧长度错误有些则当作元数据比如端口号、时间戳。移植的时候要仔细看源码否则很容易把错误标记当成有效数据传给用户逻辑。还有一个经常被忽略的细节是“帧间隔”的处理。以太网规定了帧与帧之间的最小间隔IFGCSMA/CD 时代是 96bit time千兆和万兆以太网也保留了这个概念。100G 场景下MAC 层会自动插入 IFG但如果用户逻辑一直不间断地往 FIFO 里塞数据MAC 层的 FIFO 可能会溢出造成丢包。这个问题的解决方法是在用户逻辑侧做一个简单的流控FIFO 满的时候暂停发送。3. 实操过程与核心环节实现理论说得再多不如动手做一遍。下面直接进入实操记录从工程结构、生成约束、Testbench 仿真到上板调试一条线走下来。3.1 工程结构与文件组织我习惯按功能模块划分目录结构这次工程基本是这样的fpga_udp_100g/ ├── src/ # RTL 源码 │ ├── udp_stack/ # 开源 UDP 协议栈核心代码 │ ├── mac/ # 100G MAC 封装和适配 │ ├── phy/ # GTY 收发器配置 │ ├── user_logic/ # 用户测试逻辑回环、计数器 │ └── top/ # 顶层文件例化所有模块并连线 ├── ip/ # Vivado IP 核CMAC、FIFO、时钟 ├── constraints/ # XDC 时序约束和引脚约束 ├── sim/ # Testbench 仿真文件 └── scripts/ # 编译、运行、抓取脚本UDP 协议栈核心代码基本不用改动原样拿过来。真正需要写的是 mac 适配层、phy 配置和顶层连线。3.2 MAC 层适配信号对接的繁琐工程100G MAC 这块Xilinx 官方提供了 CMAC IP 核支持 100G 以太网标准。CMAC 提供了 AXI4-Stream 接口和 GT 接口理论上我们只需要把 UDP 协议栈的 AXI4-Stream 和 CMAC 的 AXI4-Stream 对接起来就行。但实际对接的时候还是要处理三个问题。第一个是数据位宽。CMAC 的 AXI4-Stream 数据位宽默认是 512bit协议栈如果也是 512bit直接连就行。但 CMAC 还提供了 32bit 和 64bit 的调试接口如果选错了后面所有逻辑都白写。我们统一用 512bit。第二个是时钟。CMAC 输出的用户时钟是 322.265625MHz这个时钟同时也是我们协议栈的主时钟。要注意CMAC 还有一个独立的 drclk调试时钟一般用 100MHz 或 125MHz用来配置 CMAC 寄存器。第三个是 tkeep 和 tlast 的处理。CMAC 对 tkeep 的要求是帧内所有周期都必须全 1只有帧尾周期可以部分有效。如果协议栈在帧中间出现部分有效的 tkeepCMAC 直接报错。开源协议栈很多时候没有严格遵循这个要求需要在适配层做一次约束。我在适配层加了一个简单的状态机发送时检测 tlast如果 tlast 拉低的周期 tkeep 不全为 1就填充字节使能到全 1接收时则根据 tkeep 和 tlast 重新计算帧长传给上层。3.3 GTY 收发器和参考时钟100G 的四种通道4×25G对参考时钟的要求很高抖动性能直接影响误码率。我们用的是 156.25MHz 参考时钟通过一个低抖动时钟芯片分发给四路 GTY。GTY 的配置在 Vivado 里可以用 Transceiver Wizard 自动生成主要配置项包括线速率25.78125Gbps、协议模板选择 Ethernet、参考时钟频率等。上电后GTY 需要完成 CDR时钟数据恢复锁定和 PCS 对齐这部分状态可以从 GTY 的驱动状态寄存器读出来。调试时常用的手段是先用 IBERT 测试眼图确认物理层没问题再上协议栈。IBERT 是 Xilinx 自带的误码率测试工具单独做一个工程就能用建议上板前先跑一遍确认光模块和 GTY 之间的信号完整性合格。3.4 约束文件编写时序收敛是硬门槛100G 设计在时序收敛上的压力比 10G 大了一个数量级。不能说“逻辑能跑通就行”时序不过跑一段时间就出错而且错得毫无规律。关键的 XDC 约束包括# 主时钟约束322.265625MHz create_clock -name user_clk -period 3.103 [get_pins cmac_inst/inst/gt_usr_clk] # 参考时钟156.25MHz create_clock -name refclk -period 6.4 [get_ports refclk_p] # 异步 fifo 的跨时钟域约束 set_clock_groups -asynchronous -group [get_clocks user_clk] -group [get_clocks mac_clk]这里容易忽略的一个点是CMAC 输出的用户时钟不要直接当作用户逻辑的主时钟中间最好加一层 BUFG/BUFGCE或者用全局时钟资源。我当时图省事直接把 GT 的 clock 连到内部逻辑结果时序报告一片红后来改加 BUFG 后瞬间好了。3.5 Testbench 仿真验证上板之前仿真一定要跑通不然上板后问题定位会非常痛苦。我写了两套 Testbench一套是纯粹的协议栈环回测试构造一个 UDP 包从发送通道进入协议栈再从接收通道出来比对接收到的数据和发送的数据是否一致。这套测试主要验证协议栈内部逻辑的正确性。另一套是带 MAC 层的仿真把 CMAC 换成行为模型让协议栈和 CMAC 行为模型对接发送几个包再看接收端能不能解出来。同时检查 tkeep、tlast、tuser 的信号时序是否符合预期。仿真中发现的最典型的一个问题协议栈在发送端对 ARP 请求的处理是“收到 ARP 请求后立即回复”但如果 ARP 请求和正常 UDP 数据包同时到达协议栈内部会出现冲突导致数据包被丢弃。这个问题的原因是协议栈内部其实是一个单通道的状态机收发共用一个内部总线仿真里能复现是因为时序太理想把冲突暴露得很明显。解决方法是调整状态机优先级ARP 响应只在空闲时执行一旦检测到发送通道有数据包就推迟 ARP 响应。这个改动在仿真里验证通过后上板后的丢包率立刻降下来了。3.6 上板测试从灯亮到跑流量仿真通过后才是真正的开始。上板测试我分了三步走。第一步是基础链路测试不加载协议栈只让 GTY 和 CMAC 工作用 PC 端的光模块网卡和板卡直连在 PC 端用 ethtool 查看网卡是否识别到 100G 链路。这一步能确认物理层没问题。如果链路没起来大概率是光模块兼容性、参考时钟配置或者 GTY 参数的问题。第二步是 ARP 测试加载协议栈PC 端 ping 板卡的 IP 地址。如果 ping 通说明以太网层和 IP 层是通的。如果 ping 不通抓包看板卡是否发出了 ARP 响应没发出就查协议栈的 ARP 逻辑发出了但 PC 收不到就查 MAC 层和物理层。第三步是 UDP 打流测试PC 端用 iperf3 打 UDP 流板卡端接收后回传再用 iperf3 测带宽。这一步能暴露真问题——丢包、带宽不达标、时序不稳全在这里体现。我这次遇到的第一个问题就是 ping 不通。用 Wireshark 在 PC 端抓包发现板卡根本没回 ARP 响应。查了半天发现是 MAC 适配层有个信号没接对CMAC 的接收端 tuser 信号里带有错误标记协议栈接收引擎判断到这个标记就自动丢包导致 ARP 请求被误丢。修正后 ping 通了。4. 常见问题与排查技巧实录到这里移植和上板的主力工作基本完成。接下来把实际操作中遇到的典型问题和排查方式整理成一份速查表每个问题都是我踩过坑之后总结出来的。现象可能原因排查方法解决方案链路无法建立ethtool 显示无 link光模块兼容性差、参考时钟不稳、GTY 参数错误用 IBERT 测试眼图和误码率换模块、检查时钟芯片配置、重新生成 GTY 配置Link 正常但 ping 不通ARP 响应逻辑异常、MAC 地址错误、IP 地址错误Wireshark 抓包确认是否有 ARP 请求和响应检查协议栈 ARP 模块是否被错误丢弃、核对静态 MAC/IP 配置小包通、大包丢FIFO 溢出、链路层帧间隔处理不当逐步增大包长测试确认丢包阈值增加流控防止用户逻辑在 FIFO 满时继续写带宽远低于 100G数据通路位宽不匹配、时钟频率不对、IFG 过长用计数器统计每秒处理包数确保数据位宽和时钟匹配FIFO 位宽按需调整偶发丢包但不稳定跨时钟域问题、异步复位释放问题长时间 iperf3 打流观察丢包率检查时钟域转换逻辑统一复位管理时序不收敛逻辑层数过深、布局布线压力大查看时序报告中的关键路径流水线插入寄存器、优化优先级编码器、调整布局约束上板后CRC校验错误MAC 层 tkeep 处理错误、GT 通道间 skew 过大用 CMAC 自带的错误计数器排查修正 tkeep 逻辑检查 GT 通道是否都完成对齐4.1 带宽达不到 100G 的深层原因很多人在“ping 通了”之后就以为大功告成结果 iperf3 打流发现带宽只有 60G 甚至更低就开始怀疑协议栈能力不行。其实大部分时候都是应用层没有按协议栈的特点来写。我当时遇到的一个问题是板卡端接收到的包全部正常但回传的时候速率上不去。查到最后发现我为了方便直接在用户逻辑里做了一个“收多少回多少”的环路但这个环路没有考虑 100G 链路上的背压机制。发送端 FIFO 满了之后接收端还在往 FIFO 里塞数据就被丢弃了。这其实反映了一个常见误区100G 场景下任何一端的读写速率不匹配都会直接表现为丢包而丢包的表现又往往是“时间越长丢得越多”很容易让人误以为是协议栈稳定性问题。另一个影响带宽的因素是包长。100G 线速下单条链路的包处理速率是固定的。包长越小每秒钟要处理的包数就越多达到线速的难度就越大。如果 PC 端 iperf3 打的是 64 字节小包丢包率会显著上升这不是协议栈的问题而是所有网络设备的物理规律。测试 100G 的时候建议先用 1024 字节或 1518 字节的包跑通链路再去挑战小包场景。4.2 iperf3 打流和 Wireshark 抓包的配合上板测试离不开 PC 端的工具配合。我这里用的 PC 是一台装有 100G 网卡的 Linux 服务器iperf3 做带宽测试Wireshark 做报文分析两者配合基本上能定位绝大多数问题。iperf3 打 UDP 流的命令大概是这样的# PC 端作为接收端监听 5001 端口 iperf3 -s -i 1 # 板卡端发流到 PC 端带宽设 90G包长 1024 字节 # 注意 UDP 打流需要指定带宽iperf3 默认按 TCP 测 iperf3 -c 192.168.1.10 -u -b 90G -l 1024 -t 30这里有个坑iperf3 的带宽限制参数-b在 UDP 模式下是必须指定的如果不指定iperf3 默认只会打很小的流量根本测不出 100G 的带宽。另外 iperf3 的 UDP 模式测出来的是单向带宽要测双向得自己开两个进程。Wireshark 抓包的时候如果带宽很高抓包文件会非常大建议用过滤条件只保留需要的协议udp || arp同时可以把 Wireshark 的“统计 → 协议层次”功能打开快速看各类报文的数量分布。如果看到大量 ARP 请求重传多半是 ARP 响应有问题如果看到 CRC 错误Wireshark 会标记为 bad checksum说明物理层或 MAC 层有问题。5. 时序调优与稳定性验证如果链路通了、带宽也达标了剩下的问题就是“能不能稳定跑”。100G 的高速率意味着任何一点小毛病都会在高负载下放大稳定性验证是移植工作的最后一环也是我最看重的一环。5.1 时序收敛的进一步优化在整条链路上协议栈的数据通路是相对简单的真正的时序瓶颈往往出现在 FIFO 的读写控制和用户逻辑的状态机上。如果综合后时序只剩一点点余量跑一两个小时可能没问题但跑到一天就可能出一次错。对于这种场景我常用的优化手法有三个第一关键路径多插流水寄存器用面积换时序。比如状态机的状态转移判断、FIFO 的空满信号判断都可以在中间加一拍延迟换取更好的 fmax。第二大位宽数据总线尽量做位宽转换的时候不要用组合逻辑全部用寄存器输出。512bit 的 tdata 经过任何组合逻辑时序都会非常紧张直接用寄存器打一拍最省心。第三把需要高频率的模块单独出来用独立的时钟域跑。比如协议栈内部的时间戳逻辑不需要跑 322MHz可以用低速时钟通过异步 FIFO 同步回主时钟域。5.2 长时间打流与误码观察稳定性验证阶段我一般会连续打流 24 小时以上同时监控几个关键指标接收端收到的包总数、CRC 错误计数、丢包率、GT 通道的误码计数。任何一项指标不正常增长都得停下来排查。我这边实测下来纯数据通路连续打流 24 小时丢包率可以做到 10 的负 12 次方以下基本等同于链路无误码。但有一次发现 GT 通道的误码计数在持续缓慢增长排查了半天最后发现是参考时钟芯片的电源纹波偏大导致 CDR 抖动超标。换了更干净的电源轨后误码计数停止增长。这不仅是一个技术问题的解决更是一个提醒100G 链路上很多问题不是逻辑写错了而是物理层面的质量问题。电源、时钟、PCB 走线、光模块质量任何一个环节都可能成为瓶颈。调试过程中板子的电源纹波、参考时钟的相位噪声、光模块的温度都应该纳入常规监控范围。5.3 移植后的扩展思考100G UDP 协议栈跑通之后往后的扩展方向其实非常清晰。最直接的是对接 DMA 引擎——用户逻辑和 PC 之间不通过网线而是通过 PCIe 把数据送到上位机上位机再用标准 socket 收发 UPD。这个架构在很多数据采集、AI 推理场景里很常见。其次是把协议栈往 RoCEv2 方向扩展。RoCEv2 是在 UDP 之上封装了 IB 传输层报文实现 RDMA 功能。如果 UDP 协议栈做得够干净在此基础上增加 GRHGlobal Routing Header和 BTHBase Transport Header的处理逻辑就能逐步往 RDMA 靠近。当然这个工程量大得多但底层 UDP 链路先跑通是迈出第一步的基础。另外时间敏感网络TSN也是一个值得深入的方向。FPGA 相比 CPU 的一大优势就是低延迟和确定性TSN 里的时间同步、流量整形等功能FPGA 都能做到很高的精度。UDP 协议栈作为网络数据面的基础结合 TSN 的一些特性未来在工业控制、车载网络里会很有用。写在最后的一点心得这次移植上板测试从开始动手到稳定跑通差不多花了两周时间。回头来看最大的感悟有两点。第一仿真永远替代不了上板。仿真环境里一切信号都是理想化的时钟没有抖动复位没有毛刺信号没有竞争。只有上了板子你才会发现那些“理论上不可能发生”的事情在一瞬间真实地发生了。第二开源代码不是拿来就能用需要对它做深度的“适配”而不是“直连”。理解它内部的时序约定、接口定义、状态机流转比直接把端口连上去重要得多。遇到问题时先读代码再怀疑硬件这个顺序能省下很多时间。最后一个小小的建议如果你也是第一次做 100G FPGA 网络项目建议先把物理层调试工具 IBERT 用熟练然后从 10G 或 25G 的协议栈开始练手再多通道汇聚到 100G。一口吃不成胖子100G 的坑比 25G 多得多一步步来反而更快。

相关新闻

最新新闻

日新闻

周新闻

月新闻