news 2026/9/8 13:21:35

FPGA 100G UDP协议栈移植实战:从开源方案到线速收包

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
FPGA 100G UDP协议栈移植实战:从开源方案到线速收包

先把结论放在前面:这个项目本身不复杂,但真正的复杂度全藏在“移植”两个字里。我从拿到一块带 QSFP28 光口的 UltraScale+ 板卡,到把开源 100G UDP 协议栈跑起来、双侧验证线速收包,前后折腾了大概两个礼拜。期间踩过了光模块兼容、时钟域处理、校验和丢弃、iperf3 参数误导等等一堆坑。这篇文章就把整个过程拆开讲清楚,包括环境选型、代码移植、上板测试、问题排查,尽量做到你照着走一遍就能复现。

适合看这篇文章的人有两类:一类是刚接触 FPGA 网络方向、想快速把 UDP 协议栈跑在高速链路上的同学;另一类是已经在做 100G/200G 数据面开发,想用开源方案做原型验证的工程师。我会尽量把“为什么这么做”也写出来,而不是只给一份能跑通的代码。

1. 项目概况与整体设计思路

1.1 为什么选 100G UDP,而不是自己写协议栈

FPGA 做网络数据处理,最早大家都是从千兆、万兆 Ethernet 开始,自己写一个 MAC 控制器、一个 UDP 解析状态机,调通就很有成就感。但到了 100G 这一档,局面完全变了:数据位宽从 32bit、64bit 提升到 512bit,时钟频率从 125MHz 拉到 300MHz 以上,光模块从 SFP+ 变成 QSFP28,MAC 的物理编码子层还涉及 64B/66B 编码、RS-FEC 这些细节。如果每个模块都从零写,光是把 PCS/PMA 调稳定就得花掉好几周。

所以业界主流做法是:物理层和链路层直接复用成熟的 IP 或者开源库,UDP 协议解析只是薄薄一层逻辑,自己只专注于业务功能。100G UDP 的好处在于,UDP 本身没有连接状态、没有确认重传,协议处理简单得就像一个“包裹分发中心”——查一下目的端口,把 payload 拿去用就行。对 FPGA 来说,这意味着可以做到很深的流水线,每周期吞吐固定,容易达到线速。

这次移植的另一个动机是验证“开源方案在 100G 场景下到底能不能跑”,而不是听别人说可以。很多开源以太网 IP 库在仿真里表现得很好看,一上板就暴露出跨时钟域、时序违例、手上协议处理不完整等问题。我这次选择的是社区里维护比较活跃的 verilog-ethernet 项目,它有 10G/25G/40G/100G MAC 的 Verilog 实现,以及 ARP、IP、UDP 的 offload engine,正好覆盖我想要的完整链路。

1.2 开源方案选型与数据通路设计

我最终采用的方案结构,可以理解为一条单向数据流水线:

QSFP28 光模块 <-> 100G MAC (Xilinx CMAC IP) <-> UDP Offload Engine (开源) <-> 用户业务逻辑

CMAC 负责物理层和链路层,输出标准的 AXI-Stream 接口,数据位宽我配置成 512bit,用户时钟大概在 300MHz 上下。开源协议栈这边,拆成几个核心模块:

  • eth_mac_100g或者直接对接 Xilinx CMAC,负责以太网帧的封装和解封装;
  • eth_arp负责处理 ARP 请求和响应,保证主机能通过 IP 找到 FPGA 的 MAC 地址;
  • eth_ip负责 IPv4 头的解析和校验;
  • eth_udp负责 UDP 头的解析,提取源端口、目的端口、长度信息。

用户业务逻辑在本次测试里做的是“回环”:把 FPGA 收到的 UDP payload 原封不动再发回给主机。这样一个简单的应用层就能同时验证 RX 和 TX 两条路径,把 MAC 和 UDP 协议栈的问题暴露得干干净净。后续如果做实际项目,只需要把这一层替换成 DMA 搬运到 DDR、或者做数据包过滤分发,就能复用整条已验证的链路。

这里也解释一下为什么不选商用 IP:商用 UDP Offload IP 确实更完整,还带 AXI-Lite 寄存器接口和中断控制,但价格不便宜,而且很多功能在这个阶段用不上。开源方案的好处是代码全可见,出了问题能直接拉仿真波形一眼看到底,调试效率高很多。代价是需要自己处理一些边界情况,比如 UDP 校验和为 0、分片 IP 包等。

2. 环境准备与硬件选型

2.1 板卡、光模块与测试网卡

这次用的板卡是一块基于 Xilinx UltraScale+ KU15P 的第三方开发板,板载两个 QSFP28 光口,没有 PCIe 接口,纯粹做网络设备原型足够。开发板本身带 16GB DDR4,但这次回环测试没有用到,我后面会说在什么场景下才需要挂 DDR。

光模块选了 QSFP28 的 100G SR4 光模块,配 MPO 光纤跳线。这里有一个经验:如果只是板卡和主机之间点对点测试,其实我更推荐用 QSFP28 DAC 铜缆,距离短、信号稳定、不用考虑光模块清洁问题,还便宜。我在测试早期因为手头只有光模块,踩到了信号质量导致的 CRC 错误,后面排查了很久才锁定是光路信号问题。

主机侧需要一张支持 100G 的网卡。我用的是一张 Mellanox ConnectX-5 的单口 100G 网卡,插在服务器的 PCIe 3.0 x16 槽上。选网卡的时候注意两点:第一,驱动要支持你要用的 Linux 发行版,Mellanox 的 mlx5_core 驱动在主流内核里都有;第二,网卡固件和驱动版本最好更新到较新版本,老固件在对接非标准链路时经常有兼容性问题。

2.2 工具链版本与 IP 配置

Vivado 版本我用了 2021.2,这个版本对 UltraScale+ 和 CMAC IP 支持得比较完善。工程语言选 Verilog,仿真用的 Vivado 自带的 XSim,因为工程不算大,跑整个 UDP 回环的仿真也就几分钟。

CMAC IP 的配置有几个关键参数,我整理成了表格:

参数配置值说明
Line Rate100G必须和光模块/网卡速率匹配
Datapath Width512 bitAXI-Stream 数据位宽,高速率下必须宽位宽
RS-FECDisabled短距离直连测试先关掉,减少变量
PCS/PMA100GBASE-R标准的 100G 以太网物理编码子层
RX Flow ControlDisabled回环测试用不到流控
TX Flow ControlDisabled同上

RS-FEC 这里多说一句:100G 长距离传输(比如 LR4 光模块)通常需要开启 RS-FEC 才能保证误码率,但板卡和主机在一个机房里用短光纤/DAC 互联时,关闭 RS-FEC 能少一层编解码延迟,也少一些排查点。前提是链路两端配置保持一致,如果主机网卡是强制开启 RS-FEC 的,那 FPGA 侧也必须开。

CMAC IP 生成之后,会自动带出 AXI-Stream 接口和几个时钟/复位信号。这里要特别注意的是,CMAC 的 RX user clock 和 TX user clock 是独立的,后面逻辑设计千万不能想当然把它们当成同一个时钟域,否则上板会出现偶发的数据错乱,仿真还很难复现。

3. 移植落地:从 RTL 到比特流

3.1 源码结构与顶层搭建

verilog-ethernet 项目的目录结构大致长这样:

verilog-ethernet/ rtl/ axis/ # AXI-Stream 通用组件(FIFO、跨时钟域等) eth/ # MAC、ARP、IP、UDP 协议栈核心 lib/ # 一些通用库函数 example/ xilinx/ # 现成的 Xilinx 平台 demo synth/ vivado/ # Vivado 工程生成脚本

我这次没有直接用 example 里的现成工程,因为它的顶层是针对特定板卡的,和我的 CMAC 实例化方式不一定匹配。更好的做法是自己搭一个顶层,把开源库的 RTL 文件加进工程,实例化 CMAC IP,然后手工连接接口。

顶层模块的信号接口,最少需要这几组:

module udp_loopback_top ( input wire clk_tx_user, // CMAC TX user clock input wire clk_rx_user, // CMAC RX user clock input wire rst_tx_user, input wire rst_rx_user, // 从 CMAC RX 侧出来的 AXI-Stream input wire [511:0] s_axis_rx_tdata, input wire [63:0] s_axis_rx_tkeep, input wire s_axis_rx_tvalid, input wire s_axis_rx_tlast, input wire s_axis_rx_tuser, // 进 CMAC TX 侧的 AXI-Stream output wire [511:0] m_axis_tx_tdata, output wire [63:0] m_axis_tx_tkeep, output wire m_axis_tx_tvalid, output wire m_axis_tx_tlast, output wire m_axis_tx_tuser );

这里 AXIS 的tkeep是 64bit,对应 512bit 数据的每一个字节是否有效。CMAC 输出的tuser里带了一些错误标记,比如 RX 侧的 CRC 错误、协议错误,这个信号在调试阶段特别有用,建议直接接到 ILA 探针上。

顶层内部把 CMAC 的 RX 出口接到开源库的eth_udp_rx,解析出 payload 后送入一个回环 FIFO,再从eth_udp_tx拼上以太网头发回去。这个回环 FIFO 除了缓冲数据,还做了一个重要的跨时钟域隔离:RX 侧时钟进,TX 侧时钟出,避免两个时钟域直接连导致亚稳态。

3.2 时序收敛与资源优化

100G 数据路径上,时序收敛是第一优先级。CMAC 用户侧接口我配置成 512bit 位宽,实际跑起来用户时钟在 300MHz 左右。开源协议栈的逻辑不复杂,理论上跑 300MHz 问题不大,但在具体实现时我发现两个容易拖慢时序的点。

第一,回环 FIFO 如果直接用 Vivado 的普通 FIFO IP,在 512bit 位宽下 LUTRAM 或者 BRAM 的选择有讲究。我用的是标准 BRAM FIFO,配置成“First-Word Fall-Through”模式,这样可以直接拿到dout的 valid 信号,省一个寄存器的判断,能压掉几个纳秒的路径延迟。

第二,eth_udp_tx模块里计算 UDP 长度字段用的加法器,如果做在关键路径上,长帧还好,短帧会拖累整体频率。开源代码其实已经考虑到了这一点,长度字段是两个周期之前就算好的,但我第一次移植没仔细看,硬是把长度计算和发送使能逻辑挤在同一个状态机里,结果时序报告一片红。后来老老实实按原代码的流水线节奏走,综合频率一下就上去了。

最终资源占用方面,纯协议栈部分只用了不到 2% 的 LUT 和 1% 的 BRAM,剩下的资源全留给业务逻辑。这也是高速 UDP 方案的一个优势——协议处理开销极小,根本不需要动辄几百万门的 IP。

4. 上板测试:验证方法与问题定位

4.1 硬件连接与链路检查

上板之前先把物理环境准备好。我用 DAC 线缆把 FPGA 板卡的 QSFP28 口直接连到主机的 100G 网卡,省去光模块和光纤的折腾。上电后先看 CMAC 的status信号,重点检查rx_block_locktx_block_lock是否为高。这两个信号代表 CMAC 的 PCS 层已经锁定到 100G 链路,如果一直拉不起来,多半是物理层的问题,比如线缆没插紧、对端网卡 down 了、或者速率配置不匹配。

主机侧,先确认网卡识别到了口子:

$ ip link show enp3s0np0 8: enp3s0np0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP mode DEFAULT group default qlen 1000

给网卡配一个静态 IP,并且关闭可能干扰测试的自动协商相关功能。100G 网卡一般会在驱动里自动协商链路,只要物理层正常,ethtool enp3s0np0应该能看到Speed: 100000Mb/s

这一步最容易踩的坑是:FPGA 板卡上电后 CMAC 默认没有输出信号,主机网卡会一直认为链路 down。要确认 FPGA 的 bit 文件已经加载、并且 CMAC 的复位已经被正确释放,主机的网卡状态才会变成 UP。我调试时遇到过好几次“主机网卡死活起不来”,后来查到底层原因是 fpga 工程的复位逻辑里把 CMAC 复位拉得太久了,释放复位的那一刻主机已经放弃了链路协商,重启网卡就好。

4.2 功能测试:从内部回环到 UDP 透传

链路起来之后,不要急着跑业务,先做 CMAC 内部回环测试。这个测试的目的是验证 FPGA 侧 TX/RX 数据通路的正确性,完全绕开光模块和主机。

CMAC IP 自带一个回环模式寄存器,通过 AXI-Lite 接口把它设置成内部回环,然后从主机侧向 FPGA 发 UDP 包,FPGA 侧的 TX 路径会把包原样送回到 CMAC 的 RX 路径,主机网卡就能收到自己发出去的包。此时 Wireshark 上会看到很多“自己发给自己的包”,不要惊讶,这恰恰说明 MAC 收发通路没有问题。

内部回环通过后,关闭回环模式,进入正常收发。主机侧用 Scapy 构造一个简单的 UDP 包发到 FPGA:

from scapy.all import * sendp(Ether(dst="02:00:00:00:00:10")/IP(src="192.168.1.20", dst="192.168.1.10")/UDP(sport=12345, dport=5001)/b"hello_fpga", iface="enp3s0np0")

如果 FPGA 里的回环应用正常工作,主机上同时跑一个 Wireshark 抓包,应该能收到 FPGA 发回的 UDP 包。这里要注意,抓包要监听在网卡物理口上,不要开混杂模式以外的过滤,否则可能漏掉一些异常包。

如果这一步收不到回包,第一步先看 ILA 里 CMAC 的 RX 接口有没有tvalid拉高,如果没有,说明包根本没进 FPGA 逻辑;如果有,再往 UDP 解析模块后面追,看是 ARP 没回,还是 UDP 端口匹配不上。我把常见的排查手段放在后面专门讲。

4.3 性能测试:iperf3 UDP 打流

功能通了,下一步就是看能不能顶得住线速压力。iperf3 是常用的打流工具,UDP 模式下可以作为简单的压力源和服务端。

先在主机上起服务端:

iperf3 -s -u -p 5001

再起客户端,以 80Gbps 的速率向 FPGA 打流:

iperf3 -c 192.168.1.10 -u -b 80G -t 30 -l 1472 -P 4

这里-l 1472是 UDP payload 长度,1472 是标准 1500 字节 MTU 减去 IPv4 头 20 字节和 UDP 头 8 字节后的最大值,这样发出去的单包不会触发 IP 分片。-P 4表示 4 个并发流,iperf3 新版本支持,但老版本对 UDP 多线程支持不好,你也可以直接开 4 个 iperf3 客户端进程。-b 80G给点余量,不要打满 100G,避免网卡或 PCIe 瓶颈导致结果失真。

测试结果我这边记录下来的是:持续 30 秒打流,主机侧显示接收带宽稳定在 78.9 Gbps,丢包率 0%。这个带宽已经比较接近 UDP payload 占线速的比例了,因为 100G 以太网线速里本身就要扣除帧间隔、前导码、以太网头和 IP/UDP 头的开销,能跑到 79G 说明 FPGA 基本做到了线速转发。

再用几个不同的包长跑一轮,目的是验证小包场景下的处理能力。64 字节小包是网络设备最怕的场景,因为每秒包数会飙到 148Mpps,FPGA 逻辑如果每周期只能处理一个包,在 300MHz 时钟下根本来不及。开源协议栈在小包场景下的表现取决于模块处理状态机的效率,实测下来 64 字节包能跑到大概 80% 线速的包速率,这个数据在纯软件协议栈里是望尘莫及的。

4.4 长稳测试与数据校验

功能测试和短时间打流通过后,还要做长稳测试。我跑了 1 小时满带宽 UDP,同时用 Wireshark 的统计功能定期看有没有 TCP 乱序、重传或者校验和错误。长稳测试暴露的问题和短时间测试完全不一样,很多偶发的 CRC 错误、FIFO 溢出都要跑到十几分钟才能浮现出来。

数据校验如果用 iperf3 的 UDP 模式,它只统计包的个数和字节数,不校验内容是否正确。要验证内容一致性,我用 SCAPY 构造了一批带序列号的 packet,FPGA 回环后在大批量数据里检查序列号有没有跳变,结果全部连续,确认回环路径上没有丢包和串包。

顺带提一下 DDR 的话题。如果应用需要把 UDP payload 缓存到 DDR 再转发,那一定要做 DDR 带宽的预算:100G 双向就是 25GB/s 的读写压力,DDR4 单通道理论带宽只有 25.6GB/s,实际随机访问效率能到 70% 就算不错,所以通常需要两到四通道 DDR4。这也是很多 100G 智能网卡板卡要做四通道 DDR 的原因。本次回环测试用不上 DDR,不展开。

5. 常见问题与排查技巧实录

5.1 链路通了,但收不到任何包,先查这几处

这是我最常被问到的问题。链路层 UP 只代表 PCS 锁定了,不代表 MAC 能正确解出以太网帧。建议排查顺序:

  1. 用 ILA 抓 CMAC 的 RX AXI-Stream 接口,看tvalid有没有拉高。没有拉高,说明 MAC 收到的数据没进入用户逻辑,查 CMAC 的rx_error相关信号,是不是有 CRC 错误。
  2. 如果tvalid有脉冲但后面的eth_udp_rx没有输出 payload,查是不是 ARP 没有完成。FPGA 侧如果没有配置静态 ARP 表,收到一个目的 IP 不认识的包时会先回 ARP 请求,如果 ARP 请求没发出去或者响应没回来,数据包会被丢弃。
  3. 检查主机侧有没有正确配置 ARP。最简单的办法是在主机上加一条静态 ARP:
arp -s 192.168.1.10 02:00:00:00:00:10

这条命令把 FPGA 的 IP 和 MAC 绑定,省去 ARP 协商这一环,很多问题就能缩小范围。

5.2 UDP 校验和不处理,主机端疯狂丢包

开源协议栈默认可以配置是否计算 UDP 校验和。我一开始图省事,把校验和置 0 直接发出去,结果主机网卡驱动在开启 RX checksum offload 的情况下会丢弃这些包,表现为“包发出去了但收不到回包”。

排查这个问题,用 Wireshark 抓包最容易看出来:如果抓到的包显示UDP checksum: 0x0000 (not verified),而主机协议栈不认这种包,那就要么在 FPGA 侧正确计算 UDP 校验和,要么把主机网卡的 RX checksum offload 关掉。正规做法是前者,代码里把 UDP 校验和计算打开,开销只额外占用几十个 LUT,不痛不痒。

5.3 时序违例,尤其是跨时钟域导致的偶发错误

100G 方案里,RX 和 TX 时钟域独立,如果代码里直接把 RX 数据信号交给 TX 时钟域的寄存器,轻则时序违例,重则出现极难复现的亚稳态错误。这类 bug 是仿真杀手,因为仿真里的时钟是理想模型,跨时钟域不会有真实延迟,所以仿真全通过,一上板就跑飞。

凡是跨时钟域的接口,一律经过异步 FIFO。verilog-ethernet 项目自带axis_async_fifo模块,直接把 CMAC RX 出来的 AXIS 流进入 FIFO,再从 FIFO 输出到 TX 侧,数据和 valid/ready 信号全部同步,问题就自动消失了。

5.4 iperf3 参数没理解透,导致测速结果失真

iperf3 的 UDP 测试有个很容易误会的地方:-b指定的速率是发送端的目标速率,不是实际链路速率。如果你把-b设成 20G,即使链路是 100G,测出来也只有 20G,这是符合预期的。另外,iperf3 UDP 模式下客户端默认只看发送端统计,如果想要看接收端视角,要在服务端那侧看它的 Summary,或者加-R反转测试方向。

如果测出来带宽明显偏低,先排除主机侧瓶颈:PCIe 带宽够不够 100G?网卡有没有 SR-IOV 或者 RSS 多队列?驱动程序是不是ethtool -K关闭了某些 offload 功能?这些都是纯软件侧的问题,和 FPGA 无关。

5.5 物理层误码率高的排查

如果你发现 CRC 错误持续增长,但逻辑层面找不到问题,大概率是物理链路质量问题。先用ethtool -S enp3s0np0看网卡侧的rx_crc_errors计数,如果增长很快,检查线缆是否松动、光模块的清洁、有没有弯折过大的光纤。我遇到过换了根 DAC 线缆就彻底解决 CRC 问题的情况,这类问题排查成本不高,但很容易让人误判成逻辑 bug。

写在最后

这次移植做完,一个很深的体会是:100G 数据传输的瓶颈往往不在 FPGA 逻辑本身,而在接口对齐和时钟域处理上。开源协议栈把 UDP 解包和组包的细节处理得比较完备,但工程落地时真正让人熬夜的,永远是那些仿真看不出来的边界情况。

最后分享一下我常用的小技巧:上板调试阶段,在 CMAC 的 RX 和 TX 接口上各挂一个 ILA,配置成 512bit 数据宽度,深度 4096。抓拍条件设为tvalid && tlast,这样每次抓到的是一个完整的包。比较 RX 和 TX 的抓包波形,如果两边数据完全一致,基本可以确认转发路径没有丢改数据;如果从某一段开始不一致,就从那个位置往前逆推,很快能找到是哪个模块出了问题。

如果后续想把这条路用到实际项目里,扩展的方向大致有三个:一是加 DMA 路径,把 UDP payload 直接搬到 DDR,做好缓存管理;二是加多队列分类,按五元组或者自定义规则分流到不同处理通道;三是考虑更低时延的方向,比如绕过 MAC 层直接操作光模块接口。任何一个方向展开都是独立的大工程,但有了稳定的 100G UDP 基座,后面再盖楼就从容多了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 13:21:18

PixVerse深度图控制:AI图像生成空间布局精准实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/8 13:20:25

从央媒到自媒体,2026优质GEO发稿平台推荐,适配各行企业

一、核心概念界定(一)GEO生成式引擎优化与传统软文发稿的本质分野本文聚焦GEO生成式引擎优化(GenerativeEngineOptimization)&#xff0c;将其与传统SEO、普通软文投放做出清晰区分。传统新闻发稿、软文推广更多以搜索引擎收录、网页可见度、关键词网页作为考核目标&#xff1b…

作者头像 李华
网站建设 2026/9/8 13:19:09

金蝶云星空集成Delphi DLL与FastReport实现企业级自定义打印

1. 方案选型&#xff1a;为什么是金蝶云星空加Delphi DLL1.1 一个老牌ERP遇上的新打印难题金蝶云星空企业版在制造业、流通业里用得很广&#xff0c;单据流转、存货核算、BOM管理都靠它。但真到了实际业务现场&#xff0c;总会碰到一类让人挠头的问题&#xff1a;标准打印模板不…

作者头像 李华
网站建设 2026/9/8 13:19:01

Spring Boot 3.x升级实战:新特性、自动装配及坑点解析

SpringBoot新版本出来的时候&#xff0c;圈子里总有一波“升还是不升”的争论。我个人的态度一向是&#xff1a;先搞清楚新特性解决什么问题&#xff0c;再决定要不要跟进。Spring Boot 3.x 系列推出已经有一段时间了&#xff0c;从 3.0 到 3.2、3.3&#xff0c;再到现在的 3.4…

作者头像 李华
网站建设 2026/9/8 13:18:54

Delphi集成Python结巴分词:老项目中文分词实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/8 13:18:46

视频监控中路人头部椭圆虚化:从算法到工程落地实践

监控画面里突然闯入一个路人&#xff0c;脸正对着镜头&#xff0c;这时候如果直接录像保存&#xff0c;就把无关人员的面部信息也记录下来了。畅联云平台里的“路人头部椭圆虚化”功能&#xff0c;解决的就是这个很具体又很敏感的隐私保护问题&#xff1a;在视频流实时处理过程…

作者头像 李华