
做 FPGA 的人一旦碰过 100G UDP就很难再回头用 10G 那套思路去凑合。我这次的需求很直接前端采集板要把 ADC 数据以接近线速送到后端服务器延迟要求微秒级而且流量形态要完全可控。第一反应是买成品 100G 网卡但实测下来Linux 内核协议栈在每秒百万级小包的场景里中断和软中断占比太高UDP 虽然协议简单收包路径上的锁和排队还是带来了明显的延迟抖动。最后方案落到了 FPGA 上目标是在板卡上移植一套开源的 UDP 协议栈跑通 100G 线速然后完成上板测试。这篇文章就是我整个“选型、移植、上板、排坑”过程的完整记录适合正在做 FPGA 网络加速、想用开源代码快速出活的工程师参考。1. 为什么是 100G UDP场景、延迟与开源栈的契机1.1 低延迟场景里UDP 比 TCP 友好太多很多没写过硬件协议栈的人会问UDP 这么简单直接用 CPU 收不就行了在 10G 时代这个说法勉强成立到了 100G问题就完全不一样了。100G 以太网在小包场景下的线速大概是多少64 字节以太网帧在线路上实际占 84 字节前导码和帧间隔都算进去除以 100G 线速得到约 148.8Mpps。也就是每秒要处理 1.48 亿个包。这个量级下CPU 每收一个包都要走一次中断、一次协议栈解析、一次 socket 排队无论怎么优化单核都撑不住多核分摊又会引入锁和分流的复杂度。TCP 在硬件里实现更痛苦。它的状态机包含连接管理、序号跟踪、重传定时器、拥塞控制窗口任何一个环节出问题接收端都要能处理乱序和重复。UDP 就简单多了无连接、无确认、无重传协议头只有 8 字节校验和计算也完全可以流水化。打个不太准确的比方TCP 像挂号信每一封都要回执丢了还要补UDP 像直接往信箱里塞传单能到就行不要求对方签收。对硬件而言UDP 是最容易做到线速的传输层协议。1.2 100G 线速给 FPGA 带来的真正挑战很多人以为 100G UDP 的难点在 UDP 校验和其实不是。校验和就是一个 16 位累加FPGA 里用组合逻辑随便算。真正的难点是512bit 宽的 AXI4-Stream 总线每个时钟周期要同时处理 64 字节数据状态机必须在非常短的节拍内完成头解析、路由查找、校验和更新、负载转发。一旦某个分支判断多耗了一个周期就会在数据流上打洞吞吐就掉下去。这也是我坚持选开源方案而不是自己从头写协议栈的原因MAC 层和 IP/UDP 层的边界处理、校验和逻辑、ARP 和 ICMP 回显这些看似简单但细节很多的东西直接用成熟代码能省掉大量调试时间。重点是理解它、把它接进自己的工程而不是重新发明轮子。2. 开源方案选型我对比过的仓库与最终选择初期我在几个开源项目之间犹豫了很久。简单列一下对比结果项目定位驱动/依赖100G 支持我放弃/选它的原因verilog-ethernetAlex Forencich纯 RTL 以太网 MAC UDP/IP 栈无纯 FPGA 逻辑有 eth_mac_100g 等模块最终选用模块化清晰Vivado 下接入成本低Corundum完整 FPGA 网卡含 PCIe DMA 和 Linux 内核驱动强依赖 PCIe 和驱动框架支持功能太重我的板子不是标准 PCIe 网卡形态驱动适配成本高OpenNIC / ntap 一类偏研究型网卡方案依赖特定驱动和主机环境部分支持文档少社区小出问题不好查2.1 为什么把 Corundum 排除了Corundum 是非常优秀的开源网卡项目如果我要做一块完整的、带 PCIe DMA 和自定义队列的智能网卡很大概率会选它。但这次的需求不一样我的数据通路是板卡自己产生数据通过 UDP 直接发出去不需要主机 CPU 参与收包也不依赖 PCIe 的 DMA 描述符机制。如果强行上 Corundum我还要解决 Linux 驱动的内核版本兼容、IOMMU、MSI-X 中断这些问题显然偏离了“快速移植上板”的目标。2.2 verilog-ethernet 的架构速览Alex Forencich 的 verilog-ethernet 仓库核心目录是 rtl 和 lib。rtl 下面按功能拆分得很清楚eth_mac系列是不同速率的以太网 MACudp_ip_stack是 UDP/IPv4 栈的封装这里面还带了 ARP 和 ICMP 回显ip、udp、arp模块又各自独立。辅助的axis_*模块则用于 AXI4-Stream 位宽转换、异步 FIFO、注册表等非常实用。我这次实际用的是eth_mac_100g和udp_ip_stack的组合。选它的另一个原因是用户接口统一走 AXI4-Stream只要理解了 tvalid/tready/tlast/tkeep 这几个信号就能把上下游逻辑接起来。仓库里还有 testbench可在仿真阶段先把独立功能跑通大大降低上板后的排错成本。3. 硬件准备板卡、光模块与 CMAC 配置3.1 板卡与线缆选择100G 的 FPGA 方案板卡选择不多但都有共同点需要带 QSFP28 光口需要足够的 GTY/GTM 高速收发器最好还有一颗高频参考时钟。我手头用的这块是自研的 UltraScale 板核心芯片是 VU9P板载双 QSFP28如果买现成的VCU118、Alveo U200/U250 都是常见选择。Alveo 卡的输入时钟和电源管理做得比较省心适合第一轮跑通逻辑自研板则要自己核对参考时钟和复位电路。线缆方面强烈建议第一轮用 QSFP28 DAC 铜缆不要一上来就用光模块加长光纤。DAC 铜缆没有光口洁净度和光功率问题链路物理层出现问题的概率低很多可以减少变量。PC 端配套的网卡我用的是 Mellanox ConnectX-5100G 网卡里它兼容性最好驱动在主流内核里都有。如果实验室实在没有 100G 网卡也可以用两块 FPGA 板互相打流但我觉得先用 PC 网卡做标准参考更方便抓包。3.2 CMAC IP 配置与用户时钟在 Vivado 里生成 Xilinx Integrated 100G Ethernet Subsystem也就是大家常说的 CMAC时有几个关键配置值得说明。用户接口推荐选 AXI4-Stream数据位宽 512bit对应的用户时钟是 322.265625MHz这是 IP 向导里常见的默认组合。GT 参考时钟一般是 161.1328125MHz对应 4 个 25.78125Gbps 的 SerDes 通道这个频率由板卡晶振提供约束文件里要写清楚。我个人建议第一步先跑 CMAC 自带的 example design板上验证链路能否建立。CMAC 的复位逻辑比大多数人想的更啰嗦GT 收发器的复位、PCS 复位、MAC 复位还有各个 user clock 的 phase 关系都要等对应状态机跑完。example design 里已经把这些复位信号的处理做好了能帮你省掉大量查时序手册的时间。RS-FEC 默认可以先关掉。如果用的是短距离 DAC 铜缆线缆质量正常关掉 FEC 也能稳定跑开 FEC 虽然更接近长距离光模块的真实使用场景但初期会多一个变量不利于排查。4. 移植步骤从 GitHub 到 bitstream 的一条龙流程4.1 代码准备与工程组织先克隆 verilog-ethernet 仓库然后把 rtl 目录下的所有 .v 源文件加入 Vivado 工程。需要明确一点这个仓库的模块之间是松耦合的虽然 git clone 后可以直接全量加入编译但我更建议先只添加必要模块。比如用eth_mac_100g时它依赖的axis_adapter、axi_*等基础模块会自动被引用而如果同时加了eth_mac_10g也只是多花点综合时间不影响功能。对新手来说全量加进工程最省事只要确保没有同名模块冲突就可以。4.2 例化 CMAC 与 UDP 栈实例化结构大致是这样CMAC 的 512bit AXI4-Stream 用户接口先接一个异步 FIFO再通过位宽转换模块接到udp_ip_stack。为什么中间要加 FIFO因为 CMAC 的用户时钟是 322.265625MHz而栈和用户逻辑可以跑在另一个频率比如 300MHz 或 200MHz。用axis_async_fifo做跨时钟域隔离避免整个设计都绑死在 CMAC 的时钟上。频率不够高的时候时序收敛会容易很多。udp_ip_stack的关键参数就那么几个本地 MAC 地址、本地 IP 地址、是否开启 ARP 缓存、是否开启 ICMP 回显。我用的配置如下localparam MAC_ADDR 48h00_1A_2B_3C_4D_5E; localparam IP_ADDR 32hC0_A8_01_0A; // 192.168.1.10注意udp_ip_stack默认的 MAC 参数接口可能因为版本不同而异旧版本是my_mac的一组 8bit 输入新版本更倾向于编译期参数。不管哪种只需保证与 PC 网卡上的静态 ARP 条目一致即可。4.3 用户侧发送与接收逻辑移植引脚之前先在用户逻辑里做两个简单的 test pattern能极大方便后续排错。发送方向做一个计数器每 1024 个周期组装一个 UDP 报文payload 是递增数据。目的 IP 写成 PC 的 192.168.1.2目的端口固定 5001源端口固定 4000。这样用 tcpdump 一抓就能清清楚楚看到每个字段。接收方向做回环模式。把从 UDP 栈收到的 payload 原封不动再塞回发送侧目的 IP/端口换成原来的源 IP/端口。这样 PC 发一个包到 FPGAFPGA 立刻回一个相同 payload 的包验证通路非常高效。AXI4-Stream 握手信号里最容易出错的是tlast和tkeep。tlast必须在最后一拍拉高且tkeep要准确表示最后一拍的有效字节数。比如 8 字节位宽的接口如果 payload 长度是 3最后一拍 tkeep 就是 4b0111这时候 tdata 的高字节数据无效下游不能拿来计算。这个细节在 512bit 总线上更致命如果 tkeep 算错了一整包数据就会错位。4.4 约束与综合实现上板前综合实现时我注意到时序收敛整体比预期顺利但有几个位置容易出红线跨时钟域 FIFO 的读写指针逻辑、UDP 栈里的校验和流水线、以及 512bit 总线的位宽转换模块。第一版如果时序不过不要急着改 RTL 算法先看关键路径在哪个模块把组合逻辑切几级寄存器通常就解决了。另外复位信号尽量用 CMAC 输出的用户复位不要自己用计数器延迟硬凑否则容易出现复位释放时序违例。5. 上板实测抓包、打流与 ILA 三件套5.1 链路建立检查烧写比特流之后第一件事不是发 UDP而是先确认物理链路 up 了。PC 端看网卡状态ethtool eth0能看到 Speed: 100000Mb/s 和 Link detected: yes。如果显示没有 link优先检查 QSFP28 线缆是否插紧、CMAC 内部状态机是否跑完用 Vivado Hardware Manager 里的 ILA 观测 CMAC 的gt_rxstatus和us_rxstatus信号能快速判断是 SerDes 层还是 MAC 层的状态异常。还有一个非常实用的自检手段是 CMAC 的 PRBS 测试。在 CMAC 配置里把 TX 侧设为 PRBS 发生器RX 侧设为 PRBS 校验器两端在两个 GT 通道之间做内部回环或通过外部 DAC 做环回就能确认 SerDes 和线缆是否完好。链路都不干净后面所有抓包都是浪费时间。5.2 静态 ARP 与 Ping 验证Linux 默认会发 ARP 请求来解析目的 IP 的 MAC。虽然 verilog-ethernet 的栈带 ARP 模块但为了避免测试时间浪费在 ARP 缓存等待上我建议一开始就在 PC 上配置静态 ARPip addr add 192.168.1.2/24 dev eth0 ip neigh add 192.168.1.10 lladdr 00:1a:2b:3c:4d:5e dev eth0 nud permanent配置完之后先 ping 一下 FPGA 的 IP。如果 UDP 栈开了 ICMP 回显ping 能通说明二层和三层基本 OK校验和、MAC/IP 地址解析这条链路已经通了。ping 不通也别慌先用 tcpdump 看 PC 有没有发出 ARP 请求、FPGA 有没有 ARP 回复哪个方向没包就查哪一侧。5.3 UDP 打流测试ping 通了之后把 FPGA 的 test pattern 发送逻辑打开PC 上抓包tcpdump -i eth0 udp port 5001 -XX如果tcpdump正常打印出 UDP 报文而且 payload 能看到 0x00、0x01、0x02 这样的递增序列链路已经基本算通了。此时 tcpdump 结束时的统计信息里常会出现packets to unknown port received这是正常现象说明 UDP 头解析正确只是 PC 上没有一个 socket 监听 5001 端口。很多网友遇到这个统计以为丢包或协议异常其实不是它恰恰证明包已经完整到达主机协议栈。接下来用 iperf3 做带宽测试。PC 向 FPGA 发送方向如果 FPGA 开的是回环模式命令大概是iperf3 -u -c 192.168.1.10 -b 100G -t 10 -l 1400这里有个容易困惑的点也正是很多人在网上问的iperf3 用 UDP 跑 TX 时到底看 sender 端还是 receiver 端一定要两边都看。UDP 没有 ACKsender 只能表示自己发出了多少包实际到没到、丢没丢要以 receiver 的接收包数、丢包率、抖动为准。而且单线程 iperf3 往往打不满 100G受限于本机 CPU 和 PCIe 路径建议加-P 8开多流并且用-J输出 JSON 格式方便脚本比对 sender 和 receiver 的 packet 数。包长方面默认 MTU 1500 时 UDP payload 上限约 1472带宽测试想压到接近线速最好开巨型帧两端都设 MTU 9000发起ping -M do -s 8972验证巨型帧通路。如果开巨型帧后 ICMP 不通查交换机和网卡配置而不是怀疑 FPGA 协议栈。5.4 ILA 抓内部数据外部抓包无误后我还会再插一个 ILA 到udp_ip_stack的接收出口和发送入口上。ILA 触发条件设置成tvalid tlast也就是抓到一包结束时的那一拍。把抓到的 tdata 和 PC 发的原始报文逐字节比对这一步对排查后面的字节序问题非常重要。如果接口数据位宽是 64bit一包 64 字节的 UDP 报文会被拆成 8 拍ILA 看到的 tdata 是从低字节到高字节排列的。很多 FPGA 工程师习惯大端思维看到 64bit 数据里的第一个字节出现在低位就懵了以为数据反了。实际上以太网帧在线路上是高位在前但硬件总线普遍按小端方式把低地址字节放在低 bit 位这个映射关系搞清楚字节序问题就解决了一半。6. 移植中的坑链路、校验和、字节序与时序收敛6.1 链路 up 但收不到包从 PHY 到 ARP 逐层排查我遇到过一个很典型的问题ethtool显示 link up但 tcpdump 什么都抓不到。一开始以为是 UDP 栈配置问题调了很久最后发现是 CMAC 的复位逻辑没有把gt_rxuserrdy拉起来导致 GT 接收侧根本没解出数据。这个状态在外部看链路确实是 up 的因为物理信号已经建立但内部用户接口一直没有数据出来。排查顺序我建议固定下来先看 CMAC 的gt_rxstatus、us_rxstatus是否为正常值再看 PRBS 内部自检能否通过接着用外部 DAC 环回看链路最后才看到 UDP 栈。反过来排查容易把简单问题复杂化。6.2 校验和与硬件卸载PC 侧也在“搞鬼”FPGA 发出去的报文在 PC 上抓包时发现 UDP checksum 是 0x0000但 FPGA 侧明明算了校验和。这是因为现代网卡默认开了 RX checksum offload驱动会把硬件校验结果直接标记到 skb 上抓包工具显示的 checksum 已经是“由网卡硬件验证过”的 0。判断方法很简单在 tcpdump 里看bad udp csum提示如果出现才说明校验和真的有问题或者用ethtool -K eth0 rx-checksumming off关掉卸载再抓包。反过来PC 向 FPGA 发送时如果网卡开了 TX checksum offload驱动可能不填 checksum而是交给硬件在发送时计算。FPGA 侧收到校验和为 0 的包如果栈严格校验会当成坏包扔掉。调试期我建议把 PC 网卡的 tx/rx checksum offload 都关掉让内核老老实实计算排除这个变量后两边行为就确定可预期了。6.3 512bit 总线的字节序字节序问题是 100G 移植里最隐蔽的坑。同样一组数据10G 时代用 32bit 总线低字节在低 8bit大家习惯成自然到了 100G总线条数翻到 512bit每周期 64 字节一旦源/目的 MAC 地址放错字节位置报文整体就废了。常见现象是 tcpdump 能抓到包但抓出来的目的 MAC 变成了5e:4d:3c:2b:1a:00这样的反序。解决的办法很土但有效先用固定 test pattern比如 payload 从 0x00 递增到 0xFF配合 ILA 逐拍对数据确认哪一级模块把字节顺序搞反了。verilog-ethernet 的模块默认衔接正常问题通常出在用户自己写的发送逻辑或位宽转换上别一上来就怀疑开源代码。6.4 tkeep 和 tlast 的边界条件小包场景里很多帧在一拍之内就完成了 tlast此时 tkeep 可能不是全 1大包场景里最后一拍的 tkeep 又往往是部分有效。如果用户逻辑生成发送数据时没有正确计算 tkeepUDP 栈加完头之后 payload 会错位接收端解析出来的长度字段和实际数据长度对不上。建议用“包长模位宽”来生成 tkeep余数为 0 时 tkeep 全 1否则把低位对应位置 1其余清零。6.5 时序收敛与跨时钟域最后聊聊时序。用户逻辑如果直接挂在 322.265625MHz 的 CMAC 用户时钟下任何一点组合逻辑过长都会造成时序违例。我的做法是将 UDP 栈单独跑一个时钟域用户业务逻辑跑一个更低频率的时钟域中间用axis_async_fifo隔离。这样用户的计数器、RAM、状态机都不用在 322MHz 下布局布线时序压力小很多。实测下来这套结构在 -2 速度等级的 UltraScale 上非常稳布局布线一次通过。整套移植下来我最真实的感受是100G UDP 的难点不在 UDP 本身而在于底层 PHY 状态机、位宽转换和字节序这些“周边细节”。开源栈把协议处理已经封装得很好了我们要做的是把它和厂商 IP、板卡环境正确衔接再通过抓包和 ILA 一层层证明每一级都可靠。最后再分享一个小技巧上板测试时先用 DAC 铜缆 静态 ARP 回环模式把物理层和协议层都验证干净后再换光模块和长距离链路能省掉至少一半的排错时间。