拓冰建站拓冰建站
首页 / 资讯中心 / 正文

FPGA 100G UDP协议栈移植实战:从代码梳理到上板打流全记录

前阵子把一套开源 100G FPGA UDP 协议栈从参考设计移植到自研板卡上从代码梳理、时钟约束、接口适配到上板打流整个过程踩坑不少但也把思路理顺了。说实话100G UDP 在 FPGA 上不算什么新鲜事开源工程一抓一大把可“能看”和“能跑”之间隔着的是数不清的复位时序、跨时钟域和约束问题。这篇博文就把我这次移植上板测试的完整过程记录下来包括方案选型、移植要点、上板步骤和排障经验给正准备把项目从 10G/25G 升到 100G或者想在 FPGA 上做高速数据传输、硬件加速卡的工程师做个参考。1. 项目背景与方案选型1.1 为什么是 100G UDP先聊聊为什么选 UDP。数据中心、仪器仪表、视频传输这类场景里TCP 有重传和拥塞控制逻辑复杂不说要做到线速需要维护大量连接状态在 FPGA 上实现成本很高。UDP 无连接、无状态协议栈逻辑简洁非常契合 FPGA 的流水线处理方式。用户数据进来按固定格式封装 IP 头、UDP 头算好校验和直接发出去整个路径几乎可以做到一拍都不停。100G 的需求则来自带宽压力。10G 时代做数据采集一个通道勉强够用到 25G 就有点紧张现在很多场景一上来就是 4 个 100G 口起步比如高速信号采集回传、AI 推理集群的流量分发、分布式存储的数据搬运。FPGA 的优势在这里体现得很明显处理器跑 UDP 协议栈再快也受限于 CPU 主频和 PCIe 带宽而 FPGA 里协议栈本身就是硬件流水线数据从光口进来解析完直接交给用户逻辑中间几乎不产生额外延迟。选用开源方案核心是时间成本。从零写一个 100G UDP 协议栈不算 MAC/PCS 底层光是 ARP 应答、IP 校验、UDP 校验、多通道调度这些逻辑经验丰富的工程师至少也要一两个月。开源项目通常已经解决了大部分通用问题我们要做的移植工作集中在接口适配、时序收敛和上板验证这比闭门造车快得多。1.2 开源方案怎么选选型的时候我主要看了几类项目。第一类是完整以太网 MAC UDP 卸载引擎比如 Alex Forencich 的 verilog-ethernet 项目BSD 3-Clause 许可覆盖 10G/25G/100G 的 MAC 层实现配套有 UDP/IP 收发引擎、ARP 模块代码风格统一文档也比较全是很多 FPGA 高速网络项目的首选参考。第二类是 Cornell 的 UDP 卸载引擎早期在 FPGA 社区非常流行但主要针对 10G 速率设计上 100G 需要大改数据通路。第三类是各家半导体厂商参考设计里剥离出来的代码这类代码通常依赖特定芯片原语移植到别的平台工作量不小。选型时我看重四点许可协议是否宽松、是否依赖特定厂商原语、数据接口是否标准、有没有人在目标速率上真正跑通过。最终选了 verilog-ethernet 这一系因为它的 MAC 层支持 100G用户侧接口是标准的 AXI4-Stream后续不管是接 DMA、接 DDR 还是接自定义业务逻辑都比较顺手。对比下来大概是这样方案速率支持许可厂商依赖上手难度社区活跃度verilog-ethernet 系列10G/25G/100GBSD-3低可用原语封装中等高Cornell UDP 卸载引擎主要 10G类 BSD低中等中厂商参考设计剥离代码取决于原设计各有不同高高低我最后选了 verilog-ethernet 作为基础但也不是直接拿过来就用MAC 层硬件原语部分需要针对自己板卡的 GT 位置和参考时钟重新封装。1.3 硬件底细要摸清移植前的硬件调研特别重要这步没做好后面全是坑。首先看板卡的原理图确认光口接到 FPGA 的哪个 Bank、用的哪组 GT是 QSFP28 还是 SFP56 笼子这对约束文件里的 GT 位置直接相关。其次看参考时钟100G 的 GT 参考时钟一般用 161.1328125 MHz要确认时钟芯片输出是否满足要求走线是直流耦合还是交流耦合。第三看光模块类型100G 光模块有 SR4、LR4、CWDM4 等不同模块对 FEC 的要求不一样比如 SR4 通常不需要 RS-FECLR4 建议开启这会直接影响 MAC/PCS 层的配置。还有一点容易被忽略确认板卡上光口和 FPGA 之间的 SerDes 通道数。100G 以太网多数是 4 通道 25.78125 Gbps 的 NRZ 信号也就是 CAUI-4如果板卡用了 1 通道 100G PAM4 的方案MAC 侧接口会不一样。我这次用的就是 4×25G 的 QSFP28 方案用户侧数据通路按 512bit 201.416MHz 设计这是 100G 比较常规的配置。2. 移植前的准备与代码拆解2.1 读懂数据流是第一步拿到开源代码第一件事不是急着编译而是把数据流理清楚。一个典型的 100G UDP 协议栈接收方向数据流是这样的GT 接收串行数据经过 PCS/PMA 解码恢复出 66bit 对齐的块再交给 MAC 层解析前导码、校验 CRC、去掉 IFG得到完整以太网帧然后进入 UDP 引擎依次检查目的 MAC 地址是否匹配、以太网类型是否为 IPv4、IP 头版本和头部校验和是否正确、UDP 目的端口是否命中全部通过后把 payload 从 AXI4-Stream 接口送出去。发送方向正好相反用户逻辑把数据写到 AXI4-StreamUDP 引擎负责填充 MAC 目的地址、源地址、以太网类型、IP 头、UDP 头计算 IPv4 头校验和UDP 校验和在 IPv4 场景下可以填 0后面细说然后 MAC 层在帧头插入前导码、帧尾追加 CRC32最后进 PCS 编码、GT 串行发送。除了这条高速数据通路还有两条慢路径ARP 应答和 ICMP 回显。ARP 用于 IP 到 MAC 的解析对端设备在发数据前会广播 ARP 请求协议栈需要回 ARP 应答否则对端的 ARP 表里没有我们的 MACUDP 包根本发不出来。ICMP 回显就是 ping方便调试链路连通性。开源代码里这两块通常是独立的模块端口比较简单移植时重点确认时钟域和复位信号有没有接对。2.2 时钟和复位移植的第一道坎这part我愿称之为移植路上最大的坑。100G 的时钟分为几层GT 参考时钟、MAC/PCS 用户时钟、用户逻辑时钟、控制接口时钟。GT 参考时钟一般来自板载时钟芯片100G 场景下常见值是 161.1328125 MHzMAC 用户侧 AXI4-Stream 接口如果位宽是 512bit时钟就是 201.416 MHz如果位宽 256bit时钟就是 402.832 MHz。这两个时钟不能随便用 MMCM/PLL 生成因为它们和 GT 的 line rate 有严格的倍数关系必须由 GT 的 txoutclk/rxoutclk 派生或者直接使用参考时钟相关的 CDR 时钟。我这次项目里发送方向用 txoutclk 驱动的 BUFG 作为用户时钟接收方向用 rxoutclk 驱动的 BUFG。有些开源代码里为了省事直接用一个固定 200MHz 时钟驱动用户逻辑结果时序也能过但跑起来偶尔丢包一查就是时钟频率不匹配导致 FIFO 读空或写满。复位信号更讲究。GT 复位、MAC/PCS 复位、用户逻辑复位必须按顺序来GT 没完成复位就拉高 MAC 的复位PCS 的 alignment 状态机大概率起不来。开源代码里通常有一个 reset 模块负责产生多级复位释放时序移植时不要省掉这个模块直接全局复位否则会出现“上电后有时正常有时不正常”的经典问题。2.3 接口适配对接自己的业务逻辑开源 UDP 引擎的用户接口基本都是 AXI4-Stream信号为 tdata、tkeep、tvalid、tready、tlast、tuser。tdata 是数据tkeep 表示每字节是否有效tlast 表示包尾tvalid/tready 是握手信号。有个细节当 tlast 拉高的那一拍tkeep 不一定全 1因为包长不一定对齐到整拍接收逻辑必须根据 tkeep 截断数据不能无脑按整拍处理。如果你要接的是 DDR 缓存或者 PCIe DMA中间必须加异步 FIFO 做跨时钟域。我常用的做法是 FWFTFirst Word Fall Through模式的异步 FIFO读写侧独立时钟配合 almost_full 信号做反压。高水位设多少很有讲究设太小会频繁反压导致带宽上不去设太大又浪费 BRAM/URAM我一般按“线速下 2~3 个最大包”的深度来预留。用户逻辑这侧还要考虑 tready 的反压策略。UDP 引擎接收方向如果用户逻辑处理不过来tready 拉低引擎内部的 FIFO 就会上涨涨到满就会丢包。如果业务逻辑本身有处理延迟务必在入口加足够深的缓冲不能指望引擎等你的逻辑慢悠悠地处理。2.4 仿真验证先别急着上板移植完了先仿真这个步骤不能省。直接用 Vivado 仿真跑整个 100G UDP 链路比较慢我习惯把 UDP 引擎单独拿出来构造一个简单的 testbench发送方向用随机长度的数据包激励检查输出的以太网帧头、IP 头、UDP 头字段和长度对不对接收方向用预构造的以太网帧喂进去检查 payload 和 tlast/tkeep 是否匹配。再有就是模拟反压场景用户逻辑随机拉低 tready验证 UDP 引擎能不能正确处理 backpressure不会丢数据也不会出现数据错位。仿真通过以后再移植到顶层工程里做上板验证这样能把逻辑问题和时序问题分开排查效率高很多。我写过一个简单的 cocotb 脚本用 Python 做激励和检查比手写 SystemVerilog testbench 方便不少推荐试试。3. 核心移植过程与改造细节3.1 MAC/PCS 层对接开源代码里的 MAC 层通常有两种形式一种是纯 RTL 实现的不依赖厂商硬核另一种是封装好厂商 MAC IP 的 wrapper。verilog-ethernet 里两种都有看目标平台选。Xilinx 平台推荐用 CMAC 硬核资源占用少时序好收敛但需要自己写 wrapper 把 CMAC 的 AXI4-Stream 接口和开源 UDP 引擎对接起来。对接时重点看三个信号。第一个是 tuserCMAC 在接收方向会用 tuser 标记错误包CRC 错误、长度错误等UDP 引擎要忽略这些错误包否则会把坏数据当正常包处理。第二个是 local_fault / remote_fault这两个信号表示链路层故障链路不通时 MAC 不会输出数据调试时看这两个信号能快速定位问题在光模块还是逻辑。第三个是 FEC 配置100G SR4 光模块通常不用开 RS-FECLR4/CWDM4 建议开配置不对最典型的现象是 link 灯亮了但大量 CRC 错误。我这次用的 CMAC 配置是 512bit 用户接口tkeep 和 tdata 比例为 1:8时钟 201.416MHz。刚开始直接套用开源代码默认的 256bit 接口导致 tdata/tkeep 位宽对不上编译过了但跑起来数据完全乱掉后来统一改成 512bit 才正常。3.2 ARP 逻辑与静态路由处理ARP 模块的移植相对简单核心是一个查找表维护“IP 地址 → MAC 地址”的映射。开源代码里 ARP 模块通常自带请求发送和应答功能移植时改一下时钟和复位就能用。这里有个工程上的取舍如果你的应用场景是 FPGA 和 PC 网卡直连且 IP 固定不变可以直接在 ARP 模块里预置一条静态表项省掉 ARP 请求的时延。但放到真实网络环境比如 FPGA 接交换机再对接多台服务器静态表就不行了必须保留完整的 ARP 交互逻辑。我建议保留动态 ARP 功能同时允许软件通过寄存器接口写入静态表项兼顾两种场景。还要注意 ARP 表项的老化逻辑。开源实现有些不做老化长时间运行后表项会占满有些老化时间太短导致高速打流时频繁触发 ARP 请求。我一般把老化时间配置到 5 分钟以上和 Linux 网卡的 ARP 老化时间匹配避免不必要的广播。3.3 校验和逻辑的取舍IPv4 头校验和必须计算这是 IPv4 协议的基本要求。UDP 校验和在 IPv4 下是可选的填 0 表示不校验很多硬件为了节省逻辑直接填 0但在 IPv6 下 UDP 校验和是强制的。如果你要对接标准网络设备建议把 UDP 校验和也做了省得对端设备因为校验和错误丢包。计算校验和用的是 16 位反码求和UDP 校验和还要包含伪头部源 IP、目的 IP、协议号、UDP 长度。重点注意字节序网络字节序是大端FPGA 里处理时要搞对高低字节的顺序否则算出来的校验和永远不对。调试时可以用 Wireshark 抓包看 Checksum 字段是否显示 correct如果显示的 incorrect先怀疑字节序。有个细节值得一提现代操作系统网卡大多支持 checksum offload发送时软件会把校验和字段留空交给网卡硬件填充。所以你在对端 Windows 或 Linux 上用 Wireshark 抓包有时候看到“checksum incorrect”其实是抓包工具没识别 offload 后的结果不代表网络有问题别被这个误导。3.4 约束文件与工程实现移植到具体工程时约束文件是最容易出问题的。除了常规的引脚约束时序约束里必须明确设置异步时钟组把 GT 参考时钟、用户时钟、控制时钟之间的路径声明为 false path 或异步否则时序分析会报大量违例。我这次工程里典型的约束片段是这样的# 时钟定义 create_clock -name gt_ref_clk -period 6.206 [get_ports gt_ref_clk_p] # GT 输出的用户时钟由 IP 自动生成与参考时钟异步 set_clock_groups -asynchronous \ -group [get_clocks -include_generated_clocks gt_ref_clk] \ -group [get_clocks -include_generated_clocks user_axis_clk] # 跨时钟域异步 FIFO 路径不做时序检查 set_false_path -from [get_clocks -include_generated_clocks gt_ref_clk] \ -to [get_clocks -include_generated_clocks user_axis_clk] set_false_path -from [get_clocks -include_generated_clocks user_axis_clk] \ -to [get_clocks -include_generated_clocks gt_ref_clk]这里有个容易踩的坑get_clocks -include_generated_clocks一定要加因为 GT IP 输出的时钟是 generated clock不加这个参数后面的约束命令匹配不到时钟等于白写。时序收敛方面100G 的设计跑 201MHz 或 402MHz重点是控制高扇出信号。复位信号、tready 信号如果扇出超过几百个 FF建议手动复制寄存器树或者用 BUFG 驱动否则 WNS 很难为正。我用的是 VU3P 级别的器件资源余量充足主要瓶颈在布线拥塞关键是合理规划用户逻辑的位置避免把大量逻辑堆在 GT 附近。4. 上板测试全流程4.1 测试环境与链路搭建上板前把测试环境准备妥当。硬件方面FPGA 板卡、QSFP28 光模块、100G DAC 线缆或者 AOC 光缆、对端 100G 网卡我用的是 CX6 单口网卡。软件方面对端服务器装好网卡驱动准备好 iperf3、tcpdump、Wireshark。如果需要抓包分析建议在对端网卡上做镜像或者用专门的抓包工具Wireshark 直接抓 100G 流量性能压力比较大容易丢包。光模块兼容性值得注意。某些便宜光模块在特定网卡上可能协商不起来或者协商成功但误码率高。测试前先用 ethtool 确认对端网卡 link 状态确认速率是 100G 而不是降级到 40G 或 25G。FPGA 侧则看 CMAC 的 local_fault / remote_fault 状态寄存器两个信号都正常才算链路就绪。4.2 分级验证从回环到端到端我的习惯是分级验证每级都确认通过再进行下一步。第一级是 MAC 内部回环。在 CMAC 里开 local loopbackFPGA 发出的数据不经过光模块直接环回到接收方向。这一级能验证 FPGA 内部的 MAC、UDP 引擎、用户逻辑是否工作正常。测试方法是 FPGA 内部用一个简单的发包模块周期性发送固定内容的 UDP 包然后在接收侧检查能不能正确收到。这一步通过说明逻辑侧没问题。第二级是光模块远端环回。有些 QSFP28 光模块支持 loopback 模式或者用一根 DAC 线缆把 TX 和 RX 环起来。这级主要验证 GT 的收发通道、PCS 对齐是否正常同时可以顺便看一下误码率。第三级才是对端网卡直连。FPGA 和对端服务器用 DAC 或光模块互连配置好 IP 地址先 ping 通再跑 UDP 收发。这一级通过整套链路才算真正打通。4.3 UDP 打流与性能验证性能验证我主要用 iperf3 做 UDP 打流对端 Linux 服务器起服务端# 服务端对端 PC iperf3 -s -p 5201 # 客户端对端 PC反向打流到 FPGA 不方便一般用 FPGA 侧自研发包模块或网卡打向 FPGA iperf3 -c 10.0.0.1 -u -b 100G -l 1400 -t 60 -p 5201这里有个容易犯的错iperf3 发 UDP 用的是单线程模型100G 带宽下 CPU 会成为瓶颈测出来的速率不是真实线速。要真正打满 100G要么用多线程 iperf3-P 4甚至更多要么用支持多队列的专用打流工具比如某些厂商的流量发生器。我实际测试时单线程 iperf3 最多跑到 60~70Gbps多线程才能逼近线速。测试包长也有讲究。大包1400B 以上考验的是带宽能力小包64B考验的是包处理速率。100G 以太网 64B 小包的理论线速约为 148.8Mpps对端网卡如果不是专门优化过的小包场景很容易丢包这不一定是 FPGA 的问题要会区分。对端 Linux 上还要注意几件事关闭 GRO/GSO 特性ethtool -K eth0 gro off gso off否则网卡驱动会做包合并测出来的包速率虚高关闭 checksum offload 可能导致抓包看到校验和错误但实际不影响转发。4.4 性能瓶颈定位与优化如果打流时发现吞吐上不去第一件事是看丢包发生在哪一端。FPGA 侧有接收计数器、发送计数器对端网卡有 rx_dropped、rx_missed 统计对照着看就能定位。FPGA 收到但没发出去问题在 FPGA 内部FPGA 发出去了但对端没收全问题在对端网卡或链路。FPGA 内部最常见的瓶颈是 FIFO 深度不足导致的反压。UDP 引擎接收到突发流量如果用户逻辑处理速度跟不上接收 FIFO 会写满引擎只能丢包。解决办法是增加 FIFO 深度、优化用户逻辑的处理流水线、提高反压响应速度。另一个瓶颈是跨时钟域 FIFO 的读侧带宽比如从 201.416MHz 的 AXI-Stream 搬到 300MHz 的用户时钟域必须保证读侧带宽大于写侧否则迟早溢出。我这次还遇到一个性能问题小包场景下用户逻辑每包处理开销太大虽然单包处理只要几十个周期但 148.8Mpps 的包速率下每包间隔只有 6.7ns201MHz 时钟下约 1.3 个周期如果逻辑不能在 1~2 拍内完成包解析小包就会大量丢包。后来把包解析逻辑从状态机改成流水线才把丢包率降下来。5. 常见问题与避坑指南5.1 高频问题速查表现象可能原因解决方式link 灯不亮光模块兼容性、GT 参考时钟错误、DAC/光缆损坏用 ethtool 和 CMAC 状态寄存器交叉确认换模块或线缆排查link 正常但 ping 不通MAC 地址过滤未关闭的混杂模式、ARP 表未命中先关 MAC 过滤再确认 ARP 应答是否发出小包大量丢包用户逻辑处理能力不足、FIFO 深度不够优化流水线、加大 FIFO、提高反压响应速度大包吞吐上不去跨时钟域 FIFO 读带宽不足、反压频繁保证读侧带宽大于写侧调整 almost_full 水位长时间运行后偶发丢包ARP 表老化、复位信号抖动、CRC 偶发错误检查老化时间、加固复位逻辑、开启 FECWireshark 显示 checksum incorrect对端网卡 checksum offload 导致忽略或用支持 offload 解析的抓包方式5.2 两个印象深刻的调试案例第一个案例是链路状态正常但收不到任何 UDP 包。FPGA 侧 CMAC 的 local_fault 和 remote_fault 都正常发送方向也不报错但对端 PC 就是收不到包。排查了很久最后发现开源 UDP 引擎默认开启了 MAC 地址过滤我自己工程里给的 MAC 地址和对端 ping 包的目的 MAC 不匹配所有包都被过滤掉了。解决办法是把接收引擎配置成混杂模式或者确保 FPGA 的 MAC 地址和对端 ARP 表里的记录一致。这类问题最坑的地方在于链路状态看起来完全正常极易让人误判为逻辑问题。第二个案例是 100G 大流量压测时跑几分钟后吞吐突然掉到接近 0。一开始怀疑是温度导致 GT 误码后来看 CMAC 寄存器发现 rx 方向出现了大量 CRC 错误进一步排查发现是对端网卡的 FEC 配置和 FPGA 不一致。FPGA 侧开启了 RS-FEC但对端网卡用的是 Firecode两边协商出了问题偶发误码直接导致 TCP 层重传风暴。解决办法是把两边 FEC 模式统一重新 link 后才恢复正常。这个案例提醒我100G 链路的 FEC 配置一定要两端对齐不能只看 link 灯。5.3 最后几条血泪经验先写给你第一上板前先花半天时间读代码把数据通路、复位域、时钟域理清楚远比上板后瞎试来得快。开源代码不是黑盒理清数据流能帮你省掉大量调试时间。第二跨时钟域异步 FIFO 复位一定要同步释放FWFT 模式不要随便换标准模式否则高带宽下会出现数据错位。别问我怎么知道的问就是加过班。第三打流测试时不要只看 FPGA 侧计数器对端网卡的 rx_dropped、rx_missed 一样重要多端交叉验证才能准确定位丢包位置。第四100G 的时序收敛问题七成出在复位和跨时钟域三成出在布局拥塞。遇到 WNS 负数先看是不是异步路径没设 false path再考虑代码优化。最后再分享一个小技巧调试 100G UDP 时在 FPGA 里留一组寄存器计数器和一组 ILA 探针记录接收包数、错误包数、FIFO 水位、反压次数这些信息在上板调试时比什么高级工具都好用能让你在几分钟内定位问题方向而不是对着抓包结果猜来猜去。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门