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

资讯详情

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

开源100G FPGA UDP协议栈移植与上板测试实战

开源100G FPGA UDP协议栈移植与上板测试实战 开源 100G FPGA UDP 移植上板测试做高速网络的朋友应该都有体会10G 已经烂大街25G 正在普及而 100G 这个档位开始从数据中心核心交换往边缘和专用设备渗透。但 100G 的方案过去基本被专用网卡和商业 IP 垄断想在自己的 FPGA 工程里塞一个 100G 的 UDP 通路要么掏高额的授权费要么啃几百页的协议文档自己造轮子。我最近把一个开源的 100G UDP 硬件协议栈完整移植到了手上的 FPGA 开发板并且成功跑通了上板测试。这个项目解决的核心问题就是在 FPGA 上实现一个能直接对接 100G 以太网的 UDP 收发通路让主机侧或者逻辑侧可以像操作一块普通网卡一样高速收发 UDP 数据包。整个过程涉及的链路非常长从 GT Transceiver 到 MAC、从 UDP 卸载引擎到 DMA、从时序收敛到 iperf3 打流每一步都有坑。这篇博文就把我踩过的坑、验证过的方案、以及最终稳定跑通的配置完整记录下来给正在做或者准备做 100G UDP 的同行一个参考。先说结论这项技术的大致路径已经非常清晰FPGA 上的 100G UDP 早已不是实验室级别的东西但它依然不是一个“打开即用”的方案——协议栈选型、与用户逻辑的接口设计、时序约束以及上板验证方法都有大量的工程细节需要处理。适合有 FPGA 基础、正在做高速网络采集或传输方案、或者纯粹想搞明白 100G 以太网在 FPGA 上是怎么实现的工程师阅读。1. 项目背景与方案选型1.1 为什么是 100G UDP做网络加速或者数据采集的工程师经常遇到一个尴尬场景CPU 的软中断在高速网络面前顶不住。10G 的 UDP 小包如果完全靠内核协议栈处理几乎能把 CPU 打满100G 就更不用说了纯软件的方案基本没有机会。这时候就需要把数据通路“下沉”到硬件里FPGA 在中间扮演一个关键角色——通过硬件协议栈将 UDP/TCP 的封装、解封装、校验和计算等重复性工作从 CPU 中剥离出来。把网络协议栈卸载到 FPGA 上最直接的价值有两个一是释放 CPU让主机 CPU 只做业务逻辑而不是搬运数据二是降低延迟硬件处理包的延迟是纳秒级到微秒级而不是软件栈的几十微秒到毫秒级。选 UDP 而不是 TCP还有一个原因UDP 无状态硬件实现简单得多。TCP 有连接管理、重传、拥塞控制、滑动窗口这套逻辑放进 FPGA 里面资源和开发成本都是指数级上升。而绝大多数的数据采集和视频传输类业务本身就不需要 TCP 的可靠性丢几个包换更大带宽和更低延迟是完全可以接受的。所以这个项目的定位就很明确做一个硬件 UDP 协议栈支持 100G 线速收发为上层业务提供高带宽、低延迟、可预期的数据通路。1.2 硬件平台与开源方案选型移植一个 100G UDP 协议栈第一个问题是选硬件平台。从工程角度建议选带 100G 光口且 SerDes 资源足够的板子。我用的是 Xilinx UltraScale 系列的开发板板载 4 路 25G 光口通过 QSFP28 封装成 100G 的端口配套的 FPGA 有足够的 GTY Transceiver 资源和逻辑资源。选型上很多人会纠结用哪个厂商的板子。Xilinx 在高速以太网这块的 IP 相对成熟Vivado 里的 100G Ethernet Subsystem IP 可以直接生成 MACPCSPMA 的完整链路。Intel 的 Agilex 和 Stratix 10 也有对应的 100G IP但使用体验和文档丰富度上Xilinx 的资料明显更多踩坑的时候能找到的案例也多所以我建议新手从 Xilinx 平台开始。开源协议栈方面的选择其实不算多。目前比较成熟的开源方案里有一类是完整网卡方向把整个以太网 MAC 到 PCIe DMA 都实现了功能全但工程规模大移植起来窗口期比较长另一类是偏轻量级的 UDP offload 引擎只做 IP/UDP 层的封装和解析底层 MAC/PCS 直接用厂商 IP这种集成速度快适合我们这种“想在 FPGA 上快速加一个 100G UDP 端口”的场景。我最后选了后者原因很简单它屏蔽了以太网物理层的复杂度同时把最核心的 UDP 逻辑做成参数化、可配置的模块我们只需要关注接口和数据通路。对比项完整软网卡方案轻量级 UDP offload 引擎集成难度高涉及 PCIe DMA 等较低纯 UDP 通路相对独立资源占用高逻辑存储SerDes中等取决于付现 FIFO 大小可靠性与生态较高有社区维护中等需自行验证细节适合场景通用网卡/云计算专用数据采集/加速业务2. 协议栈架构与核心原理拆解2.1 UDP 硬件协议栈的组成结构整个 UDP 硬件协议栈从下往上分可以切成四层物理层、链路层、网络层和传输层。在 FPGA 上物理层和链路层通常都是直接用厂商的 Ethernet IP 核而我们也确实应该这么做。自己写一个 100G 的 PCS/PMA 是完全不现实的SerDes 的高速串行逻辑、时钟恢复、FEC 这些都是厂商积累了很多年的核心资产复用是最高效的路。物理层对应的是 GTY Transceiver完成 100G 光模块的光电信号转换和高速串行收发。链路层是 MAC 层负责以太网帧的封装与解析包括前导码、帧起始界定符、CRC 校验等。再往上就是这次移植的核心UDP offload 引擎。它做的事情用一句话概括就是——把标准的 IPv4/UDP 数据报文的处理逻辑用硬件逻辑实现。具体包括解析上行方向从 MAC 收上来的数据的以太网帧识别出 IPv4 UDP 报文剥离 MAC 头、IP 头和 UDP 头把纯 payload 交给上层同时对 CRC、IP 首部校验和、UDP 校验和做检查。处理下行方向从上层要发出去的数据把 payload 逐级封装成 UDP 报文、IP 报文、以太网帧并在封装过程中硬件计算好 IP 首部校验和和 UDP 校验和。这个引擎和 MAC 之间是通过 AXI4-Stream 接口通信的。AXI4-Stream 是 FPGA 里最常见的流式接口没有地址线只有 tdata、tvalid、tready、tlast、tkeep 等信号非常适合用来做高速数据搬运。除了收发引擎本身还有一个很重要的模块DMA。因为 100G 的端口如果只靠 FPGA 内部逻辑直接吞吐数据最终还是要给主机 CPU 用PCIe DMA 是绕不开的。不过这一步的复杂度不低很多人在移植的时候会先放弃 DMA而是把 UDP 数据直接接到自定义的用户逻辑——比如接一个采集模块、一个 DDR4 存储控制器或者一个高速信号处理流水线。我这次就先把 UDP 通路验证通再把数据接到 DDR4 控制器实测可以做到大包线速写入内存这个后面会说。2.2 关键技术点校验和、ARP 与分片前面提到的校验和计算看起来简单实际上暗藏不少玄机。以太网帧的 CRC 是由 MAC 硬件完成的一般不用我们操心。但 IPv4 首部校验和和 UDP 校验和必须自己做。IPv4 首部校验和的做法是把 IP 头按 16bit 为一组做二进制反码求和。这个计算在发送路径上是由硬件完成的每次发包之前算一遍就行。关键点在于IP 头里的字段是会变的比如 ID 字段、总长度字段每发一个包都会不同所以不能只在初始化的时候算好一次。UDP 校验和更麻烦一点它有一个伪首部的概念要把源 IP、目的 IP、UDP 长度、协议号17都参与到校验和的计算里。也就是说计算 UDP 校验和的时候不但要看 UDP 头还要看 IP 头的相关信息。在纯硬件实现里通常的做法是先把 UDP 头和数据部分送入校验和计算逻辑同时从 IP 头寄存器里读源 IP、目的 IP 和 UDP 长度等字段拼成一个“伪首部”一起参与累加。为了不降低吞吐率一般用两级的树形累加结构而不是一个周期一个周期地串行累加。还有个容易被忽略的点是 ARP 协议。要做标准的以太网通信单纯把 UDP 报文从 FPGA 发出去是不够的——如果对端主机的 ARP 缓存里没有 FPGA 的 MAC 地址报文会被丢掉。硬件栈必须在收到 ARP 请求时自动应答而且要在开机的过程中主动发出 ARP 请求来获取对端的 MAC 地址。很多开源的 UDP offload 引擎里ARP 模块反而是验证起来最啰嗦的部分因为它的状态转移逻辑虽然不复杂但涉及超时重传、缓存老化等边角情况。分片问题是另一个容易踩坑的地方。IP 分片在硬件里实现起来非常消耗资源因为要缓存整个 IP 报文然后按分片规则切分成多个分片报文。TCP/UDP 大包如果超过 MTU在软件协议栈里由操作系统分片在硬件卸载引擎里就得自己处理。我建议不要做分片直接限制会话的 MTU比如设置 1500 字节的 MTU或者把上层业务的数据包控制在 1400 字节以内。做 100G 业务的一般都是数据中心内部环境MTU 9000 的巨型帧也常见但如果你用了 jumbo frame一定要确认交换机支持并且在 FPGA 侧同步调整最大帧长度的匹配规则。2.3 时钟体系与跨时钟域设计100G UDP 在 FPGA 上跑不起来的最常见原因不是逻辑写错了而是时序没过。而时序问题里面有一半是时钟架构没设计好。100G 以太网的线速率是 100Gbps在 Xilinx UltraScale 平台上用 4 条 25G 通道每条通道的线速率是 25.78125Gbps64b/66b 编码后。GT 参考时钟一般是 156.25MHz这个频率是固定的但 GT 内部通过 PLL 倍频到线速率。而用户逻辑侧的 AXI4-Stream 接口数据位宽如果是 512 bit那么时钟频率就是 100G / 512 ≈ 195.3MHz。如果位宽是 256 bit时钟频率就得跑到约 390.6MHz这对逻辑时序的压力会非常大。我这次选的是 512 bit 位宽、约 322MHz 的路径实际上 100G MAC IP 输出的 AXI4-Stream 时钟通常是在 322MHz 左右取决于 IP 内部总线位宽和版本这个频率下逻辑设计要非常注意流水线级数和路径延迟。另一个关键点是跨时钟域。UDP offload 引擎的 AXIS 时钟和 DMA/用户逻辑的时钟往往不在同一个频率需要通过异步 FIFO 来做时钟域转换。这里有一个很实用的经验异步 FIFO 的深度一定要留足尤其是在接收方向。100G 线速下一个 64B 的小包只有约 5ns 的间隔如果上游的下游逻辑偶尔反压一个周期FIFO 深度不够就会丢包。我用的接收侧 FIFO 深度是 512这是在 100G 满速下实测不丢包的保守深度。3. 移植适配与工程搭建全流程3.1 移植前的准备工作把开源代码拿到手第一件事不要急着综合先梳理它的目录结构和依赖项。我拿到这个协议栈之后发现它依赖一个特定版本的 Xilinx 100G Ethernet Subsystem IP 和对应版本的 Vivado版本不匹配会导致 IP 的端口名和接口时序对不上移植难度倍增。准备工作分三步走第一步确认开发环境。我用的 Vivado 2023.1目标器件是 XCU250 系列。开源仓库里用的可能是 2020.2 或者 2021.1我会先建一个最小工程只例化 100G Ethernet Subsystem IP确认 IP 能正常生成、综合能够通过再在这个基础上叠加协议栈代码。这一步的核心价值是把环境问题隔离掉别等把所有代码都加进来了才发现是 IP 版本不兼容。第二步理清时钟和复位。FPGA 工程的时钟架构必须在上板之前就在脑袋里画清楚GT 参考时钟是谁给的板载晶振还是外部时钟PCIe 参考时钟是谁给的业务逻辑用的 user clock 是从哪里分出来的。这个项目里100G MAC IP 输出的 user clock 是 322MHzDDR4 控制器用的时钟是 300MHzPCIe 的 clock 是 100MHz三个时钟域之间有数据交互必须全部通过异步 FIFO 或者寄存器同步器处理。第三步修订外设接口约束。开源代码里带的约束文件几乎肯定不能直接用因为引脚分配、时钟约束、IO 标准都跟具体板卡强相关。我花了几个小时把开源的 XDC 文件逐行看了一遍把和板卡相关的约束抽出来替换成当前板卡的实际引脚把时钟约束改成开发板实际的时钟源频率和引脚位置。这一步我建议不要偷懒直接重新写约束文件因为开源工程里对 GT 的约束、对高速时钟资源的约束是有讲究的它的 IBUFDS_GTE4 位置、GT_REF_CLK 的引脚分配方法都是可以直接参考的直接推倒重来容易丢信息。3.2 关键代码改写与接口适配开源代码拿来之后能百分之百直接用的概率很低我这里说的“适配”包含两层一是对外部 IP 接口的适配二是对内部数据通路的适配。先看接口适配。这个协议栈假设的 MAC IP 接口标准是 Xilinx 官方 100G Ethernet Subsystem 的 AXI4-Stream 接口但不同版本之间会有些微差别比如 tkeep 的位宽定义、tuser 的字段含义、以及流控信号的握手方式。这些细节如果对不上仿真阶段还不好暴露仿真模型常常把 tready 拉高就完事了但上板之后 IP 的行为跟模型不完全一致就会出现丢包和死锁。我的做法是写了一个很薄的 adapter 模块它的唯一职责就是做接口时序的转换和信号的打拍对齐让协议栈核心逻辑看到的接口是干净的、符合预期的。这个 adapter 不改变数据内容但把 MAC IP 和协议栈之间容易产生歧义的地方全部消灭掉。再看内部数据通路。开源代码里可能默认把收到的 UDP 数据直接送到了某个固定的用户接口比如 AXI4-Stream FIFO或者某个寄存器接口。我要做的是把这个数据出口替换成自己的模块。这里有一个容易踩的坑开源代码的时序往往是刻意优化过的比如它可能要求用户逻辑在收到 tlast 的同一个周期完成某些状态更新如果替换后的用户模块跟不上这个时序就会导致协议栈内部状态机挂死。解决办法是保留原代码里的核心状态机不动只在数据出口加一级异步 FIFO 做缓冲让时序解耦。3.3 时序约束与综合实现所有代码改完进实现之前先把约束文件过一遍。100G UDP 工程的约束重点有三个时钟约束、GT 约束、以及跨时钟域约束。时钟约束主要看主时钟定义对不对。如果 100G MAC IP 的时钟树已经由 IP 自动约束了那我们只需要关注用户逻辑部分的时钟。最容易出问题的反而是很基础的管教约束比如某个开发板上的差分时钟引脚没有声明成 DIFF 信号导致综合器报错或者布线资源分配异常。GT 约束的核心是把 GT 参考时钟引脚和 GT 通道正确绑定这通常在 XDC 里用get_pins约束到 GT 的参考时钟端口。如果绑定错误现象是上板之后链路起不来GT 的复位状态一直拉不下去。综合实现之前我会先跑一遍report_clock_interaction看看有没有未约束的时钟域交叉。100G 工程的跨时钟域路径非常多如果全部交给工具去自动分析时序报告会长得吓人而且很多路径其实是异步 FIFO 的跨时钟路径应该设置set_clock_groups -asynchronous来排除掉。这里有个经验不要无脑把所有跨时钟路径都设成 false path因为如果将来数据路径上加了组合逻辑可能掩盖真实时序问题。我的实测流程是综合之后先看时序摘要时序违例超过 100ps 的路径用report_timing_summary逐条分析。在 100G 设计里最常见的问题是 GT 输出的并行数据到 MAC 核内部的路径不收敛这通常需要通过调整 IP 配置比如 pipe-line stage 选项来解决而不是靠改 RTL。4. 上板测试与性能验证4.1 环回自测先把通路跑通代码综合、实现、生成 bitstream 之后第一步不是直接接主机打流而是先做片内环回。这步的意义是先把有没有逻辑错误这件事确定下来。片内环回的方式很简单在 100G Ethernet Subsystem IP 的配置里把 PCS/PMA 层设置为近端环回模式也就是数据从发送侧出来之后不经过光模块和线缆直接在 FPGA 内部就绕回到接收侧。这个模式下光模块没有信号FPGA 自身的收发通道是闭合的。跑环回测试用不着 iperf3直接用一个简单的内部测试模块发一个固定长度的 UDP 报文然后检查接收侧能收到相同内容的报文。如果环回都跑不通那问题出在协议栈内部的收发状态机、或者 MAC 的配置上先别急着往主机侧接。环回通过之后需要把光模块插上改用远端环回模式。远端环回是把光口和线缆接到一个远端设备上比如一台支持 100G 的交换机或者对端服务器数据从 FPGA 发出去之后经过光模块、线缆再到对端由对端把报文回送回来。这一步测的是整个物理链路的可靠性。这里有一个值得注意的细节100G 光模块对信号质量非常敏感。如果远端环回之后出现大量 CRC 错误先别怀疑协议栈先把你手上的光模块和线缆换一对再测。我遇到过一次“莫名丢包”排查了半天最后发现是光模块的插拔次数多了金手指脏了接触不良。4.2 用 iperf3 实测 100G 带宽环回通路没问题就可以真正用主机打流了。打流工具我用的是 iperf3命令很简单# 服务端对端机器 iperf3 -s -u -i 1 # 客户端FPGA 所在主机 iperf3 -c 192.168.1.10 -u -b 0 -i 1 -t 30-b 0表示不限带宽用最大速率发包。UDP 模式下 iperf3 无法自动测出吞吐率只能统计实际收到的包数因此真正能证明 100G 线速的指标是收包速率接近线路速率。这一阶段的典型情况分两种如果 DMA 和用户逻辑已经打通数据最终能落到主机内存那 iperf3 能直接显示出 Mbps。但如果像我第一次那样只验证 UDP 通路而数据最终直接丢弃那 iperf3 输出的是“收到 XX 包”但带宽显示只有几个 G——这通常说明 usercore 逻辑吞吐率上限到了。我这次的做法是把 UDP 接收侧做成“计数 定时上报”的模式FPGA 内部统计每秒钟收到的字节数和包数通过串口或者 PCIe 寄存器上报。这样即使不上报给 iperf3也能直接证明线速能力。实测下来配置 4 通道 25G 的 100G MAC IP在使用 1420 字节大包的时候FPGA 收包速率可以达到线速CPU 占用几乎为零。小包64 字节场景下瓶颈不在协议栈而在包处理速率FPGA 也需要做到每秒 1.48 亿个包的解析能力这时候对内部流水线和 FIFO 带宽的压力明显上升。很多开源项目在文档里会标注“64 字节小包线速”作为性能指标这个指标的含金量比大包高得多。4.3 长稳测试与数据校验带宽测出来没问题不代表工程就交付了。高速网络设备最怕的不是速度不够而是不稳定——跑 10 分钟挂死、跑 3 小时偶发丢包、一连跑 48 小时才出现一次 CRC 错这种问题最难排查。长稳测试我建议至少跑 24 小时以上。测试期间用 ILA集成逻辑分析仪监控几个关键信号比如发送侧 tready 拉低的频率、接收侧丢包计数器的值、GT 的误码率计数。数据校验这块我用的是一个非常土但有效的办法在发包侧预设一个递增值序列比如每个包的第一个 8 字节是包序号后面的数据是固定递增序列接收侧检查包序号是否连续、数据内容是否跟预期一致。如果发现包序号跳变说明有丢包如果序号连续但数据内容错了说明是翻转或者写时序问题。5. 踩坑实录与调试技巧5.1 常见问题速查表这次移植过程中遇到的问题我整理了一张速查表基本覆盖了同类工程里最高频的故障现象可能原因处理方式GT 的 tx/rx 状态一直无法 readyGT 参考时钟未正确约束或硬件上没起振用 ILA 观测 tx/rx byte aligned 信号检查参考时钟引脚约束环回模式能通远端模式不通光模块和线缆质量问题或 PCS 层 FEC 未开启换光模块和线缆或在 MAC IP 里开启 RS-FEC大包通小包丢接收 FIFO 深度不足反压导致丢弃加大 FIFO 深度或优化下游逻辑的反压链路IP 校验和错误数据正常校验和计算逻辑对伪首部的处理有误检查 UDP 校验和伪首部是否包含了源/目的 IP 和长度长时间运行后丢包某些边界状态没有彻底处理比如 ARP 表老化加长 ARP 缓存有效时间或定期主动刷新DDR4 写入速度上不去用户逻辑到 DDR4 控制器的 AXIS 接口位宽/时钟不匹配检查 AXI 数据位宽和时钟频率必要时加异步 FIFO 解偶时序不过关键路径在 GT 到 MAC 之间IP 核的 pipeline 级数配置不当调整 100G Ethernet Subsystem 的 pipeline option重新实现5.2 调试手段与独家心得有个心得我觉得值得单独说调试 100G 设计一定不要上来就抓线上全速数据。把速率降下来跑把包间隔拉大跑一步一步往上加压力。我第一次调试时直接开满速结果丢包立刻发生但根本没法定位是哪里丢的。后来把测试改成先以 1Gbps 的速率发一次发 1000 个包全部正确再 10Gbps再 50Gbps最后才上 100G。这样一旦出现问题我就可以根据出问题时的速率档位初步判断瓶颈环节。比如在 50G 以下都正常一上 50G 就丢包那大概率不是逻辑功能问题而是时序收敛或者 FIFO 带宽到了极限。另外要善用 ILA 的触发条件。不要只触发一次采集要设置更精细的触发条件比如tvalid !tready也就是“反压发生”的信号。在满速状态下反压是一直存在的所以直接触发很可能立刻触发数据全是反压点价值有限。更好的方式是触发“连续 N 个周期反压”的事件这在 Vivado 的 ILA 里可以通过级联计数器实现。这能帮你定位是哪个模块在反压反压持续多久顺着这个线索追下去瓶颈位置就清楚了。最后再分享一个经验也是这次项目里最深的体会很多开源 100G UDP 项目看起来功能齐全但真正运行起来大多数问题都出在很少有人会主动提到的“边界条件”上——比如 ARP 请求在链路刚起来时的高频重复、UDP 校验和为 0 时的特殊语义、还有对接外部交换机时对巨型帧长度的限制。这些细节决定了你的移植工作是顺利跑通还是陷入无穷无尽的调试。如果你也在做类似的事情记住一个原则别盲目相信任何一方的“默认配置”——开源代码的默认配置不一定适配你的板子厂商 IP 的默认配置也不一定适配你的业务。把所有接口的时序拿到手、把时钟树构建好、把数据通路的瓶颈前置到设计阶段去考虑后面上板的时间就能省下一大半。
返回列表