尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

FPGA 100G UDP开源协议栈移植实战与避坑指南

FPGA 100G UDP开源协议栈移植实战与避坑指南 做 100G 的 FPGA UDP 移植说难不难说简单也真不简单。我这次是从 GitHub 上拉了一个开源的 UDP 协议栈工程目标是把它从原版适配到手里的板卡上然后上板、跑通、打流。整个过程折腾了大概一周中间踩了不少坑今天把完整过程整理出来给同样在做高速网络方向的朋友一个参考。这个内容适合几类人准备在 FPGA 上做 100G UDP 收发、但不想从头写 MAC 和协议栈的工程师手里有一个开源工程却不知道从哪下手改的初学者以及已经在移植、但对 GT 参考时钟、PCS 锁定、iperf3 打流这些环节还有疑问的开发者。下面直接进入正题。1. 这个开源工程到底能帮我们省多少事1.1 我用的开源框架与选型理由现在 GitHub 上做 FPGA UDP 的开源项目并不少但成熟度差别很大。我这次选的是基于 alexforencich/verilog-ethernet 思路改造的工程原版里面已经带了 10G/25G/100G 的 Ethernet MAC、UDP 卸载引擎、ARP、ICMP 处理以及完整的 AXI4-Stream 接口示例。这类工程最大的价值在于它把以太网帧的组装、拆解、CRC、PCS/PMA 适配这些脏活都处理好了你只需要关心用户侧的数据通路。选型的时候我也对比过 Corundum 这类更完整的开源网卡方案。Corundum 确实很强大自带 PCIe DMA、多队列、完整驱动但它本质是一个网卡 SoC对硬件平台要求高移植成本也大。如果只是验证 100G UDP 协议栈而不是做一块完整的智能网卡直接用轻量级的 MAC UDP offload 方案更省时间。我的选型逻辑很简单先看原工程跑的板卡资源和我手里的板卡差异有多大再看需要的业务功能是否齐全最后才决定要不要上重型方案。1.2 UDP 协议栈为什么适合在 FPGA 上做聊移植之前先想明白一个问题为什么 100G 这个量级大家都优先搞 UDP而不是 TCP因为 TCP 的状态机太复杂了滑动窗口、乱序重排、拥塞控制、连接管理这些逻辑在 CPU 上跑很灵活在 FPGA 上写也不是不能做但光是把 SACK 和重传定时器做好代码量就比 UDP 多一个数量级。UDP 就简单得多UDP 头部就 8 个字节加上 IP 头部 20 字节整个协议处理可以做成一个相对固定的流水线。FPGA 是典型的并行硬件做 UDP 这种查表 组包 校验的固定逻辑天然比处理器有优势。尤其 100G 线速下理论上一秒钟要处理大约 1400 万个小包纯靠 CPU 中断加协议栈很难顶住FPGA 用流水线可以稳定处理。另外现在做高速 FPGA UDP 的工程大多只覆盖 IPv4 UDPIPv6 支持都很少。这一点移植前要有预期别拿 IPv6 的包来测协议栈里根本没做那个分支。1.3 移植前心里要有的预期移植这种开源工程最大的误区是以为HDB 里改个 FPGA 型号就行。实际改动量取决于三块物理层GT 收发器 / PCS/PMA IP、引脚约束XDC、用户侧数据通路。物理层是最容易出问题的因为不同板卡的高速参考时钟、GT 类型、光模块连接方式都可能不同。引脚约束属于累了点但出错概率可控。用户侧数据通路则要仔细看原工程的接口风格是 64bit 还是 512bit反压怎么处理直接决定你后续加业务逻辑的工作量。我给自己定的目标很简单先把开源工程自带的 UDP 回环跑通再用 iperf3 做 UDP 打流验证线速和稳定性。这个目标听起来不大但把 GT、时钟、复位、AXI-Stream 这些环节全部捋顺信息量已经很大了。2. 移植第一步把原工程和目标板的差异清单列出来2.1 从原工程里剥离和芯片强相关的部分拿到开源工程之后别急着 compile。先看目录结构把代码分成两类和芯片强相关的、和芯片无关的。和芯片强相关的通常是这些GT wrapper也就是高速收发器的例化层里面全部是原厂原语的例化PCS/PMA IP 核的生成文件比如 Xilinx 的 100G Ethernet PCS/PMA、CMAC 硬核时钟管理模块MMCM/PLL 的例化引脚约束文件XDC 或 SDC。和芯片无关的是UDP 收发引擎、ARP、ICMP 处理逻辑AXI4-Stream FIFO、跨时钟域处理用户业务逻辑比如包计数、回环控制、payload 生成。我建议动手改之前先把上面这些文件列一个清单出来标注保留 / 修改 / 删除。我这次的原工程是基于 Xilinx UltraScale 架构写的手里板卡也是 UltraScale所以 GT wrapper 和 IP 核还有部分参考价值。如果你的目标板是国产 FPGA那 GT wrapper 必须整个重写工作量会明显大很多。2.2 板卡资源与引脚约束核对这个环节我吃了不少亏当时差点把一块板子搞到没法用。第一步是查原理图确认 QSFP28 光口接的是哪个 Bank、哪几对高速收发器引脚以及参考时钟从哪进来。100G 以太网用的是 4 个 25G 通道也就是说需要 4 组 TX/RX 高速串行通道再加上一组给 GT 用的参考时钟。参考时钟是重灾区。不同板卡上给 QSFP28 的 MGTREFCLK 频点可能不一样常见的是 156.25MHz也有 161.1328125MHz 的版本。这个频点必须和 IP 核里的配置匹配配错了最典型的症状就是 PCS 永远无法 lock、链路起不来。还有的板卡把 GT 参考时钟引到了别的高速串行接口上复用原理图里看不仔细就会接错。引脚约束我建议这样列set_property PACKAGE_PIN AK4 [get_ports clk_ref_clk_p] set_property PACKAGE_PIN AL4 [get_ports clk_ref_clk_n] set_property PACKAGE_PIN AM21 [get_ports qsfp_tx_p[0]] set_property PACKAGE_PIN AN21 [get_ports qsfp_tx_n[0]] ...注意差分引脚只约束 P 端N 端会自动匹配。同时要给这些端口加上 IOSTANDARD通常高速 GT 引脚会由工具自动推断但参考时钟这种普通差分引脚必须显式写上不然后面跑 Implementation 会报一堆时序问题。2.3 接口位宽、时钟、复位设计开源工程的 UDP 引擎比如 udp_complete_64、udp_64_rx 这些接口大多是 64bit AXI4-Stream。而 100G 的硬核 MAC比如 Xilinx CMAC用户侧接口通常给到 512bit时钟大概在 322MHz 量级。这就出现一个位宽匹配问题不能粗暴地把 512bit 总线接到 64bit 总线上。我这次的做法是加了一层适配逻辑把 100G MAC 的 512bit 数据流拆成多路 64bit 通道再分别送给 UDP 引擎。因为 100G 物理层本质上是 4 个 25G 通道把数据流按照通道维度拆分逻辑上很自然每个 64bit 通道都能保持独立逻辑。这样做的代价是顶层例化要多几个 UDP 引擎实例但避免了用 FIFO 降速导致吞吐上不去的问题。时钟和复位也要单独交代清楚。GT 恢复时钟、用户逻辑时钟、MAC 接口时钟之间必须处理好跨时钟域不要想当然共用一个 clk。复位问题上很多开源代码是异步复位、同步释放但 IP 核的复位时序要求更严格我建议按GT 复位 → PCS 复位 → MAC 复位 → UDP 引擎复位这个顺序依次释放省得后面调试的时候莫名奇妙。3. 实操替换 GT/100G MAC、接上 UDP 引擎、加计数器3.1 以 Xilinx 100G 软核为例重做物理层我这次工程里用的还是 Xilinx 的 100G Ethernet PCS/PMA 软核。如果你的板卡是带 CMAC 硬核的 UltraScale也可以直接用 100G Ethernet Subsystem流程类似只是接口位宽和时钟会有差异。重新生成物理层 IP 的时候有几个参数必须和原工程保持一致或者根据目标板卡做出明确调整线速率100GBase-R 是 4 × 25.78125G参考时钟频率与板卡原理图一致用户接口数据位宽我选的和 MAC 匹配的 512bit是否使能 CRC / 流量控制 / 1588 等选项为了简单我全关了只保留最基础的收发通路。IP 生成好之后替换工程里的 gt_wrapper 和 mac 相关例化。这个环节不要自己手写 GT 原生代码除非你有充分的理由。用原厂 IP 的好处是各种校准、复位时序、状态指示都现成出问题可以用 ILA 直接抓内部信号。3.2 把 64bit 的 UDP 引擎接到 512bit MAC 接口上这是整个移植中逻辑上最费心思的一块。我一开始也想简单拿一个异步 FIFO 把 512bit 转成 64bit然后再接 UDP 引擎。结果打流测试时发现UDP 接收在一个通道上能跑满但整体吞吐上不去因为 FIFO 成了瓶颈。100G 的有效数据速率约 100Gbps64bit 接口要跑到 1.56GHz 才能撑住这明显不现实。正确思路是做通道拆分。我参考以太网本身的通道化特性把 512bit MAC 数据按照偏移拆成 4 路 64bit 数据流。每一路有自己的 AXI-Stream 信号独立做 UDP 接收和发送。这样每个 UDP 引擎只需要处理约 25Gbps 的速率64bit 接口在 390MHz 左右就能满足时序压力小很多。这里要特别注意 AXI-Stream 的 tkeep 和 tlast 处理。拆通道之后帧头帧尾的分布比较复杂尤其是短包可能只出现在某一个通道上。如果 tkeep/tlast 处理不对会出现UDP 引擎收到半包这种诡异现象。我在代码里专门加了一个对齐模块负责把通道上出现的完整以太网帧重组以后再做 MAC 地址和 IP 地址过滤。3.3 约束文件与时钟约束示例做完逻辑以后XDC 约束直接影响能不能跑进时序。除了引脚约束时钟约束必须写对。比如参考时钟约束create_clock -name gt_ref_clk -period 6.4 [get_ports clk_ref_clk_p] set_input_delay -clock gt_ref_clk -min 0.5 [get_ports clk_ref_clk_p] set_input_delay -clock gt_ref_clk -max 1.0 [get_ports clk_ref_clk_p]用户逻辑时钟一般由 IP 核的输出时钟驱动时序工具会根据 IP 内部关系自动约束但如果你在适配层里新建了一个 PLL/MMCM一定要把这个时钟也 create_clock 一下并且确认和 MAC 侧时钟的相位关系。还有一个坑是 false path。GT 通道上的同步信号、状态寄存器采集信号很多可以按异步处理但不要图省事把所有跨时钟域信号都设 false path。正确的做法是数据总线通过异步 FIFO 同步控制信号用打拍或握手复位信号做同步释放只有确定不参与时序收敛的信号才设 false path。3.4 编译与时序收敛注意事项我编译过程中遇到的主要问题是物理层 IP 例化端口和原工程不一致还有部分 GT 原语在目标芯片上没有物理位置。这种问题看综合日志很好定位关键词搜 ERROR、[Place 30-6xx] 之类。时序收敛方面100G 逻辑的主频虽然比 CPU 低但数据总线宽布线拥塞很容易发生。我的经验是尽量不让 UDP 引擎内部出现跨 512bit 总线的组合逻辑寄存器输出尽量直接连到 AXI-Stream 信号减少组合逻辑链如果某些路径实在收敛不了考虑给高速数据通路加流水寄存器但要注意 tready 反压不能断否则会影响吞吐。编译跑完以后我习惯先看资源占用和时序余量。如果 timing summary 显示 WNS 是负的不要慌先看负时序余量出现在哪条路径。一般和 GT 复位、跨时钟域信号有关优化这类路径能解决大部分问题。4. 上板测试从 link up 到跑满带宽的完整操作4.1 测试拓扑与物料清单上板之前先准备好测试环境。我的拓扑非常直接FPGA 板卡上的 QSFP28 光口通过一根 DAC 铜缆直连到服务器上的 100G 网卡。服务器网卡我用的是 Mellanox ConnectX-5驱动加载以后用 ethtool 确认速率已经是 100G。物料清单FPGA 开发板板载 QSFP28 接口服务器带 100G PCIe 网卡安装好驱动QSFP28 DAC 线缆一根长度不超过 3 米短距离测试比光模块加光纤省事网线连接服务器管理口方便远程操作避免反复跑机房。如果手里没有 100G 服务器网卡也可以两台 FPGA 板卡对打但排查链路问题会麻烦一些因为两边都是硬件逻辑不好区分是谁的问题。用服务器网卡做对端你至少能用 ethtool、wireshark 这些软件工具把数据链路看清楚。4.2 PC 端准备与抓包策略服务器网卡先配 IP比如 192.168.1.10/24FPGA 侧固定 192.168.1.20。不要开 DHCP也不要指望 FPGA 会响应 DHCP 请求。MTU 我建议先用默认 1500跑通以后再考虑是否需要开 jumbo frame。100G 线速下大包确实容易跑满带宽但 MTU 9000 需要 FPGA 侧 MAC 和 UDP 引擎都支持否则会有分片问题。抓包工具我用 Wiresharkfilter 直接写udp但要注意服务器网卡收到的大量 UDP 包Wireshark 可能由于系统 buffer 不够而漏抓。这个不影响链路判断抓包主要是看格式不是精确计数。精确计数用系统统计ethtool -S enp1s0f0 | grep -E rx_packets|tx_packets netstat -sunetstat -su里的 packets received 和 packets to unknown port receive 这两个计数很有用前者是内核收包总数后者是内核收到但找不到对应 socket 的 UDP 包。如果 packets received 一直涨而到 unknown port 的包也涨说明 FPGA 发包没问题只是服务器上没有对应的 UDP socket 在监听这个可以在测试时用 iperf3 起一个服务端。4.3 ping、ARP、UDP 回环、iperf3 打流的执行顺序我的建议是不要上来就跑 iperf3先走一遍基础链路验证。第一步测 ARP。在服务器上执行ping 192.168.1.20如果 ping 通说明 ARP 请求和应答、ICMP echo、ICMP reply 全部正常。如果 ping 不通先在 Wireshark 里看有没有 ARP 请求发出、有没有 ARP 应答回来这一步能直接确定问题出在物理层还是协议层。第二步测 UDP 回环。FPGA 里做一个最简单的 loopback收到的 UDP payload 原样从发送端口返回。服务器上写一个简单的 Python 脚本往 192.168.1.20 的某个端口发一串数据同时监听那个端口看能不能收到同样的数据。这个测试通过说明 UDP 接收、发送、MAC 层地址过滤都正常。第三步才是 iperf3 打流。在服务器起接收端iperf3 -u -sFPGA 作为发送端通过内部逻辑持续向服务器的 IP 和端口发 UDP 包。如果 FPGA 不方便跑协议生成也可以在服务器上用 iperf3 发包FPGA 做回环服务器同时起 iperf3 -u -s 接收这样也能测出丢包率。4.4 怎么判断真的跑到了 100G很多人以为带宽测试就是看 iperf3 标注的 sender 速率。实际上你要区分三层线速、有效吞吐、用户态吞吐。线速指的是物理层 100GbpsFPGA 侧如果以最小帧间隙同时发 4 个 25G 通道物理上就能到。有效吞吐要扣除 Ethernet 帧头、IP 头、UDP 头、IFG 和 preamble理论值大约在 94.5Gbps 左右取决于包长。iperf3 的 UDP 测试结果通常还会再低一点因为用户态 socket buffer、IRQ 处理也会占开销。我判断的标准是FPGA 侧计数器观察到的 MAC TX 字节数符合预期服务器网卡 ethtool 统计的 rx_packets 增速和 FPGA 发送数一致iperf3 显示的 UDP 丢包率不高通常 0% 或低于 0.01%。如果以上几个条件都满足可以认为 UDP 通路已经跑到了 100G 级别。5. 实测踩坑记录link 不 up、收不到包、打不满速5.1 坑一GT 参考时钟频点设置错误第一次上板我最先遇到的就是 link up 不了。板卡上 QSFP28 的参考时钟是 156.25MHz但原工程里 IP 核配置的是 161.1328125MHz。这个错误很隐蔽因为 FPGA 编译、烧录都不会报错只是上电后 GT 的复位状态始终无法退出PCS 一直处于 loss of lock 状态。定位过程是这样的先用 ILA 抓 GT 状态寄存器发现 rx_aligned 一直为 0再往上游查发现 gt_rxresetdone 和 gt_txresetdone 都是 0说明收发器根本没有完成校准。查原理图确认参考时钟频点后回到 IP 核配置页修改为 156.25MHz重新生成再上板就正常了。所以拿到板卡的第一件事是弄清原理图上 MGTREFCLK 的频点。即使原工程写了参考时钟也要核实因为有些开发板为了通用性会把多个时钟源接到同一个 GT bank不同时钟源频点不一样。5.2 坑二TX/RX polarity 不对导致链路不稳定第二个问题是链路偶尔起来、偶尔起不来起来了跑一会儿又会掉。用 ethtool 查网卡链路状态显示 autoneg 完成但 RX errors 一直在涨。FPGA 和光模块 / DAC 铜缆之间是高速差分信号如果 PCB 上走线反相信号也是能跑的但误码率会非常高。Xilinx GT 提供了 TX polarity 和 RX polarity 配置可以在 IP 配置界面或者动态寄存器里改。我这边把 4 个通道的 RX polarity 全部取反之后误码率瞬间降到 0。判断 polarity 问题有个技巧看 GT 的 rx_byte_alignment 状态如果一直无法对齐再检查 rx_polarity或者干脆先用 IP 自带的 IBERT 跑一下误码率测试哪条通道误码高就怀疑哪条通道的 polarity。5.3 坑三MAC 能收包UDP 引擎却一直不出 hdr这个问题很典型。MAC 层计数显示收到了大量包但 UDP 引擎的 rx_udp_hdr_valid 从来没拉高过。排查的时候我首先怀疑 MAC 层过滤条件不对但看信号MAC 已经输出了完整帧。于是 ILA 抓 UDP 引擎内部的解析逻辑发现它在以太网类型字段判断时直接匹配了 0x0800但实际抓到的帧里以太网类型是 0x88A8后面跟着一层 VLAN tag。原因找到了上游设备或者测试脚本发出的帧带了 802.1Q VLAN 头。开源 UDP 引擎如果没做 VLAN 剥离会把 VLAN 标签当成 IP 头开始解析自然解不出 UDP。解决方法是屏蔽 VLAN或者在 UDP 引擎前加一层 VLAN 剥离逻辑。后来我也建议做测试时先用最朴素的帧格式验证别一上来就带 VLAN 头。5.4 坑四小包线速瓶颈与反压设计最后是性能问题。100G 线速下64 字节小包的极限速率大约 148 Mpps这个数字对任何硬件处理逻辑都不轻松。开源 UDP 引擎通常假定用户逻辑消费数据的速度足够快一旦用户逻辑处理不过来必须通过 tready 反压回去。我测试时发现大包 1400 字节能轻松跑满 94Gbps 有效吞吐但小包 64 字节只能跑到大约 60Gbps并且丢包率很高。原因不在 UDP 引擎而在回环逻辑里对 payload 的处理太慢导致反压持续生效MAC 侧在 FIFO 满后必须丢包。这个问题的优化思路有两个一是提高用户逻辑的流水线处理能力让每个通道独立消费数据二是调整 FIFO 深度和反压阈值不要让 FIFO 一满就立刻丢包。如果只是做协议栈验证小包丢一部分可以接受但要把这个瓶颈记录清楚方便后面优化。6. 移植完成后的性能验证与后续演进建议6.1 100G UDP 性能的合理预期与测量口径测试完成以后我整理了一份性能基线测试项结果说明ARP 请求/应答正常链路通了之后的第一个业务ICMP ping正常64 字节 ping 1000 个包0 丢包UDP 回环正常随机长度载荷回环内容一致UDP 大包发送约 94Gbps1400 字节包长接近有效吞吐上限UDP 小包发送约 60Gbps64 字节包长用户逻辑消费速率受限这个结果是比较真实的。如果你看到别人晒 100G UDP 没有任何丢包最好问一下他用的包长是多少、FPGA 里用户逻辑在处理什么业务单纯一个100G UDP 跑满其实有很多前提条件。6.2 后续往 NIC 方向演进的扩展点如果只是做协议栈验证到这一步就可以收尾了。但如果你想把这块 FPGA 做成真正的 100G 网卡还需要补几块内容PCIe DMA 通路把 UDP payload 直接搬到主机内存而不是在 FPGA 内部做回环多队列和 RSS 哈希让不同五元组的 UDP 流量分散到不同队列中断与 doorbell 机制主机驱动才能高效收发驱动层支持这个决定了网卡能不能被操作系统原生使用。这些工作已经超出移植开源 UDP 协议栈的范畴但可以考虑基于 Corundum 这类开源网卡方案继续演进它有现成的 PCIe DMA 和 Linux 驱动框架。6.3 最后一点个人心得这次移植最大的体会是开源工程的代码质量普遍不错但物理层相关的部分必须结合自家板卡重新做不要幻想原位替换能直接跑通。所有坑里参考时钟频点、Polarity、VLAN 标签这几个问题是最常见的建议在你自己的工程里把这些检查项做成上板 checklist。另一个建议是从一开始就保留完整的计数器信号。无论 MAC 层计数、UDP 引擎计数还是 AXI-Stream 的 tvalid 计数全部接到一个调试总线通过 ILA 实时观察。这个习惯能让你在上板排错时省一大半时间。后面我再做类似移植会直接把这个调试体系写成固定模板不再临时抱佛脚。
返回列表