做 FPGA 高速接口的同行应该都有体会:10G 是以太网基本功,25G 开始要认真看时序,到 100G 就是另一套玩法了。前段时间我接了一个需求,要把一套开源 100G FPGA UDP 协议栈移植到自研板卡上,完成上板验证,目标是用 iperf3 跑出接近线速的 UDP 吞吐,同时把丢包率压到可控范围。
整套流程走下来,从开源仓库到 bitstream,从 GTY 参考时钟配置到 512bit 数据总线上的时序收敛,从实验室光纤连线到抓包比对,每一环都有代表性的大坑。我把这次“开源 100G FPGA UDP 移植上板测试”的完整过程整理出来,方案选型思路、关键模块原理、移植步骤、排错记录一次说清楚。
适合三类人读:准备从 10G/25G 往 100G 迁移的 FPGA 工程师,想评估开源 UDP 卸载方案是否靠谱的系统架构师,以及正在为 100G 上板疑难杂症失眠的同行。即便你手头的板卡还是 25G 甚至 10G,这篇文章里关于时钟、异步 FIFO、校验和、抓包排错的思路也完全通用。
1. 项目定位与方案选型:100G UDP 的开源路线
1.1 需求拆解:我们要的其实不只是 UDP
拿到需求先做拆解。高速数据采集、网络加速、存储网关这类场景里,100G 接口的意义不是“带宽数字好看”,而是把 CPU 从逐包处理里解放出来。FPGA 上实现 UDP 卸载之后,主机或者前端逻辑只需要提供目的 IP、目的端口和 payload,剩下的 MAC 帧封装、IP 头、UDP 头、校验和全部在硬件里完成,单包处理延迟可以做到微秒级以下。这种确定性的低延迟是纯软件协议栈给不了的,也是我们要在 FPGA 上做 UDP 移植的根本原因。
为什么选 UDP 而不是 TCP?答案很直接:TCP 是有状态协议,有连接管理、拥塞控制、重传机制,在 FPGA 里完整实现代价极高,吞吐还容易被状态机瓶颈拖累。UDP 无连接、无状态,硬件只需要做“封装+校验+转发”,逻辑简单,吞吐容易做高。数据中心里不少专用加速链路本来就不需要 TCP 的语义,视频流、数据复制、部分 RDMA 场景都用 UDP。对 FPGA 来说,UDP 是性价比最高的网络协议入口。
1.2 开源方案对比:Corundum 与 verilog-ethernet
动工之前,我认真盘了一遍市面上的开源方案。这里给一个基于我个人使用经验的对比表,方便后面参考:
| 方案 | 带宽支持 | 核心定位 | License | 移植工作量 |
|---|---|---|---|---|
| Corundum | 10G/25G/100G | 完整网卡平台(PCIe DMA+UDP/RDMA 卸载) | BSD | 高,依赖 Xilinx 100G IP |
| verilog-ethernet | 10G/25G | 轻量以太网组件库(MAC、LFSR、UDP/IP 卸载) | BSD | 中 |
| 商业 Xilinx IP+自研 UDP | 100G | MAC/PCS 用商业 IP,UDP 逻辑自研 | 商业 | 中高 |
| 学术项目 | 不等 | 特定协议栈 | 各异 | 参考价值为主 |
Corundum 是 Alex Forencich 那套 verilog-ethernet 的“全家桶”升级版,把 PCIe 根端口、DMA 引擎、UDP 卸载、100G MAC 全链路打通,社区活跃度很高,issue 响应也快。verilog-ethernet 更轻量,适合不需要 PCIe、只想把 UDP 收发逻辑嵌进自研数据通路的场景。我这边需要做一块独立的 100G 数据加速板卡,既要从光口收 UDP 包,也要从光口发 UDP 包,和主机 PCIe 的耦合其实是次要的,但 Corundum 的模块化程度和板卡参考设计完整度最好,所以选了它。
1.3 移植前明确边界:哪些能复用,哪些要重写
选型定了不等于万事大吉。开源代码是通用设计,落到自研板卡上一定会遇到“参考设计假设了特定时钟、特定引脚、特定 IP 版本”的问题。我在移植前先划了一条边界:物理层完全复用 Xilinx CMAC 加 GTY 的配置思路;MAC 层到 UDP 卸载层尽量不做大改动,只调整包尺寸参数和查表表项去适配场景;PCIe 相关模块当前用不到就先挂空或者走最小配置,等链路跑通再回来补。
这个边界的意义在于控制变量。100G 上板调试横跨物理层、协议层、主机软件三层,如果一次改动太多,出了问题根本定位不到根因。先把“收发 UDP 包”这条主链路跑通,再谈性能优化和功能扩展,这是我做高速接口项目一直坚持的顺序。
2. 100G 链路核心原理:移植前必须搞懂的三个层面
2.1 物理层:GTY 收发器与四通道聚合
100G 以太网物理层听起来吓人,拆开看就是一个“四通道聚合”结构。100GBASE-R 由 4 条 25.78125Gbps 串行通道组成,每条通道跑 64b/66b 编码,净数据等效 25Gbps,四条合计约 100Gbps。所以 FPGA 侧第一步是把 GTY 收发器配置到 25.78125Gbps,参考时钟几乎都用 156.25MHz。
新接触 100G 的工程师容易在一个地方卡住:10G 时代逻辑侧用 64bit 总线、156.25MHz,直接跟线路速率对应,很好理解;100G 的逻辑侧总线就变成了工程权衡的焦点。Xilinx 100G CMAC 通常给出 512bit 的 AXI4-Stream 接口,逻辑时钟需要跑到 322.265625MHz 附近,也可以选更宽的位宽来降时钟,但布线资源和寄存器扇出会迅速吃掉 FPGA 的余量。这里的第一条纪律是:逻辑侧时钟不是你想跑多少就多少,物理层 IP 已经把选择范围定死,移植时不要为了“看起来稳”去随意改动 IP 内部时钟配置。
2.2 MAC/PCS 层:64b/66b、RS-FEC 与 CMAC 状态
100G MAC 和 PCS 层的核心是 64b/66b 编码:每 64bit 数据加 2bit 同步头,变成 66bit 在串行链路上传输,接收端靠同步头做字对齐和通道绑定。CMAC 里还会做通道重排序,因为 4 条通道在接收端不一定按原始顺序到达。对调试者来说,CMAC 的 status 信号是关键中的关键:PCS 对齐、AM 锁存、通道绑定、FEC 误码计数,都是判断链路是否健康的直接证据。
RS-FEC 单独说几句。100G 短距光模块(SR4/LR4)一般可以不开 FEC,但走 DAC 铜缆或者背板 KR 模式时,RS(544,514) FEC 基本是强制要求,否则误码率高到没法稳定传包。FEC 必须在收发两端配置一致,这是上板联调最容易翻车的点之一:FPGA 侧开了 FEC,对端设备没开,结果就是链路不稳定、误码重传满天飞。在实验室里,我先统一用不开 FEC 的 SR4 光模块把功能调通,再切换到最终场景验证 FEC 配置,这样变量少、定位快。
2.3 UDP 卸载引擎:从 MAC 帧到载荷的流水线细节
UDP 卸载引擎在 100G 上的逻辑复杂度并不比 10G 高多少,难点全在“带宽×位宽×时钟”的组合上。一条典型的 TX 流水线是这样的:
- 应用逻辑通过 AXI4-Stream 把 payload 打进 TX 队列;
- 引擎查询 ARP 缓存,拿到目的 MAC 地址,生成以太网头;
- 插入 IP 头,填版本、总长度、TTL、协议号 17,计算 IP 校验和;
- 插入 UDP 头,填源端口、目的端口、长度,计算 UDP 校验和;
- 整帧送到 100G MAC 计算 FCS(CRC32),再进 PCS 编码。
RX 方向就是反向操作:MAC 层校验 FCS 并剥离以太网头,再校验 IP/UDP 头,最后按四元组(源 IP、目的 IP、源端口、目的端口)查表分发到不同接收队列。
这里面有一个非常隐蔽的边界:UDP 校验和算法是“逐 16bit 累加再取反”,硬件引擎如果做增量更新(比如只改端口号就快速重算校验),必须小心 16bit 进位回卷的边界。很多自研引擎在这个位置有隐蔽 bug,表现就是大多数包正常,特定长度或特定载荷下偶发校验错包,特别难查。开源实现一般是完整重算校验和,逻辑简单,正确性优先,这个取舍在 100G 高带宽下反而更稳。另外 ARP 表必须有老化机制,否则对端换 IP 之后你还往旧 MAC 地址上发,表现就是“链路明明是通的,UDP 就是没有回包”。
3. 移植实操:把开源代码落到自研板卡
3.1 读懂参考设计:三个文件决定成败
Corundum 仓库里每个参考板卡一个目录,典型如 Alveo U250、VCU118。拿到仓库第一件事不是改代码,而是读参考设计里的三个关键文件:顶层 RTL、约束文件、时钟 IP 配置。顶层 RTL 告诉你 100G MAC 怎么例化、用户时钟怎么产生、复位逻辑怎么设计;XDC 约束告诉你哪些引脚绑定到哪个 GTY Quad;时钟 IP 配置告诉你 REFCLK 频率、GTY 通道速率和逻辑时钟频率从哪来。
我这次是把自研板卡原理图跟 U250 参考设计逐引脚比对,列了一张映射表:GTY 位置、REFCLK 引脚、QSFP28 的管理引脚(I2C、中断、LP 模式)、状态指示灯、拨码开关,全部对应清楚才动代码。这个过程枯燥但是值得,100G 板子改错一个引脚约束,轻则综合报错,重则上板后链路完全起不来,而你很可能还在错误方向排查几个小时。
3.2 板级适配:引脚、时钟、约束逐项核对
100G 的约束文件里,有几项必须逐条确认,不能照抄参考设计:
- GTY 引脚:QSFP28 的 TX/RX 差分对必须落在所选器件可用的高速 Quad 上,且跟 REFCLK 在同一个 Quad,或者属于允许跨 Quad 的拓扑关系;
- 参考时钟:156.25MHz 差分时钟必须进 GTY 专用参考时钟引脚,不能随便用一个普通时钟引脚替代;
- 逻辑时钟:CMAC 用户时钟、AXI 总线的 create_clock 约束,要跟着 IP 配置走,不能自己拍脑袋定频率;
- 时序例外:跨时钟域 FIFO 两侧的 set_false_path 或 set_max_delay 要成套维护,漏一条就可能在布局布线时浪费大量时间。
还有一类常被忽略的约束是电源和模块管理相关引脚。100G 光模块功耗不低,QSFP28 的插拔检测、模块功耗协商信号都要正确接上并约束到位,否则模块可能因为“没被主机正确识别”而拒绝上电发射激光,链路自然起不来。这些细节全藏在原理图里,移植时不花时间核对,上板后必然还账。
3.3 工程搭建与 IP 配置:小步快跑
Corundum 的 fpga 目录里有 Tcl 脚本可以生成 Vivado 工程。我的做法是把参考板卡的 Tcl 复制一份,改成新板卡的器件型号、约束路径和 IP 参数。这里特别强调版本一致:开源代码通常是在某个 Vivado 版本下验证过的,如果新工程用了更高版本,100G Ethernet Subsystem 和 Transceiver IP 的界面参数可能会有变化,导致“明明照着脚本来的,生成的 IP 却跟参考设计对不上”。遇到这种情况,先回到仓库 README 里确认它推荐的 Vivado 版本,别一上来就追新。
IP 集成之后先做一次空跑,确认所有 IP 的时钟、复位能正常产生,再往上挂 UDP 引擎和测试逻辑。我习惯在顶层保留一个 AXI-Lite 寄存器组,把关键状态寄存器拉出来,方便硬件调试。开源代码的调试接口一般都很完善,把该接的寄存器接全,后面上板能省一半时间。
3.4 时序收敛:322MHz 逻辑域的调优手段
综合实现跑完,第一轮 timing 大概率不过,这是 100G 项目的常态。512bit 总线在 322MHz 下的扇出压力比 10G/25G 高一个量级,我总结出几个有效手段:
- 高扇出复位信号做同步复位树,避免异步复位直接扇出到几百个寄存器上;
- AXI4-Stream 长路径上插入 skid buffer,这种带流控的流水寄存器会增加几拍延迟,但能换来时序收敛;
- UDP 校验和的加法树用 DSP 或逐级打拍,不要在组合逻辑里一次算完 32bit 累加;
- 对无需关心的跨时钟域路径明确设置 set_max_delay 或 set_false_path,避免工具白费布线资源;
- 关键路径上把单周期全处理改成多拍流水,配合寄存器重组拆长路径。
最终这张板卡的 100G 逻辑域,我要求时序收敛后必须保留正余量才生成 bitstream。有人喜欢压线过,觉得省资源,但 100G 上板之后温度、电压稍有波动,临界路径会先挂掉,表现就是跑着跑着开始随机丢包。这种 bug 后期排查成本远高于当时多花两三版实现的成本。
4. 上板测试:从链路自检到 iperf3 满带宽打流
4.1 测试环境与物理链路检查
测试环境搭建本身不复杂:FPGA 板卡插 QSFP28 模块,用一根 100G DAC 线直连对端设备。对端可以是另一块 FPGA 板卡,也可以是带 100G 网口的服务器。上电后第一件事不是跑业务,是先查链路物理状态:GTY 的 QPLL/CPLL 是否锁定、CMAC 的 PCS 对齐和 AM 锁定状态、FEC 误码计数是否正常。如果对端是大网卡或交换机,还要看对端链路是否 up。
我这次用 Vivado Hardware Manager 直接在 Hardware Session 里挂 ILA,把 CMAC 状态信号和 GTY 锁存信号一起拉出来,比反复读寄存器直观得多。这一步能确认 80% 的物理层问题:时钟有没有进、收发器有没有锁、PCS 有没有对齐。链路状态确认正常之后再加载业务 bitstream,千万不要在物理层没确认的情况下,浪费几个小时去查 UDP 逻辑。
4.2 回环自检顺序:先内部数字回环,再物理回环
板卡刚点亮时直接拿主机打流是灾难,因为你分不清丢包是 FPGA 的问题还是主机网卡的问题。正确顺序是:先在 FPGA 内部做数字回环,把 TX 数据直接送回 RX,验证 MAC/UDP 逻辑本身;再开 GTY 的 near-end PMA loopback,验证收发器的串行通路;然后用 DAC 线缆做物理回环,验证外部光模块和连接器;最后才跟对端设备联调。
开源仓库一般会带 LFSR 伪随机序列发送器,verilog-ethernet 里那个 lfsr_udp 就是干这个的,可以持续发送指定长度的 UDP 包,对端收到后比对序列号和数据,丢包、错包一目了然。我习惯先用 64 字节小包把链路打死,再用 1472 字节大包覆盖长度边界,最后做随机长度混合测试。小包考验单包处理速率和 MAC 层效率,大包考验 FIFO 深度和校验逻辑,两者关注点不同,必须分开测。
4.3 iperf3 UDP 打流与主机侧缓冲调优
链路自检通过后进入吞吐测试,iperf3 是最常用的工具。UDP 打流命令大概是:
# 接收端 iperf3 -s -u -i 1 # 发送端,往接收端 IP 打 50G UDP 流 iperf3 -c 192.168.30.2 -u -b 50G -l 1472 -t 60 --get-server-output这里有几个实测教训。第一,iperf3 的 -b 只是目标带宽,不会自动增大系统 UDP 缓冲区,Linux 上要提前把接收缓冲区调大,否则稍有拥塞就是千分之几的丢包,表面看是 FPGA 的问题,其实是主机 recvfrom 处理不过来:
sysctl -w net.core.rmem_max=134217728 sysctl -w net.core.rmem_default=67108864第二,Windows 上跑 iperf3 UDP 更要注意,系统默认的 UDP 缓存区非常小,100G 打流下瞬时丢包极其夸张。可以在注册表 HKLM\SYSTEM\CurrentControlSet\Services\AFD\Parameters 下把 DefaultReceiveWindow 和 DefaultSendWindow 调整到 512MB 量级再重启,跟“100G 网卡一打满带宽就疯狂丢包”这类问题高度相关,本质是协议栈缓冲不够,不是网卡或 FPGA 的问题。
第三,吞吐和丢包要分开看。实测 FPGA 到服务器方向,用 -b 50G 打上去,接收端显示的吞吐能到 49.5G 以上、丢包率低于 0.001%,在 100G 链路上已经算很好了。追求 100G 满线速纯 UDP 打流不现实:接收端 Linux 协议栈单核单队列处理能力有限,瓶颈在服务器软件侧,不在 FPGA。
提示:打流前先确认对端网卡的 RSS 多队列和流控开关。100G 场景下单个接收队列很容易成为瓶颈,RSS 把流量散到多核多队列,接收端丢包会显著下降。
4.4 数据正确性验证与抓包避坑
吞吐达标不等于数据对。我验证正确性用“双层核对”:FPGA 端每个 UDP 包带递增计数器+固定魔数,对端校验计数连续性和魔数,这是离线比对;同时服务器侧开 tcpdump 抓包,把 pcap 导出来用脚本解析 UDP 载荷,逐字节比对,这是在线核对。两套结果一致才算通过。
抓包这里有个很常见的坑:在 Wireshark 的过滤器栏里写udp,却发现依然能抓到 ICMP 或者其他非 UDP 报文。原因在于 Wireshark 有两种过滤:捕获过滤器和显示过滤器。捕获过滤器是 BPF 语法,在抓包时生效,写错位置等于没过滤,抓下来的是混杂模式全量报文;显示过滤器只是不显示某些包,报文已经存在 pcap 里。核对数据正确性时,反而建议故意不开任何过滤器,全量抓下来再按五元组分类,信息最全,过滤问题留给后续脚本处理。
5. 常见问题速查与排错实录
5.1 链路起不来:按物理层→PCS→FEC 分层定位
100G 链路起不来的故障,90% 集中在物理层、PCS 层、FEC 配置这三层。我把这次和以往项目里的典型问题整理成速查表,方便排查时对照:
| 现象 | 可能原因 | 排查手段 |
|---|---|---|
| GTY QPLL 锁定失败 | REFCLK 频率或引脚配置错误 | ILA 抓 qpll_lock,检查时钟树 |
| PCS 对齐不上、AM 锁不住 | 对端速率不匹配、线缆/光模块问题 | 换线、换模块、核对对端配置 |
| FEC 误码率飙升 | 两端 RS-FEC 配置不一致 | 统一 FEC 开关,观察 corrected/uncorrected 计数 |
| link 起来又立刻掉 | DAC 线太长或质量差 | 换短 DAC 线或换光模块 |
| 只有 TX 没有 RX | RX 差分对约束错误或对端未发数 | 查引脚约束、对端发包状态 |
100G 调试最忌讳“流水式试错”:每改一个参数就重新综合、重新加载,时间全浪费在等待里。GTY 和 CMAC 的很多状态是运行时可读的,先用 ILA 把锁存状态、错误计数看清楚,判断出方向再决定要不要改代码重综合。一次只改一个变量,这条纪律在高速接口调试里永远适用。
5.2 丢包与吞吐上不去:从接收侧反推
丢包不能只盯着 FPGA。我遇到过一个典型案例:FPGA 到服务器方向 100G 长时间打流,服务器 tcpdump 显示完全没丢包,iperf3 却报告 0.5% 丢包率。排查到最后发现,iperf3 接收线程和服务器网卡 RX 中断绑在了同一个 CPU 核上,中断处理把应用线程抢占了。用 taskset 或 iperf3 的 -A 参数把收发两端绑定到不同核上,丢包直接清零。
反过来,如果 FPGA 作为接收方丢包,重点查这几处:发送端计数器是否真的发满了带宽;接收 FIFO 深度是否足够,100G 下链路另一端的一个短暂突发就能灌满小 FIFO;UDP 查表分发引擎有没有对某些包头走了较长路径造成流水线气泡。查这类问题最好的工具就是计数器:把发送包数、接收包数、FIFO 上下溢、ARP 命中失败全部做成寄存器,触发一次就知道丢包发生在哪一级。
5.3 抓包与计数器不一致:先怀疑自己,再怀疑链路
最后记一个调试时的经典错觉:Wireshark 或 tcpdump 看到的包数比 FPGA 发送计数器少,就断言 FPGA 丢包了。先别急着怀疑 FPGA,先检查抓包点。tcpdump 抓包结束时打印的统计里有关键三行:captured、received by filter、dropped by kernel。received 是内核里收到的报文数,dropped 是内核因为缓冲不足丢弃的报文数,只有 captured 才是真正写进 pcap 的。如果 dropped 不为 0,说明抓包工具自己在丢包,跟链路和 FPGA 都没有关系。
这次项目里,有一版测试结果显示 FPGA 发送了 1000 万个 UDP 包,服务器 tcpdump 只抓到 998 万个,吓出一身冷汗,最后发现是 tcpdump 缓冲区被瞬时大流量冲掉了几百个包。把 tcpdump 的 -B 参数调大、加上 -n 禁用反向解析之后,两边计数就吻合了。调试高速链路,任何“不一致”都要先把测量工具本身的可信度验证一遍,再下结论说设备有问题。
这次 100G 移植上板,我最大的体会是:开源代码把 100G UDP 的入门门槛从“抄代码”降到了“配约束”,但门槛并没有消失,只是转移到了配置、约束、调试这些看不见的地方。时序余量、PHY 配置、主机协议栈缓冲,任何一环都能让一个看似简单的 UDP 收发问题变得极其隐蔽。
最后分享一个实用习惯:100G 项目里,把 FPGA 内部所有关键计数器,包括发送包数、接收包数、FEC 错误数、FIFO 上下溢、ARP 命中失败数,全部引到可读寄存器,配合串口或者 ILA 随时打印。这个习惯帮我省下的排错时间,比写这些逻辑多花的几小时多得多。希望这次实战记录能让你在踩同样的坑时,少消耗一个通宵。