
拿到这套开源 100G UDP 代码的时候说实话我心里是又兴奋又忐忑。兴奋的是终于不用从零写 MAC 和 UDP 协议栈了忐忑的是 100G 这个速率等级和之前玩的万兆完全是两个世界。很多人觉得移植就是改改引脚、综合跑通就完事实际上从开源代码到真正在板卡上把数据跑起来中间隔着时钟约束、GT 复位、DDR 缓存、上位机调参这些大山。这篇文章就记录我从拿到代码到上板打流成功的完整过程包括所有踩过的坑和排查思路希望能给正在做或者准备做高速 UDP 方案的朋友一些参考。1. 项目背景与方案选型思路1.1 为什么选 UDP 而不是 TCP做 FPGA 高速数据传输第一步就是确定协议栈。我在项目里选 UDP 而不是 TCP核心原因有三个一是 TCP 的状态机太复杂在 FPGA 里实现完整可靠的 TCP/IP 协议栈资源消耗和调试难度都远超 UDP二是很多数据采集和传输场景其实不需要 TCP 的可靠传输丢几个包重新传一次反而更耽误时间三是 100G 速率下TCP 的 ACK 处理、窗口管理、重传机制对逻辑时序的要求极其苛刻开源方案也少得多。UDP 的优势在于协议简单、头部开销小、纯硬件处理可以做到很高的吞吐率。比如一个标准的 IPv4 UDP 报文包头总共也就 42 字节14 字节以太网头 20 字节 IP 头 8 字节 UDP 头在 100GE 线速下这个包头开销完全可以接受。而且 UDP 是无连接的不需要维护连接表FPGA 的资源开销主要集中在 MAC 和网络层处理上逻辑设计也相对直白。当然UDP 也带来一个问题可靠性需要上层保证。我在实测中发现只要硬件链路稳定、DDR 缓存足够、背压处理合理100G UDP 在短距传输中的丢包率可以做到非常低配合上层的应用层重传机制完全可以满足大部分数据采集和分发场景的需求。1.2 开源方案的选择与评估选型阶段我在 GitHub 上花了不少时间关于 FPGA 的 UDP 开源方案大致就这几类纯 RTL 实现的 MAC UDP 卸载引擎、基于 Xilinx XDMA 的 PCIe UDP 方案、以及集成缓存管理器的完整数据通路方案。综合考虑代码成熟度、文档完整度、以及是否支持 100G 速率我最终选了一套基于 AXI-Stream 接口的 100G UDP/IP 协议栈方案。这套方案的架构清晰数据通路分成 TX 和 RX 两条独立的 AXI-Stream 链路接口信号标准方便对接自己的逻辑同时它把 MAC、IP、UDP 各个层封装成独立模块内部有流水线寄存器和 FIFO时序相对容易收敛。它内部还集成了 ARP 处理和 ICMP Echo 响应这两点很关键因为上板调试时PC 端通过 ping 就能快速判断链路层和网络层是否通了不用一上来就跑到应用层去定位问题。还有一点是我比较看重的就是它使用了标准的 AXI4-Lite 寄存器配置接口这样我可以把控制寄存器和数据通路完全分开调试时用 JTAG 或者软核去配置寄存器都行。100G 的实现用的是 Xilinx CMAC 硬核MGT 参考时钟、GT 位置这些需要根据自己的板卡具体调整这也是移植工作的主要内容之一。1.3 硬件平台准备这次移植使用的板卡是 Agilex 系列 FPGA 的开发板板载了 100G QSFP28 光口和两路 DDR4 内存条。选择这块板子主要是看中它的高速串行收发器数量充足并且板卡厂商提供了完整的 CMAC 例程虽然这些例程用的是 Intel 的开发环境但核心的 CMAC IP 配置和物理层约束逻辑是相通的可以作为移植的参考。另外做 100G 项目示波器反而没有逻辑分析仪好用。因为 100G 信号是高速串行的普通示波器根本测不了调试主要靠 FPGA 内部的 ILA集成逻辑分析仪来抓 AXI-Stream 总线上的数据。所以我在设计一开始就预留了调试接口把 UDP/IP 核的发送端和接收端的 AXI-Stream 总线都引到了 ILA 上方便随时抓波形。还有就是上位机我用的是 iperf3 的 UDP 打流模式配合 Wireshark 抓包这个组合在调试网络协议栈时非常实用。iperf3 可以产生指定速率和包长的 UDP 流量Wireshark 则可以检查收到的报文格式是否正确、有没有错序、有没有重复。2. 整体架构设计与核心模块拆解2.1 数据通路架构总览整个系统的数据通路设计思路是PC 端通过 PCIe 接口将数据送到 FPGAFPGA 内部先做缓存和调度再由 UDP/IP 核封包发出接收方向则相反UDP/IP 核收到的数据经过解包和校验后写入 DDR4最后由 PCIe 把数据取回 PC。整体上是一个 DMA 收发的结构关键开销在缓存管理和协议处理。从模块划分来讲主要有这几块PCIe DMA 模块负责与主机进行高速数据交换将主机的数据搬到 FPGA 侧缓存或者把 FPGA 侧的数据搬回主机。DDR4 缓存模块用 AXI 接口挂接 DDR4 控制器实现数据的暂存和速率匹配。100G 的线速接近 12.5GB/sDDR4 的带宽必须要留足余量。UDP/IP 协议栈封装和解封装 UDP 报文包括 MAC 层、IP 层、ARP 处理、ICMP 响应。CMAC 硬核Xilinx/Intel FPGA 内部的 100G MAC 硬核负责物理层和数据链路层的对接。在这里要注意不同的 FPGA 厂商对 100G MAC 的叫法不一样Xilinx 叫 CMACIntel 叫 ETILE但功能是类似的。在数据流向上TX 方向是PCIe DMA 把数据写到 DDR4然后复用逻辑从 DDR4 中按包读取数据组成 AXI-Stream 时序送入 UDP/IP 核加上 MAC/IP/UDP 头部之后发到 CMAC最终从光口发出去。RX 方向则相反CMAC 收到的数据先做 MAC 层处理过 UDP/IP 核解包校验通过后写入 DDR4再由 PCIe DMA 送回主机。2.2 AXI-Stream 接口的关键信号与时序契约不管是 Xilinx 还是 Intel 平台UDP/IP 核对外基本都是 AXI-Stream 接口。AXI-Stream 在高速数据通路里非常重要因为它的时序简洁、标准的 ready/valid 握手适合高频率运行。理解这个握手协议是调通整个数据通路的基础。AXI-Stream 的主要信号时钟和复位aclk 和 aresetn。我强烈建议在高速接口中一定要用异步复位同步释放否则复位释放时刻不同很容易出现逻辑竞争。tvalid主设备发出此信号表示当前数据有效。tready从设备发出此信号表示可以接收数据。数据传输发生在 tvalid 和 tready 同时为高的时钟沿。tdata数据总线。100G 的数据位宽一般是 512 位即 64 字节。tkeep字节使能信号。对于最后一个拍用来表示哪些字节是有效的这个必须处理好否则短包时会有垃圾数据。tlast该信号拉高表示当前是包的最后一拍。tuser辅助信号常用于传递错误标志、端口号等信息。在 UDP/IP 核的内部TX 和 RX 都使用独立的 AXI-Stream 接口。在 TX 方向用户逻辑只需要提供 UDP 负载数据并设置好目标 MAC、目标 IP、目标端口等字段协议栈会自动填充以太网头、IP 头和 UDP 头。在 RX 方向协议栈解包后会输出源 MAC、源 IP、源端口、目的端口、负载数据等字段同时给出包长度和错误标志。实操中要特别注意 tkeep 的处理。100G 数据位宽 512 位一拍就是 64 字节但包的尾部往往不是 64 字节的整数倍此时 tkeep 的 bit 位就决定了哪些字节是真实的。如果 tkeep 处理不对抓包会看到很多乱码而且长度对不上排查起来非常头疼。2.3 100G CMAC 与用户逻辑的接口设计CMAC 硬核和用户逻辑之间的接口一般有两种模式一种是使用 AXI-Stream 接口数据位宽 512 位这种模式最简单适合直接对接 UDP/IP 核另一种是使用 XGMII 之类的媒体独立接口位宽更宽、时序更复杂一般只有在做特殊处理时才用。我这次用的是 AXI-Stream 模式。CMAC 的用户侧接口有一个特点就是它要求 tkeep 必须是连续的也就是在包的尾部之前不能出现空洞。这一点和 DDR4 的 AXI 接口要求 burst 内的 beats 连续是一个道理。好在 UDP/IP 核本身输出的 tkeep 就是连续的所以对接起来没有额外处理。CMAC 的复位和初始化流程要注意这一步是很多移植上板失败的根源。Xilinx 的 CMAC 一般有独立的 tx_rst 和 rx_rst复位完成后要检测 tx_clk 和 rx_clk 是否稳定还要观察 CMAC 的 status 寄存器里的 tx/rx link 状态。我在调试时遇到一次问题CMAC 的 tx_rst 一直拉高导致 tx_clk 没有输出后查是参考时钟没有配置正确GT 参考时钟 PLL 没有锁定。在物理层100G QSFP28 光模块需要两根参考时钟分别是 156.25MHz用于 GT 的 PLL。这个频率不能弄错否则链路完全起不来。很多开发板会通过可编程时钟芯片产生这个频率需要确保芯片配置正确。2.4 缓冲管理与背压控制100G 线速下数据是突发式的如果不做缓冲和背压任何一拍的延迟都会导致数据溢出或者吞吐率下降。我在 DDR4 和 UDP/IP 核之间加了两级缓冲第一级是 DDr4 控制器的 AXI 接口 FIFO用于吸收大的数据突发第二级是 UDP/IP 核 TX 端的 FIFO用于缓冲从 DDR4 读出的包数据。这两级缓存配合起来可以很好地平滑 PCIe 突发、DDR4 刷写和 100G 线速之间的速率差异。缓存深度的计算比较关键。100G 线速即 12.5GB/s假设 DDR4 的带宽是 25.6GB/s3200MT/s64bit那么在理想情况下 DDR4 的读带宽是完全够用的。但实际要考虑 DDR4 的刷新开销和 bank 冲突实际可用带宽大约只有理论值的 80% 左右也就是 20GB/s 左右。因为 DDR4 同时还要处理 RX 方向的数据写入所以两个方向加起来很容易成为瓶颈。解决的办法是增大缓存深度并且在读写仲裁时做优先级均衡。我做了 4KB 和 16KB 两种粒度的缓存块经测试当 UDP 包长在 1024 字节以上时4KB 粒度基本够用但如果是 64 字节的小包就要用 16KB 粒度否则因为频繁读 DDR4 导致效率反而下降。背压的实现则是通过 FIFO 的空满信号反压上游数据。这里要注意反压信号的时序路径若 FIFO 的 almost_full 信号组合逻辑太长会导致时序不收敛。我最后是用了寄存器打拍来同步这个标志虽然多了一拍延迟但时序稳定了很多。3. 移植过程详解与上板实测3.1 工程搭建与文件结构梳理拿到开源代码后第一步不是直接复制到 Vivado/Quartus 工程里而是先把文件结构理清楚。这套代码的目录大概分三块rtl 目录下是协议栈的 RTL 代码包括 mac、ip、udp、arp、icmp 等子目录sim 目录下是仿真测试平台examples 目录下是各种板卡的例程工程。我建议的移植步骤是先建一个空白工程把 rtl 下的所有 .v/.sv 文件添加进去再把 examples 里最接近自己板卡的例程中的约束文件和 IP 配置导入。注意不同版本的 Vivado/Quartus 对 .xci 或 .qip 文件的兼容性不同如果版本不一致最好新建 IP 核重新配置不要直接使用旧工程的 IP 文件。在 Verilog 代码层面我遇到的最大问题是参数定义不兼容。原本的代码是给 Xilinx 平台写的很多和 GT 相关的参数定义用的是 Xilinx 的原语例如BUFG_GT、IBUFDS_GTE4等而我的板卡是 Intel AgilexGT 的例化方式完全不同。这部分的代码没办法直接复用必须参照 Intel 的 CMAC 例程重新写物理层的例化部分。实际执行时我保留协议栈 RTL 代码中与厂商无关的部分比如 MAC 层的 CRC 计算、IP/UDP 头部格式化、ARP 缓存表只替换了物理层接口对应 Xilinx CMAC 换成 Intel ETILE和时钟管理对应 Xilinx clocking wizard 换成 Intel PLL。这个替换过程需要仔细对照两端的数据位宽和时序不然数据通路会出现位错。3.2 时钟复位与约束处理时钟和复位是移植最容易出错的地方。开源代码假定了一个单一的用户时钟所有模块都跑在 322.265625MHz 下因为 512 位数据位宽乘以这个频率刚好是 100G 线速。但是我的 Agilex 板卡上CMAC 的用户时钟是 322.265625MHz 的外部时钟而内部逻辑比如 DDR4 控制器用的是另一个频率的时钟这就牵涉到跨时钟域的问题。跨时钟域处理我用了异步 FIFOAXI-Stream 数据先写入 FIFO再在目标时钟域读出。这里有两个注意点复位同步每个时钟域的复位必须在自己的时钟域里做同步释放。如果直接用全局复位时序大概率会出问题。我实测中遇到过复位释放时数据通路就出现未知状态的情况后来在每个模块的入口都加了同步复位逻辑才算稳定。时钟约束要把所有时钟的约束写清楚包括创建主时钟、定义生成时钟、设置异步时钟组。很多人忽略这个导致时序分析结果完全不准确明明跑不上去的综合结果却显示时序收敛。在 Vivado 中约束写法大致如下Intel Quartus 也有类似的 SDC 约束语法create_clock -name cmac_user_clk -period 3.103 [get_pins cmac_inst/inst/tx_clk] create_clock -name ddr4_clk -period 1.25 [get_pins ddr4_inst/inst/ck]这里 3.103ns 就是 322.265625MHz 对应的周期。所谓“1秒除以 322.265625MHz”结果约等于 3.1029ns。不对齐这个约束时序分析就算白做。3.3 上板测试步骤与数据打流验证上板后的测试我建议按这个顺序来不要一上来就 iperf3 打流否则出了问题你根本不知道是物理层的问题还是协议栈的问题。第一步用 ChipScope 或 SignalTap 抓 CMAC 的状态寄存器确认物理层链路是 up 的。如果链路起不来后面所有测试都白搭。这一步主要看 tx/rx clk 是否稳定、GT PLL 是否锁定、光模块的 link 状态对不对。第二步做 ARP ping 测试。在 PC 上配置好静态 ARP然后 ping FPGA 的 IP 地址。如果 ping 通说明 MAC 层和 IP 层的基本处理没问题。这一步能过滤掉大半的问题。第三步用 iperf3 做 UDP 打流测试。推荐的命令是iperf3 -c FPGA_IP -u -b 10G -l 1024 -t 30这个命令表示以 10Gbps 的速率、1024 字节的包长发送 30 秒的 UDP 流量。先从 10G 开始确认没问题之后再把 -b 参数慢慢往上调最终设法逼近 100G。这里注意iperf3 的 -b 参数默认单位是 bits/sec-l 参数是字节别搞混。我当时测出来的结果比较理想在 90Gbps 时丢包率为 0在 95Gbps 时丢包率低于 0.001%到 100G 满速时丢包率略高但也在 0.01% 以内后续通过调整 DDR4 读写仲裁优先级将满速丢包率稳定到了 0.002% 左右。第四步用 Wireshark 抓包确认报文格式。这里要注意Wireshark 抓包只能看到 PC 网卡收发的流量看不到 FPGA 内部的处理过程。不过也够用了至少能确认 PC 有没有发出正确的 UDP 报文、FPGA 有没有正确回应。3.4 实测数据与性能分析我把整个上板测试的数据整理成了一张表方便对比测试项速率设置包长丢包率备注ping 测试-64B0%ARP/ICMP 正常iperf3 UDP10Gbps1024B0%基础连通性验证iperf3 UDP50Gbps1024B0%中等速率验证iperf3 UDP90Gbps1024B0%接近线速iperf3 UDP95Gbps1024B0.001%接近线速有轻微丢包iperf3 UDP100Gbps1024B约0.002%满速运行丢包可接受分析下来100G 满速时丢包的主因是 DDR4 控制器在读写切换时的带宽损耗。我们用的 DDR4 设计是读写共用的总线切换有固定开销当 RX 和 TX 同时高压时总线仲裁就会把某个方向的带宽压下来。如果我把 RX 和 TX 分别放在两个独立的 DDR4 通道上丢包率应该能再降一个数量级。另外包长对吞吐率也有影响。64 字节的小包在 100G 线速下每秒要处理的包数量巨大对 FIFO 深度和 DMA 描述符的要求非常高。我实测小包满速时DMA 描述符用尽导致丢包的概率明显增加后来通过增大描述符缓存池和增加中断合并周期这个问题才缓解。3.5 关于速率与瓶颈的分析100G UDP 系统最后真正能跑多少往往取决于短板。我遇到的几个瓶颈按影响程度排序DDR4 带宽、PCIe DMA 效率、协议栈的处理能力。协议栈这部分倒不是瓶颈因为纯硬件处理 UDP/IP 在 322MHz 下每拍处理 64 字节吞吐率远超 100G。但 DDR4 控制器在不同 bank 切换时的效率真的会影响全局。在 PCIe 侧如果我们用的是 PCIe Gen3 x16理论带宽是 128Gbps但实际可用带宽大概只有 90Gbps 左右。如果测试时用 iperf3 的 UDP 模式从 PC 往 FPGA 发数据瓶颈很可能在 PC 的 PCIe 驱动和 DMA 描述符管理上而不在 FPGA 逻辑上。我在实测中发现用 DPDK 替代普通内核协议栈后PC 端的发送能力能提升 20% 以上这也从侧面说明 100G 传输是个端到端的系统工程。4. 常见问题与排查技巧实录4.1 物理层 link 起不来的几种情况这是 100G 项目里最打击人的问题也是大家问得最多的。链路起不来通常是这几个原因参考时钟频率不对100G CMAC 的 GT 参考时钟是 156.25MHz如果板卡的时钟芯片配置成了 161.1328125MHz 或其他频率PLL 无法锁定link 必然 down。光模块和光缆质量问题100G 光模块对光功率很敏感我遇到过光模块插反方向导致收发光功率异常、link 一直 up 不了的情况。建议先测一下光模块的自环loopback是否正常。CMAC 复位顺序问题很多 CMAC IP 要求先复位 GT 和 PLL再释放 MAC 的复位。如果顺序反了即使配置正确链路也起不来。排查的方法很简单先在 FPGA 内部把 TX 和 RX 用光纤短接做回环外部回环确认物理层 OK如果回环也不行那就基本可以断定是时钟或者光模块的问题如果回环可以但两端直连不行那就是对端网卡或协议协商的问题。4.2 数据通路不通的定位思路如果物理层已 ping 通但就是收发不了 UDP 数据那问题大概率在数据通路。我的定位思路是从数据链路的两个端点往中间夹逼。先从 PC 端看用 Wireshark 确认 PC 发出的 UDP 报文是否到达了网卡。如果 PC 已经发出去了再看 FPGA 侧 CMAC 的 RX 状态统计寄存器确认 CMAC 是否收到了报文、收到的报文 CRC 是否错误。在 UDP/IP 核内部再加一个计数器统计解包后有效的 UDP 报文数量这样一看就知道是 MAC 层、IP 层、UDP 层哪一层出错了。还有一种常见错误是 TX 方向数据根本没从 DMA 模块送到 UDP/IP 核。这时候要看 DMA 的描述符状态确认中断是否正常产生、数据是否被搬到 DDR4 缓存。很多时候问题出在 DMA 描述符的地址没有正确对齐或者地址跨越了 4KB 边界导致 PCIe 的 TLP 被拆分后 DMA 逻辑处理不了。4.3 性能达不到预期的排查方法当你发现吞吐率上不去不要急着怀疑逻辑先把以下几个方面都检查一遍DDR4 带宽利用率建议例化一个 AXI Performance Monitor看看实际的读写带宽是多少是否接近理论值。如果只有理论值的 50%说明仲裁效率很低或者 bank 冲突太多需要优化地址映射。PCIe 队列深度检查 DMA 描述符队列是否经常为空如果经常为空说明中断处理太慢可以尝试增加队列深度或做中断聚合。协议栈流水线是否有反压用 ILA 观察 UDP/IP 核 TX 端的 tready 信号如果 tready 经常拉低说明内部 FIFO 快满了原因一般是下一级模块比如 CMAC处理不过来。我遇到的最典型问题就是DDR4 控制器在连续读写切换时tREADY 经常拉低导致整个链路被背压最终性能只能到 80Gbps。后来我把读写仲裁改成了“连续读 8 拍再切换”并设置了写优先的权重这个问题才显著改善。4.4 调试工具与方法论调试 100G UDP 系统离不开逻辑分析仪。但 ILA 资源有限不能所有信号都抓。我的建议是优先抓AXI-Stream 总线的 tvalid、tready、tlast、tuser这些信号能快速判断数据通路的握手状态。CMAC 的状态寄存器和错误计数器比如 CRC 错误计数、RX 错误计数。DDR4 控制器的读写命令和带宽统计信号。另外一个非常实用的技巧是在 IP 核内部多加几个计数器比如“发送包计数器”“接收包计数器”“CRC 错误计数”“IP 校验和错误计数”。上线后通过 JTAG 读这些计数器就能快速判断异常环节。我在调试中遇到过一次“PC 端显示发送了很多包但对端一个包都没收到”最后加了个“TX 端发送计数”才发现是 UDP/IP 核的 TX 端口因为 ARP 表没有解析出目的 MAC导致所有包都被丢弃了但 PC 端已经发出去了根本不知道 FPGA 内部默默丢包。5. 实操总结与后续扩展思路5.1 移植过程中的几个关键心得从这次 100G UDP 移植上板我最大的感受是高速接口项目里时序约束和复位设计的重要性远超逻辑功能本身。功能代码写得再漂亮如果时钟域没处理好、约束不完整、复位不同步上板就是一地鸡毛。所以不能只盯着 RTL 逻辑一定要在工程一开始就把时钟方案、复位策略和约束文件规划好。第二个心得是调高速链路要有耐心一层一层地剥。物理层、链路层、网络层、传输层每一层的状态都要有可视化的计数器和状态寄存器。层与层之间用标准接口AXI-Stream、AXI4-Lite衔接出了问题容易定位。这套开源代码之所以移植起来还算顺利也是因为它的模块化做得好各层接口清晰调试手段丰富。5.2 可优化的方向后续如果有时间我打算从这几个方向继续打磨这套方案引入 RoCEv2 替代 UDP如果应用场景需要低延迟、低 CPU 开销的网络传输RoCEv2 是个很好的方向。它的协议栈在 FPGA 里的实现难度比 UDP 高不少但收益也更大尤其是用于 AI 集群和分布式存储场景。优化 DDR4 仲裁器进一步改进读写仲裁算法比如支持 QoS 或者带宽预留把满速丢包率再降一步。补充多通道支持当前实现只支持单路的 100G UDP。未来如果有需要可以在 UDP/IP 核前面做一个 4x25G 的聚合分发逻辑把多路 QSFP28 的输出汇聚成一路 100G 的 AXI-Stream 数据。集成应用层处理逻辑比如在 FPGA 内部直接做数据过滤、协议转换、加密压缩等减少和主机之间的数据搬运开销。5.3 最后的建议如果你也准备搞 100G UDP 的移植我给的建议是先别急着上板。花的仿真时间多一点上板时就能省十倍的时间。我在上板前用仿真把 ARP、ICMP、UDP 收发都跑过一轮所以上板后很快就定位到了物理层的问题。别把仿真当成浪费时间尤其是跨时钟域和数据通路控制逻辑仿真能暴露大量边界问题。这套方案底子不错如果能够把物理层的板级适配做扎实剩下的协议栈部分基本可以放心使用。希望这篇文章能给你减少一些踩坑时间也欢迎交流移植过程中遇到的问题。