
我不知道是不是只有我一个人有这种感觉打开一个开源的 100G UDP 协议栈工程时第一眼觉得“这不就是改改引脚约束、烧个 bit 就行了吗”真到了板子面前才发现从参考工程到自己跑通线速中间隔着一整条银河。这篇文章我把最近一次从 verilog-ethernet 协议栈移植到 UltraScale 板卡、最终把 100G UDP 跑通并用打流工具验证到接近线速的完整过程写下来包括方案选型、模块原理、移植细节、上板调试链路和实测数据给正在做 100G 数据采集、图像传输、或者想从 40G 往 100G 迁移的 FPGA 开发者一个能直接参考的路线图。1. 为什么偏偏是 100G UDP应用场景与方案取舍1.1 哪些项目真的需要 100G 以太网先说应用场景。很多做 FPGA 的工程师一听到 100G第一反应是“数据中心交换机才用得上吧”。但实际上这几年 100G UDP 在非数通领域的需求涨得非常快我接触到的主要有三类第一类是高精度仪器和实验室设备。比如示波器、信号分析仪、质谱仪实时采集到的数据量动不动就是几十 Gbps以前走 PCIe 插卡方案要装驱动、要处理 DMA 中断换成 100G 以太网之后数据线一根光纤就解决主机端一个支持 100G 的网卡就能收部署成本反而更低。第二类是图像和雷达数据实时回传。高分辨率工业相机、激光雷达、相控阵雷达输出的原始数据速率很容易超过 40G用四路 10G 捆绑不是不行但多纤束、多 IP、多端口带来的工程复杂度远不如单路 100G 干净。第三类是半导体测试机台。ATE自动测试设备里大量用到 FPGA 做 pattern 发生和采集测试头和上位机之间需要高速搬数据UDP 因为延迟低、可实现性好一直是主流选择。如果你只是做千兆或者 10G 的板卡下面很多内容也可以参考但 100G 的很多坑是高速链路特有的这也是我专门写 100G 的原因。1.2 为什么选 UDP 而不是 TCP 或 RoCE这个可能是很多新手最先问的问题。我的答案一句话在 FPGA 上把 TCP 跑到 100G 线速的代价比绝大多数项目能承受的成本高得多。TCP 有状态管理、乱序重排、滑动窗口、拥塞控制在 CPU 上跑是一件很成熟的事但在 FPGA 里要实现一个 100G 线速的 TCP 协议栈要么花大价钱买商业 IP要么用开源实现但资源占用和时序收敛都是大麻烦而且调试周期会拉得很长。RoCERDMA over Converged Ethernet性能确实好但它需要网卡、交换机、驱动全家桶配合PFC 流控配置就是一个深坑很多场景的 IT 环境根本不让动交换机配置。UDP 则完全是另一回事协议本身无状态FPGA 里做 MAC、IP、UDP 三层解析转发总共几千个 LUT 就能搞定能做到确定的低延迟也容易跑到线速。代价就是丢包不管、乱序不管、可靠性靠应用层自己做。对于数据采集这类“数据尽量多传丢了下一帧还能补”的场景UDP 的不可靠根本不算缺点。1.3 FPGA 平台怎么选硬核 CMAC 是底线做 100G 以太网FPGA 平台选择的第一原则不是资源大小而是有没有 100G 的硬核 MAC/PCS。Xilinx UltraScale 家族的集成 100G Ethernet MAC也就是俗称的 CMAC是 100G UDP 方案的基石。它在出厂时就是硅片上验证过的硬核支持 100G MAC PCS PMA线速跑到 100Gbps 全凭它兜底用户逻辑只需要做 MAC 上层的 UDP/IP 处理。如果选的器件没有 CMAC比如很多中低端 Kintex-7、Artix-7那就只能靠软核实现 100G MAC时序收敛难度非常大基本不推荐自己造轮子。我这次用的是XCVU9P板卡是常见的 QSFP-DD 光口方案参考时钟 156.25MHz。如果你用的是 VU3P、VU5P 或者更小一些的 KU15PCMAC 的配置方式和管脚位置会略有差异但移植思路完全一样。硬件选型还有个容易忽略的细节QSFP-DD 和 QSFP28 的引脚定义不同。100G 通常跑 4 个 25.78125Gbps 通道QSFP28 就能承载但 QSFP-DD 可以向下兼容。做板级设计时要确认好光模块座子、光模块类型和 FPGA 的 GTH/GTY 位置对应关系不然软件改到哭也没用。2. 开源协议栈选型与核心模块拆解verilog-ethernet 能给你什么2.1 开源方案对比为什么不从零写做 FPGA UDP很多人第一反应是“我用状态机硬写一个 UDP 发送不就行了”——10G 以下确实可以但到了 100GUDP 包校验和计算的位宽、时序、总线位宽问题都会跳出来所以移植一个成熟开源协议栈是性价比最高的路径。目前圈子里最成熟、维护最活跃的开源以太网协议栈是Alex Forencich 的 verilog-ethernet另一个大项目 Corundum开源 100G NIC的 UDP 部分也源自这套代码。它支持 1G/10G/25G/100G提供了从 MAC 到 IP、UDP、ARP、RGMII/XGMII/CMAC 接口的一整套 RTL 源码还有 AXI-Stream 接口的标准封装非常适合作为二次开发的基础。我对比过的其他方案包括T-Engine 的 UDP offload engine在特定社区比较流行但文档较少接口不够通用。商业 IP 核如 Xilinx XDMA 配合 XUP 的 UDP 栈或者第三方公司的 TOE IP性能成熟但 License 费用不低而且黑盒化之后遇到问题不好排查。所以最终我选了 verilog-ethernet。它的开源协议是 GPL-2.0商业项目使用时需要注意 License 合规但做预研和内部测试没有障碍。2.2 从物理层到 UDP 的完整链路verilog-ethernet 的代码层次和硬件实际的数据流非常对应自下而上大概是这样的CMAC 硬核物理层与链路层的边界由 Xilinx IP 提供。负责 100G PCS/PMA、FEC、时钟恢复、流量控制等。对用户侧吐出 512-bit 的 AXI-Stream 总线。eth_mac_100g可选的 MAC 层 wrapper处理 preamble、FCS 校验等。需要注意 CMAC 本身已经包含 MAC 功能所以使用 verilog-ethernet 的eth_mac_100g时通常配置成“没有内部 MAC”的模式避免重复处理。eth_ipIP 层支持 IPv4 接收和发送带校验和计算、分片组装如果开启、TTL/Hop Limit 处理。eth_udpUDP 层做源目的端口匹配、长度计算、校验和计算。eth_arpARP 模块自动应答 ARP 请求方便主机端 ping 通和获取 MAC 地址。每一层都提供rx_axis_*和tx_axis_*两组独立的 AXI-Stream 接口层与层之间用简单的 ready/valid 握手连接。想跳过 IP 直接做 MAC 层测试就跳过eth_ip/eth_udp想做一个纯 UDP 收发通道就把eth_arp、eth_ip、eth_udp串起来非常简单清晰。2.3 数据通路与接口定义懂 AXI-Stream 才能真正接线100G 的数据通路易犯的错误是把它当成普通窄总线来用。100G 的 AXI-Stream 数据位宽通常是512-bit在 322.265625MHz 的时钟下满负荷跑。512-bit 是什么概念一个周期 64 字节正好是一个最小的标准以太网帧尺寸不含 preamble。这意味着每个时钟周期最多只能处理一个包的开头所以握手信号和tkeep、tuser的时序非常敏感。verilog-ethernet 中用户侧接口重点关注这几个信号s_axis_rx_tdata/m_axis_tx_tdata512-bit 数据s_axis_rx_tkeep64-bit表示每个字节是否有效s_axis_rx_tlast包的最后一个 beats_axis_rx_tuser用户自定义信号通常包含错误标记或包长度信息s_axis_rx_tvalid/s_axis_rx_tready握手理解的要点在于tready 拉高表示接收方准备好接收数据tvalid 拉高表示发送方有数据要发送一个 cycle 的传输只有在 tvalid 和 tready 同时为高时才发生。所有 FIFO、计数器、状态机都建立在这个握手基础之上。移植过程中我最深刻的体会是不要试图绕过 AXI-Stream 去直接抠内部信号就算绕过了后面接自定义逻辑、DMA、缓存时终究还是要回到这套接口上来。把 AXI-Stream 理解透了100G 的移植才算真正入门。3. 移植上板的第一步搭建工程、搞定时钟复位和引脚约束3.1 拿到参考工程后别急着全盘照抄verilog-ethernet 的仓库里带有 example 工程里面有基于 VCU118、VCU108 等官方板卡的约束和 top 设计。遇到常见板卡的可以参考但它不会适配你的板子所以第一步是理解自己板卡的硬件结构。以我用的板卡为例关键信息有三类光模块接口QSFP-DD 的四路 25G SerDes 连接到 GTY Bank 的哪几个引脚参考时钟是 156.25MHz 还是其它值。这里不能想当然必须对照原理图确认。控制接口QSFP 的 I2C 引脚、ModPrsL模块在位信号、ResetL 引脚等这些如果没接好光模块可能根本不会点亮。LED 和拨码用于调试状态指示最好预留几个后面排查问题非常有用。拿到原理图先画一张自己的“信号-引脚-时钟”对照表再开始改代码。这步省不得。3.2 纯 RTL 还是 Block Design我的建议Xilinx 的 CMAC IP 在 Vivado 里可以通过 IP Catalog 配置生成有两种集成方式纯 Block DesignBD把 CMAC、时钟、复位、协议栈模块像拼积木一样连起来。优点是自动连线、不容易漏信号缺点是不灵活版本升级和代码管理都比较痛苦而且对 100G 这种高速工程BD 里的很多自动连接层级反而让你不好定位问题。纯 RTL IP 实例化只通过生成 IP 的 .xci 文件集成 CMAC顶层代码里手动例化所有模块。优点是可控性强版本管理方便代码能看懂每一处连接缺点是需要手动处理很多 IP 配置细节。我的做法是介于两者之间CMAC 和时钟/复位用 IP 定制化生成然后在纯 RTL 的 top 文件里例化协议栈、FIFO、DMA 控制器全部手写 RTL 连接。这样做的好处是排错时可以直接打开综合后的 schematic 去查每一段连线而不是面对一个大黑盒。3.3 时钟和复位看起来简单实际最容易出问题100G 工程的时钟没那么复杂但错了就是全线崩溃。关键时钟有以下几类coreclkCMAC 的用户侧时钟通常 322.265625MHz由 156.25MHz 参考时钟经过 PLL 倍频得到。rxclk / txclk 域CMAC 接收和发送路径可能使用独立的时钟域跨时钟域必须用 FIFO 或同步逻辑隔离。用户逻辑时钟如果协议栈和用户逻辑使用同一个 322.265625MHz直接全部使用 coreclk 即可如果用户逻辑跑在别的频率比如 250MHz 或者 300MHz端口之间需要接异步 FIFO。复位逻辑是另一个大坑。verilog-ethernet 的模块大多是同步复位但 CMAC 的复位在初始化阶段需要足够长的时间而且复位释放的时序需要满足要求。我踩过的一次问题就是上电后 CMAC 的 tx 侧状态不正常ping 不通后来发现是因为顶层复位时间太短CMAC 还没完成初始化就被释放了复位。建议在复位逻辑里增加一个计数器保证复位持续至少 1ms并且在复位释放后等 CMAC 的tx_axis_arstn、rx_axis_arstn信号拉高后再开始数据发送。4. 最容易翻车的地方配置、回环逻辑与 AXI-Stream 数据通路4.1 MAC 地址、IP 地址、端口号改错一个都白搭verilog-ethernet 的 UDP/IP 模块允许通过寄存器配置本机 MAC、IP、端口等也支持在 RTL 里直接常量例化。我强烈建议用 RTL 常量先把协议栈跑通再去谈寄存器配置。RTL 常量配置里有三个容易踩的地方MAC 地址需要先确认是组播 MAC 还是单播 MAC。如果主机端 ARP 请求发过来FPGA 回 ARP 应答使用的源 MAC 必须和自己的 MAC 地址一致否则数据链路层就会被主机丢弃。我之前写错过一次结果 Wireshark 能抓到 ARP 但 ping 就是不通排查了整整一个下午。IP 地址FPGA 的 IP 和主机端必须配置在同一子网比如主机是192.168.50.1FPGA 是192.168.50.100掩码 255.255.255.0。这个如果错了包能发出去但回不来。本地端口和目的端口端口号在应用层必须一一对应。例如 FPGA 接收数据时配置本地端口为 5001发送数据时配置远端端口为 5001端口错误时 UDP 包会被主机协议栈直接丢弃Wireshark 能看到应用收不到这就是典型的“半通”状态。4.2 回环逻辑上板后第一件事不是对接应用移植完成、综合实现、烧上板子之后我的建议是不要一上来就直连主机打流要先在 FPGA 侧做一次回环测试确认链路本身是通的。回环做法有两种外部回环用一根光纤或光模块的回环头把光模块 TX 直接连到 RX。这种回环覆盖了完整的物理层能验证光模块、SerDes、CMAC 的全链路。内部回环在 CMAC 配置里启用内部 PCS 回环或 PMA 回环不需要光模块就可以做电路板调试。注意这种回环不经过物理介质只能验证 FPGA 内部逻辑。我的调试顺序是先 PCS 回环 → 再做外部光模块回环 → 最后对接主机。回环测试通过后再做一个简单的发包计数器或LFSR 伪随机数发生器通过 UDP 不停地往主机发数据。这个环节可以不加完整应用逻辑只需要在 FPGA 里构造一个固定的 UDP 包发出去然后用 Wireshark 抓包确认。这一步能帮你过滤掉“逻辑假通、物理真不通”的困扰。4.3 AXI-Stream 握手的暗坑tready 在复位期间必须拉低AXI-Stream 握手逻辑看起来简单但在 100G 的高速率下容易暴露问题。我重点提醒两个问题一tready 在复位期间必须为低。如果复位期间 tready 被拉高对端会认为数据被接收但实际上没有直接导致丢包。很多 FIFO IP 的复位行为是 tready 拉低的但自定义逻辑里要特别注意。这在低速总线上可能不明显100G 下每个 cycle 都是 64 字节丢一个 cycle 就是 64 字节没了测试丢包率不堪入目。问题二tuser 传递不完整。verilog-ethernet 的 rx 路径中tuser携带了包是否 CRC 错误、是否存在截断等信息。很多人在做包过滤或者统计时会忽略 tuser结果收到的包里混入坏包主机侧表现为偶发乱码。正确做法是在接收入口检查tuser如果是错误包直接丢弃并计数。5. 上板测试实战从 Loopback 到 iperf3 打流全流程5.1 连通性测试先谈 ping 通再谈线速协议栈移植后的第一个里程碑是主机能 ping 通 FPGA 的 IP。这个阶段需要准备一台带有 100G 网卡的服务器比较常用的是 Mellanox ConnectX-5/6 或 Intel E810。一根直连的 DAC 线缆或者两根光纤加光模块建议先用直连 DAC省去光模块兼容性的变量。主机端配置静态 IP192.168.50.1/24。FPGA 的 IP 配置为192.168.50.100/24。一切就绪后先在主机端 ping 一下 FPGA。如果通说明 ARP 应答和 ICMP 回显逻辑没问题。如果不通按这个顺序排查用ifconfig确认网卡已经 link up链路状态正常。用 Wireshark 在主机侧抓包看有没有 ARP 请求发出有没有 ARP 应答返回。如果 ARP 应答没有返回重点检查 FPGA 侧 CMAC 状态、MAC 地址匹配逻辑、eth_arp 模块。如果 ARP 应答回来了但 ping 不通检查 ICMP 回显逻辑和 IP 层校验和。有一次我排查了很久最后发现是 CMAC 的 TX FIFO 深度配置太小连续发几个包之后 FIFO 溢出导致回显包丢失。记住ping 通只能说明链路基本可用不代表高速传输没问题。5.2 用 iperf3 和 UDP 测试工具打流工具选型与参数细节连通性确认后就可以进入打流阶段。主机端推荐 iperf3虽然它主要面向 TCP但 UDP 模式也很实用。命令如下# 先在 FPGA 侧或者对接的应用服务器启动服务端 iperf3 -s -p 5001 # 主机端发起 UDP 打流目标带宽 80Gbps iperf3 -c 192.168.50.100 -u -b 80G -p 5001 -t 30 -l 1400参数说明-u表示 UDP 模式-b 80G表示以 80Gbps 的码率发送-t 30表示持续 30 秒-l 1400表示 Payload 长度注意这里是应用层负载长度不包含 UDP/IP/MAC/Ethernet 头所以实际线速会略高于 80Gbps如果是自己写 FPGA 端逻辑可以配合一个 UDP 发送模块固定每秒发送一定数量的包计数统计发送包数和字节数。然后与主机的收包统计做对比就能算出丢包率。另一个很实用的工具是 Python 的 scapy 或 socket 脚本适合做精确报文控制。比如发送一个特定 payload 的 UDP 包然后在 Wireshark 里查看时间戳和延迟import socket sock socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.sendto(b\x00 * 1024, (192.168.50.100, 5001)) sock.close()5.3 怎么判断 100G 真的跑满了而不是“只是看着在收包”很多人跑到一半就以为成功了其实距离线速差得远。判断 100G 跑没跑满不能只看主机的网卡速率显示需要三个维度同时验证FPGA 侧发送计数器和接收计数器统计协议栈入出口的字节数和包数。比如发送端计数器显示每秒发出 95Gbps 的字节数这就说明 FPGA 协议栈的发送通路是没瓶颈的。主机侧网卡统计Linux 下用ethtool -S eth0查看rx_bytes、rx_packets、rx_dropped。FPGA 发 95G主机收了 95G且无rx_dropped说明链路、DMA、驱动都没问题。Wireshark 抓包可以抓一部分包的 payload 做内容校验比如发送端填的是递增序列号接收端校验连续性和正确性。三个维度都对上才敢说 100G UDP 上板成功。6. 实测数据与调优心得带宽、丢包和几个坑6.1 实测数据接近线速是可以做到的我在这块 XCVU9P 板卡上最终的实测数据如下表测试项结果单流 UDP 吞吐97.2 Gbps64 字节小包线速148.8 Mpps接近理论极限丢包率95Gbps1500B 包长0.0003%FPGA 逻辑资源占用协议栈部分~40K LUT 12K FFUDP 延迟FPGA 收包到发包约 1.2μs要说两点体会。第一单流 UDP 跑到接近线速是可能的但需要网卡驱动和中断绑定配合。如果主机端不调优比如中断没有绑核、网卡队列只有一个很容易出现 CPU 瓶颈明明是 100G 的链路却只能跑 60G。建议设置 RFSReceive Flow Steering或确认 RSS 多队列生效。第二64 字节小包场景是整个系统最严格的压力测试。100G 以太网的线速极限是 148.8Mpps64 字节包按 20 字节帧间隙算这意味着 FPGA 侧每个包的处理预算只有 6.7ns。如果你的 FPGA 逻辑里对每个包做大量处理比如查表、过滤、复制就很难跑满。对于小包压力测试建议先关掉所有不必要的逻辑只保留最简单的收发通路验证能达到多少 Mpps再逐一加回功能。6.2 小包速率为什么是硬指标以及怎么计算我们说 100G UDP 能不能线速光说带宽没用因为大包很容易跑满小包才是考验。100G 以太网的线速包速率公式是线速包速率 100Gbps / (帧间隙 20B 前导码 8B 帧头 14B 包长)64 字节包时100e9 / ((20 8 14 64) * 8) ≈ 148.8 Mpps每个包的处理时间只有约 6.7ns。在 FPGA 上做简单的 FIFO 缓存和转发这个速度没问题但如果你还要做包头解析、多级过滤、流表查表就要用流水线设计一个包的状态机不能在 6.7ns 内做多次 RAM 访问。所以做 100G 移植小包线速要当作一个基础性能指标来测很多在低速系统里碰不到的瓶颈在这里会直接暴露。6.3 最后几个容易忽视的坑再分享几个我这次移植过程中印象深刻的坑都是测试时才能发现的坑一回环未关外部测试全废。如果 FPGA 侧在调试时开了内部回环后面忘记了外部的打流数据会全部被回环挡住主机表现为只发不收或收的是异常包。排查链路问题前第一件事就是确认所有回环配置已关闭。坑二自定义协议栈里忘了处理 CRC。100G 以太网帧的 FCS 由 CMAC 计算和校验但如果你直接使用 verilog-ethernet 的eth_mac_100g并额外开启了 MAC 功能就有可能在 MAC 层重复添加 CRC导致包被 PC 丢弃。正确做法是确认 CMAC 负责 CRC协议栈侧关闭 CRC 处理。坑三Windows 下 UDP 缓冲区太小导致瞬态丢包。如果你在 Windows 主机上做测试工具接收UDP 默认缓冲区很小即便是千兆网都可能因为缓冲区溢出丢包。可以通过注册表修改全局 UDP 缓存大小或者直接换 Linux 环境。实测下来Linux 下修改net.core.rmem_max的效果立竿见影sysctl -w net.core.rmem_max268435456 sysctl -w net.core.rmem_default268435456坑四光模块兼容性。100G 光模块市场鱼龙混杂有些模块在特定交换机或者网卡上会协商失败或者降速。测试时如果发现链路起不来先确认模块型号是否在板卡和网卡的兼容列表中再检查 DDM 信息里的光功率。移植 100G UDP 这件事真正走完一遍之后你会发现代码量其实并不多难的是对链路每一层的理解和对细节的耐心。我这次从拿到开源代码到最终跑通前后花了大约两周其中真正写 RTL 的时间很少绝大多数时间都花在排查一条条的握手时序、一遍遍的抓包确认上。建议后面想做的朋友先花时间把 CMAC 的数据手册和 verilog-ethernet 的源码通读一遍再开始动手会少走很多弯路。如果后面有机会我打算在这个基础上继续做 UDP 多通道聚合、应用层重传机制和 PCIe DMA 对接到时候再写一篇新的实践记录。