
做 100G FPGA UDP 这个方向有一段时间了。以前一直在 10G/25G 上折腾这回从开源仓库拉了一套 UDP 协议栈代码硬是把它搬到了带 100G 硬核 MAC 的板子上从源码阅读、接口适配到最后 iperf3 打流跑到接近线速整个流程走得并不轻松。过程中踩了不少坑尤其是 CMAC 的时钟约束、AXI-Stream 位宽对齐还有 Windows 下 UDP 接收缓存导致的丢包问题每一个都能让调试时间翻倍。这篇就把整个移植和上板测试的过程完整记录下来。适合正在做 FPGA 高速网络入门、手里有 100G 板卡但不知道从哪里下手的同学也适合那些已经能把 10G 跑通、想往 100G 迁升的人。我会把方案选型、代码适配、测试方法、问题排查都拆开讲尽量让不同基础的读者都能照着走。1. 项目整体设计与思路拆解做这个项目前我先把目标定清楚不是要从零写一个 UDP 协议栈而是把一个成熟的开源实现移植到自己的板卡上并且通过上板验证它真的能跑 100G。这个定位决定了后面所有的动作——源码选型、接口封装、测试手段都不是“我能不能写出这段逻辑”的问题而是“我怎么把别人的工程用起来”的问题。1.1 为什么选 UDP 而不是 TCP先从最基础的为什么开始。100G 以太网的线速大约是 103.125Gbps如果要做 TCP Offload EngineTOEFPGA 逻辑要处理连接管理、滑动窗口、超时重传、保序等一堆状态机资源消耗和工程复杂度都非常大。而 UDP 是无连接的没有 ACK、没有重传、没有流量控制协议栈只需要做三件事解析/打包 IP 头、解析/打包 UDP 头、校验和计算。在 FPGA 上这三件事加起来的逻辑资源可能还不到一个 100G MAC 硬核的五分之一。所以我现在面对的应用场景比如高速数据采集、网络测量、存储数据传输基本都是“能扔就扔”的业务UDP 正好是最高效的承载方式。测试工具也好找iperf3 打流、Wireshark 抓包都是现成的。1.2 开源方案怎么选我最终参考的是两个比较出名的开源项目一个是 Alex Forencich 的 verilog-ethernet另一个是 Corundum。前者是一个相对轻量的 MAC/IP/UDP/ARP 协议栈库代码模块划分清晰适合做协议层移植后者是一个完整的 100G FPGA 网卡参考设计打通了 PCIe DMA、描述符环形队列、中断管理、100G MAC 整个链路但复杂度也高得多。我的思路是分两步走先用 verilog-ethernet 里现成的 eth_udp 模块把 UDP/IP 协议层跑起来验证板卡上 100G MAC 通路是好的如果后续需要和主机 CPU 交换数据再上 Corundum 那套 DMA 架构。这样风险和调试难度都能分离开不至于一开始就被一大堆 DMA 描述符搞晕。设计上没有绕开任何一层物理层是 FPGA 板卡上的 QSFP28 光口或者 DAC 线MAC 层用 FPGA 自带的 100G 硬核比如 UltraScale 上的 Integrated 100G Ethernet Subsystem协议层用开源 UDP 栈用户接口最终暴露成标准的 AXI-Stream 或者简单的 FIFO。后面测试时FPGA 内部可以做回环也可以接外部 100G 网卡打流灵活度高很多。2. 移植前的准备工作与关键点分析移植不是拿过来就能跑的。开源代码默认的目标板卡和你的实际硬件之间存在很多差异这些差异如果不在动手前理清上板后只会把时间浪费在无效调试上。我大概花了一天时间做准备工作事实证明非常值。2.1 硬件平台与工具链准备这次用的是一块国产 UltraScale 板卡上面的 QSFP28 接口对应 FPGA 的高速收发器。需要确认几个硬件事实QSFP28 光口或 DAC 线的四通道 lane 是不是连到了 FPGA 的 GTY 收发器上参考时钟输入是哪个引脚走线有没有跨 bank。这些信息只能从板卡原理图和 XDC 约束里挖。板上的可编程时钟芯片在复位后默认输出的频率是多少。100G 硬核 MAC 的 GT 参考时钟一般需要 156.25MHz如果时钟芯片上电默认值是 100MHzMAC 根本起不来。光模块的中断、复位、使能引脚有没有被 FPGA 控制还是直接接地。很多调试问题都出在光模块被复位或者模块供电没使能上。工具链我用的是 Vivado 2022.2。100G 硬核 MAC 的 IP 必须用你安装版本对应的方式生成不同版本之间 IP 的 GUI 和输出文件结构变化不小照着老版本教程写会吃大亏。测试主机准备了 Windows 和 Linux 两台Windows 上面跑 Wireshark 和 iperf3Linux 那台装的是 Mellanox ConnectX-5 100G 网卡方便来回对比。2.2 开源 UDP 协议栈模块结构梳理verilog-ethernet 这个库的代码风格比较规整核心模块大致可以分成这几块eth_mac以太网 MAC 的接收和发送处理前导码、FCS 校验。eth_ipIP 层解析和封装负责 IP 头校验和。eth_udpUDP 层解析和封装。eth_arpARP 请求和响应负责把 IP 地址解析成 MAC 地址。eth_rx、eth_tx把上面的模块串成完整的接收/发送流水线。我实际用到的接收数据通路是 eth_rx → eth_ip 的 IPv4 和 UDP 解析 → 用户逻辑自定义端口过滤。发送数据通路则是用户逻辑给出目的 IP、目的端口和数据 → eth_udp 封装 → eth_ip 封装 → eth_tx 发包。这个分层方式和操作系统协议栈的思路是一样的只不过换成硬件流水线实现。读代码阶段我把每一层的包格式对齐了一遍尤其是 IP 头长度字段、UDP 长度字段、校验和算法。verilog-ethernet 里支持 UDP 校验和的增量更新即在每拍数据进来时实时累加不需要等整个包结束再算这对 100G 线速非常关键。2.3 明确要改哪些接口原仓库的 eth_udp 模块和 100G MAC 硬核之间不是直接就能接上的。最大的问题是总线位宽很多开源 UDP 栈设计是 8 位或者 64 位 AXI-Stream 接口但 100G MAC 的用户数据接口通常给到 512 位一旦总线位宽不匹配就必须要加位宽转换逻辑。另外复位和时钟关系也要改。verilog-ethernet 假设外部提供一个统一时钟和异步复位但 100G CMAC IP 会输出至少两个时钟一个 GT 参考时钟芯片内部用一个用户逻辑接口时钟。UDP 协议栈必须跑在 CMAC 输出的用户时钟域下否则跨时钟域会出问题。还有 AXI-Stream 握手信号的细节。100G CMAC 的 tvalid 和 tready 之间时序比较紧开源模块如果对 backpressure 处理得太简单线速跑到一半就可能断流。后面我把 512 位到 64 位的适配和异步 FIFO 放在了一起一次性解决了位宽和跨时钟两个问题。3. 核心移植过程从源码到上板这一节是整个项目的主体。我会把从创建 CMAC IP 到最终生成比特流的步骤拆开讲重点说清楚每一步为什么这么做以及在适配过程中最容易出错的地方。3.1 接入 100G MAC 硬核100G MAC 硬核在 Vivado 里的名字一般是 Integrated 100G Ethernet Subsystem它把 PCS、PMA、MAC 控制都封装好了。生成 IP 的时候有几个关键选项需要根据自己的板卡定线速率选定 100G对应参考时钟 156.25MHz。如果你板卡上的可编程时钟输出频率不是这个值需要专门配置时钟芯片。用户数据接口位宽我选了 512 位这也是大多数 100G 应用的默认选择。位宽越大数据总线频率越低时序越容易收敛。时钟方案选择独立或共享我让 RX 和 TX 都使用同一组时钟域简化了逻辑。前提是板卡和时钟芯片能支持这种配置。CMAC IP 生成之后工程里会出现一个例化模板。我照着模板把 CMAC 的 tx_axis_tdata、tx_axis_tkeep、tx_axis_tvalid、tx_axis_tready、rx_axis_tdata、rx_axis_tkeep、rx_axis_tvalid、rx_axis_tlast 这些信号引到顶层再接上自己写的 UDP 协议栈适配层。这里有一个很关键的坑CMAC 的 AXI-Stream 接口是 512 位一次传输就是 64 字节。但一个 UDP 小包比如 64 字节以太网帧刚好占一拍tkeep 不一定全 1。如果适配逻辑没有处理 tkeep 的边界情况很容易把多余的空字节当成数据发出去或者把有效数据切错位置。我在顶层专门加了一个 tkeep 对齐检查收到包后先看 tlast 和 tkeep 是否匹配不匹配直接报错丢弃。3.2 UDP 协议栈接口适配协议栈这边我没有直接用整个 eth_rx/eth_tx 封装而是把 eth_ip、eth_udp、eth_arp 单独拉出来用因为这样灵活性更高也好排查问题。适配的核心工作是位宽转换。100G CMAC 输出的 512 位数据首先进入一个异步 FIFO把时钟域从 CMAC 用户时钟切到协议栈逻辑时钟。这个 FIFO 用 Xilinx 的 axis_async_fifo IP 或者开源实现都行关键是深度要够。我最初用 512 深度的 FIFO 测小包没问题但打到 1500 字节的包时偶尔丢包后来发现是 FIFO 深度不够改成 2048 深度之后就好了。FIFO 出来之后数据还是 512 位的 AXI-Stream需要转换成 64 位或者 128 位送给 eth_udp。这一步用 axis_adapter 或者自己写一个简单的拼接逻辑都可以。我实际测试下来用 64 位接口跑 100G 线速时时钟频率要求会比较高时序容易紧张后来改成 128 位接口时序一下子好看了很多。所以如果你板卡上行有余力建议不要用 64 位直接上 128 位。eth_udp 的接口里需要提供目标地址解析。推荐的做法是把 ARP 模块和 UDP 模块配合使用收到 ARP 请求时自动回复发送数据前先查 ARP 表查不到就发 ARP 请求等待响应。这样才能保证对端网卡或主机能正常回包。3.3 时钟、复位与约束细节整个 100G UDP 设计里最容易翻车的是时钟约束。CMAC IP 生成时会自动带出一些关于 GT 时钟、用户时钟的约束但你自己加的协议栈逻辑所使用的时钟必须在 XDC 里显式声明 create_clock 或者用 set_property 指定来源。否则 Vivado 在做时序分析时就只能靠默认猜测结果就是布局布线后时序报告一片红。复位逻辑也要处理好。CMAC 的复位时序里有明确的先后顺序比如要先等 GT reset 完成、再释放 MAC reset。如果直接把板卡全局复位信号接到 CMAC 的 reset 上上电初期 MAC 内部可能还没有初始化完成链路起不来。我是把 CMAC 的 tx_reset_done、rx_reset_done 信号拉出来等这两个信号都拉高之后再过几十个时钟周期释放协议栈的复位这样最稳定。XDC 约束方面除了 CMAC IP 自动生成的约束我额外加了这些对 QSFP28 的复位和模块选择引脚设置输出约束。对光模块 I2C 引脚如果不使用设置成高阻或禁用输出。对协议栈内部的异步 FIFO 跨时钟域路径设置 ASYNC_REG 或 set_false_path避免 Vivado 误报时序违规。3.4 综合实现与时序收敛适配完代码、写完约束后第一次综合跑出来周期 3.2ns也就是大约 312MHz而我的协议栈逻辑目标时钟是 322MHz 附近。时序报了一堆 violation主要落在 UDP checksum 计算路径上。解决办法是把校验和模块内部插入两三级流水寄存器让每一级的组合逻辑变短。checksum 计算插入流水线是可以的只要保证同一个包的 checksum 用的是同一个包的起始状态就行。我是在包开始时把累加器清零然后在数据流经每一级流水时同步递减最后在包结束位置做增量修正。这其实是很多硬件协议栈的常规做法完全不影响正确性。加了三级流水之后时序全绿。综合资源情况大概是这样LUT 用了不到 8 万FF 六万多BRAM 二十几块。这个资源占用在 UltraScale 上非常轻松说明 UDP 协议栈并不吃资源真正的瓶颈还是在高速接口和布线布局。4. 上板测试流程与验证方法代码下载到板卡上并不意味着马上就能跑 100G。我习惯先把链路分成几段从底层到上层逐级验证每一段确认没问题再进入下一段。下面是我实际使用的测试顺序和方法。4.1 光模块链路与内部回环测试上电之后第一件事是确认光模块有没有正常 link。接上 QSFP28 光模块插上 DAC 线或者光纤用板卡的 ILA 抓 CMAC 的状态寄存器。如果 link 状态为 up说明物理层通了。如果 link 起不来先看参考时钟频率对不对再看光模块的 reset 和使能引脚有没有被拉错电平。我见过的最冤情况是可编程时钟芯片上电默认输出 100MHz没有跑初始化脚本导致 156.25MHz 参考时钟从未出现。物理层通了之后先在 CMAC 内部做回环测试。CMAC IP 自带一个回环开关可以把发送的数据从 TX 环回到 RX不经过外部光模块和线缆。这个测试用来确认 FPGA 内部的 MAC 收发通路是好的。我把发送侧接一个简单的数据发生器接收侧 ILA 监控只要能抓到回环数据MAC 通路就算过了。这个环节能过滤掉大量“明明是硬件问题却被当成逻辑问题”的折腾。4.2 外部连通性与 ARP 测试回环通过后把回环关掉接上外部主机。FPGA 侧设置一个静态 IP比如 192.168.100.10主机网卡设置同一网段的 IP。先在主机上 ping FPGA 的 IP如果能 ping 通说明 ARP 模块和 IP 层都没问题。ping 不通的情况下别急着查整体逻辑先用 Wireshark 在主机上抓包如果看到大量 ARP 请求但没有任何响应问题基本出在 FPGA 的 ARP 模块或者 MAC 地址写错了。如果能看到 ICMP echo 请求发出去但没有回包说明 ICMP 响应逻辑或者 UDP 协议栈里面的 IP 转发有问题。这里要注意一个小细节如果 FPGA 的 MAC 地址设为 00:00:00:00:00:00主机系统通常会忽略来自该地址的帧导致 ARP 响应看起来“发出去但没人理”。我最初踩过这个坑后来统一用 02:00:11:22:33:44 这类本地管理地址就正常了。ping 通之后再用一个简单的 UDP 发送工具从主机往 FPGA 发几个固定内容的数据包。FPGA 侧收到后把内容回传给主机主机上用自定义小工具验证回传内容一致。只要这一步过了UDP 通路的基本正确性就有了。4.3 iperf3 打流与 100G 带宽验证基础连通性通过之后开始正式打流。Linux 主机上直接跑 iperf3命令大概是这样iperf3 -u -c 192.168.100.10 -b 80G -t 60 -l 1400这条命令是让 iperf3 以 UDP 模式向 FPGA 发送 80Gbps 的流量持续时间 60 秒每个包负载 1400 字节。如果 FPGA 只是接收并统计可以在 FPGA 侧加上简单的计数器统计收到的包数和字节数对比主机发送的数据就能算丢包率。但这里有个问题iperf3 默认的 UDP socket 缓冲区非常小在 Windows 上尤其明显。如果不修改系统缓存区哪怕程序里设置了发送速率操作系统也会因为 socket 缓冲区不足而丢包。Windows 下要修改全局 UDP 系统缓存区也就是注册表里的 AFD 参数把 DefaultReceiveWindow 和 DefaultSendWindow 调大然后重启系统才能生效。Linux 下相对简单用 sysctl 调整内核参数sysctl -w net.core.rmem_max134217728 sysctl -w net.core.rmem_default134217728 sysctl -w net.core.wmem_max134217728 sysctl -w net.core.wmem_default134217728然后 iperf3 再跟一个-w 128M参数显式指定 socket 缓冲区大小。这一套改完之后80G 的流量能稳定跑到大约 78Gbps 左右去掉以太网帧间隙和协议开销这个数字已经很接近线速了。4.4 Wireshark 抓包与帧格式检查打流通过之后还需要抓包确认帧格式没问题。在主机上用 Wireshark 抓 FPGA 发出的 UDP 报文逐个字段核对Ethernet 头里面的源 MAC、目的 MAC 是否正确。IP 头里版本号、头长度、总长度、协议号UDP 是 17。UDP 头里源端口、目的端口、长度。校验和是否正确IP 头校验和尤其重要因为很多网卡驱动会在收包时丢弃校验错误的包。我在测试时遇到过一种情况包能收到Wireshark 里也能看到但 iperf3 统计到的丢包率很高。后来发现问题是 IP 头校验和算错了交换机或网卡会把一部分校验错误报文直接丢掉导致看起来是随机丢包。所以 Wireshark 里如果看到大量 bad checksum 标记一定要优先查校验和逻辑。额外说一个 Wireshark 的常见困惑。有时候设置了显示过滤器udp但状态栏计数里还是会出现 ICMP 报文很多人以为是过滤器失效了。这其实是正常现象显示过滤器只是隐藏不符合条件的报文并不会在抓包阶段把它们扔掉所有非 UDP 报文仍然会进到抓包文件里只是不被显示。想要在抓包阶段就排除 ICMP需要把 UDP 过滤器写进“捕获过滤器”而不是“显示过滤器”。搞清楚这个区别能省不少排查时间。5. 常见问题与排查技巧实录做这个项目过程中遇到的高频问题我整理成了一张表每一项都是实际踩过的坑基本照着排查就能定位到问题。5.1 高频故障速查表现象可能原因解决方法光模块 link 起不来参考时钟频率不对或没有初始化检查可编程时钟芯片确认 GT 参考时钟是 156.25MHzARP ping 不通源 MAC 地址异常或 ARP 模块逻辑错误用本地管理地址 02:00:xx:xx:xx:xx抓包确认 ARP 请求是否收到丢包率高但是链路正常UDP socket 缓冲区太小或 FIFO 深度不够修改系统 UDP 缓冲区增大 FPGA 内部异步 FIFO 深度包能收到但 checksum bad校验和计算逻辑错误分模块查 IP 头校验和和 UDP 校验和必要时用 ILA 对比逐字节结果时序违例出现在协议栈逻辑校验和路径太长在 checksum 累加路径插入流水寄存器分层处理iperf3 只能跑到 10Gbps 左右主机 CPU 瓶颈或者 Windows socket 限制换多队列网卡调整 RSS 设置Linux 下用多线程打流CMAC 状态显示 local fault光模块没有插好或线缆故障换线、换光模块检查物理连接后再看寄存器5.2 排查思路从现象定位到根因排查的过程其实是有套路的。我的习惯是先确认物理层再确认 MAC 层最后才查协议栈逻辑。如果光模块 link 是 down就不要浪费时间看协议栈直接查时钟、复位、光模块供电。如果 link 是 up 但收不到任何报文用 ILA 抓 CMAC 输出接口看看 MAC 层有没有有效数据这样能快速划分责任边界。曾经有一个问题让我折腾了一个晚上FPGA 能收到 ARP 请求但是发出的响应报文主机就是收不到。抓包发现响应确实从 FPGA 的 TX 接口发出了但主机网卡显示链路短暂 down 然后又 up包全被丢了。后来查下来是某个复位逻辑在收到特定包时产生了毛刺把 MAC 的 TX 通道复位了一下。改用 CMAC 自带的 tx_reset_done 信号作为复位释放条件并且对复位信号做了跨时钟域同步问题就消失了。这类问题的核心教训是高速设计里的很多“随机故障”本质都是时序或异步信号处理不规范。100G 这种速率下任何没有同步的跨时钟域信号都可能在某个极端时刻产生毛刺表现出来就是偶发丢包或者链路闪断。所以别嫌麻烦所有跨时钟域信号一律用两级同步器或者异步 FIFO 处理。5.3 小包线速打流的极限问题小包场景是 100G UDP 测试里最容易引发性能瓶颈的场景。64 字节的以太网帧加上前导码和帧间隔在 100G 线速下每秒要处理的包数量会非常恐怖。FPGA 侧如果包处理逻辑没有做成全流水FIFO 深度不够很容易出现瞬间溢出丢包。我测试时用 1518 字节大包能轻松跑到 80Gbps但把包长切到 64 字节后只能跑到一半多。问题定位在接收侧的 UDP 解析模块它在每个包头时有一些多拍判断逻辑导致回压时间太长。优化方式是把端口过滤、长度判断这些操作全部改成组合逻辑旁路单独用一拍完成然后把用户数据处理做成流式流水线效果立刻改善。如果你想做小包线速建议在设计阶段就开始考虑每包一拍的处理能力。所有需要查表的逻辑比如端口匹配、IP 过滤都要能在 tvalid 拉高后的第一个周期给出结果否则后面就是无底洞。最后再分享一个小技巧测试 100G UDP 时除了功能验证强烈建议在 FPGA 内部加一组计数器分别统计收到的总包数、总字节数、错误包数、CRC 错误数。上板之后不要只依赖 iperf3 或者 Wireshark 的结果因为它们看到的是主机网卡的视角和 FPGA 实际收包情况可能不一致。两边数据一对丢包发生在链路、网卡还是协议栈立刻就能分清楚。另外做移植时不要急着改源码。先把开源代码按原样在一个小工程里跑通仿真再对照实际板卡改动接口适配层这样遇到问题可以确定是源码本身还是自己的适配逻辑有问题。开源项目能省大量时间但它们的设计假设不一定匹配你的硬件环境留好退路才不会被卡死。跑通一次 100G UDP 只是第一步。后续如果想做完整网卡功能还得把 PCIe DMA、多队列、中断处理一层层加上去。这次移植的经验里最值钱的不是那几根线速跑满的测试日志而是对整个 100G 数据通路的理解——知道了每一段应该怎么验证、怎么排查换个板卡换个场景也不过是重新走一遍流程的事。