这个项目的起因其实很直接——手头拿到了一个带双QSFP28接口的高端FPGA板卡,核心逻辑资源、DDR4和PCIe硬核都很充裕,但网络侧只有MAC/PCS的硬核,没有现成的UDP协议层。问了一圈商用IP授权,价格让人倒吸一口凉气。于是把目光转向了开源方案,目标非常明确:把一套开源的100G UDP协议栈移植到这块板卡上,做成能实际收发用户数据的参考设计,再用打流工具把性能测个明明白白。
这个工程做下来,踩坑不少,但收获非常大。整套流程从选型、移植到上板测试,几乎覆盖了FPGA高速网络开发会遇到的全部典型问题:时钟怎么规划、CRC字节序怎么处理、跨时钟域FIFO怎么设计、线速能不能跑满、小包PPS为什么上不去、PC端UDP接收缓冲区怎么调。这篇文章我会把这套完整的移植和测试记录整理出来,给准备做10G/25G/100G UDP方向的同学一份可以直接抄作业的参考。
1. 开源方案选型:为什么选它而不是自己写
先说结论:我们最终选择了Alex Forencich维护的verilog-ethernet开源协议栈。这个项目在GitHub上非常有名,想碰高速以太网的FPGA工程师基本绕不开它。选择它的核心原因有三点:接口标准化程度高、对Xilinx硬核MAC的适配非常完善、社区活跃度足够。当然也不是说只有这一个选项,COREnite的10G/25G UDP方案也可以参考,但那个更偏向商用授权,开源友好度不如前者。另外OpenCores和GitHub上还有一些零散的UDP核,但大多数只做到1G/10G,接口风格也很不统一,扩展性差。
选型对比其实很关键,我整理了一张表:
| 方案 | 最高速率 | 接口风格 | 商用授权风险 | 100G支持 | 维护状态 |
|---|---|---|---|---|---|
| verilog-ethernet | 100G | AXI4-Stream / 自定义总线 | 无(MIT类) | 支持CMAC硬核适配 | 更新活跃 |
| COREnite UDP/IP | 25G | 自定义 | 需商业许可证 | 无 | 稳定但更新慢 |
| 自研UDP设计 | 视能力而定 | 自定义 | 无 | 无 | 需自行维护全部时序 |
对比完就懂了,自研UDP协议栈在10G以下还可行,越往上越痛苦。100G以太网的PCS/PMA层基本不可能用纯FPGA逻辑实现,必须依赖UltraScale+的CMAC硬核或者Intel的E-tile硬核。协议栈真正要做的核心工作是MAC层之上的IP解析、ARP应答、UDP头解析和校验和计算,这部分逻辑量其实不大,但关键路径时序和跨时钟域设计才是真正的难点。
verilog-ethernet这个仓库里自带了一个完整的UDP/IP协议栈模块(udp_ip),它把MAC层接收到的帧做解析后直接丢给用户逻辑,用户逻辑需要回包时把数据塞进TX接口就行。它还实现了ARP协议,这在局域网调试时非常重要——PC端要先发出ARP请求拿到FPGA的MAC地址,UDP包才能真正发出去。如果协议栈里没有ARP处理,你会遇到非常诡异的“PC发送端显示发送成功但FPGA收不到,wireshark里显示PC一直在广播ARP请求”的问题。
选型时还要注意一个点:官方仓库的例程和文档主要针对10G/25G场景,100G的例程相对少一些,需要自己把hd接口改成对应CMAC的512bit或者64bit数据位宽。这意味着你必须有基本的FPGA数字逻辑功底,如果完全新手,建议先从10G版本跑通,再往100G迁移。
2. 软硬件环境与关键原理准备
2.1 硬件平台与时序规划
我手头的板卡核心芯片是Xilinx UltraScale+ VU9P,有两个QSFP28接口,每个接口物理层对应四路25.78125Gbps的高速SerDes,加上RS-FEC后总带宽正好是100Gbps。用的FPGA型号支持CMAC硬核,这几乎是100G UDP方案的必经之路——没有硬核PCS,纯逻辑想跑到上百Gbps的线速率,时序根本收敛不了。
时钟规划是整个移植中最容易被低估的环节。100G以太网跑的是25.78125Gbps线速率,GTY参考时钟通常是161.1328125MHz(由QSFP28模块参考晶振分频得到),CMAC内部会把这个时钟倍频到所需的SerDes速率。而用户侧AXI4-Stream接口的时钟则取决于数据位宽:如果用512bit位宽,时钟约是322.265625MHz;如果用64bit位宽,时钟频率就得跑到1.6GHz以上,这在FPGA里根本不可行。所以开源协议栈在100G场景下几乎都推荐用512bit数据位宽,内核逻辑跑到322MHz左右还算轻松。
移植之前一定要和板卡的硬件手册仔细核对:
- QSFP28的参考时钟频率是161.13MHz还是155.52MHz,这个必须和硬件设计匹配,错了链路直接起不来。
- CMAC复位时序要参考Xilinx官方例程,尤其要注意tx/rx reset done信号何时拉高,顺序错了会导致链路状态机卡死。
- QSFP28模块的I2C管理接口(通常挂在I2C控制器上)需要在上板时配置成正确的线速率模式,否则模块可能识别为4×25G的Breakout模式,而不是当作单端口100G使用。
2.2 数据通路与协议栈结构
整个UDP数据通路的架构是这样的:QSFP28光模块接收光信号,经GTY SerDes进入CMAC硬核,CMAC完成64B/66B编解码和PCS成帧后,把AXI4-Stream格式的MAC帧交给用户侧逻辑。用户侧逻辑里第一个模块是MAC RX FIFO,主要作用是做跨时钟域和带宽匹配。接着进入UDP/IP协议栈,它解析MAC头、IP头、UDP头,提取出payload数据,同时校验CRC、IP校验和以及UDP校验和。发方向完全对称:用户数据打包成UDP报文,加IP头、MAC头,再通过CMAC发出。
这个过程中有一个特别容易被新手忽略的细节,就是字节序问题。以太网协议是大端序,而FPGA内部大多数Design上习惯使用小端流水,如果不做字节翻转,你会发现抓包工具显示的目的MAC地址字节是反的,IP地址也从 192.168.1.10 变成了 0A 01 A8 C0 这种逆序。开源协议栈内部已经处理好了字节序,但你自己写的用户逻辑如果对接它的接口,必须按照它定义的字节序来填充IP地址和UDP端口,否则包出去了对方设备根本认不出。
另外,UDP校验和的算法也很容易写错。UDP over IPv4的校验和是计算伪头部(源IP、目标IP、协议号、UDP长度)加UDP头部加数据,如果校验失败,很多网卡和操作系统IP栈会直接丢包,但发送端看起来毫无异常——因为它根本不关心对端是否收到。这也是实测中对端“收不到包”的一个隐性原因,建议在协议栈里加一个CRC/UDP校验错误计数器,调测时先确认计数为0再去查其他环节。
3. 移植过程:从工程骨架到上板文件
3.1 搭建Vivado工程和时钟约束
这个环节的核心目的是把手上的开源文件和板卡BSP结合成一个干净整洁的工程。我先新建了一个空白的Vivado 2022.2工程,目标芯片选择XCVU9P-FLGA2104-2L-E,然后按以下顺序添加文件:
- 先是CMAC IP核,Vivado的Catalog里直接搜“CMAC”就能找到UltraScale+ Ethernet 100G CMAC IP,生成时选择带AXI4-Stream接口的模式。
- 然后把verilog-ethernet仓库里的rtl目录下相关文件添加进工程,包括mac、ip、udp、arp这些子目录下的模块。
- 最后写一个顶层wrapper:实例化CMAC IP和UDP协议栈,中间插上我们的FIFO做跨时钟域缓冲。
时钟约束是这里最容易出问题的地方。CMAC IP会根据配置自动创建几个时钟,比如gt_rx_clk、gt_tx_clk、axi_lite_clk等,Vivado会自动推断很多主时钟约束,但你必须在XDC里明确约束用户侧逻辑时钟。我遇到的第一个时序问题是用户逻辑跑了322MHz,但报告里总有约200ps的负slack,后来定位到是跨时钟FIFO的读写指针逻辑没有做寄存分隔,扇出太大。解决办法是把异步FIFO的读写指针各自用两级同步器打一拍,并加set_max_delay约束避免路径过紧。
3.2 用户接口适配:把自定义流式接口改成AXI4-Stream
开源UDP协议栈的对外接口在不同版本里略有区别,但总体上都是围绕一个流式接口:发送端有tdata、tkeep、tvalid、tlast、tuser,接收端也一样。CMAC硬核的AXI4-Stream接口非常标准,但数据位宽和信号命名可能有差异,所以需要一个适配层把两者对齐。
我按照下面的方式处理:
- 先把CMAC的s_axis_tx接口直接拉到顶层,注意CMAC的tuser信号里有帧错误标记,如果不需要可以忽略。
- 协议栈内部的udp接口则通过一个宽度转换FIFO适配。因为协议栈的内部数据通路是64bit(10G直出)或者512bit(100G直出),你需要根据实际选型把位宽对齐。我用的是512bit通到CMAC,所以协议栈内部给MAC层的数据接口也保持512bit,避免每拍还要做位宽拼接的额外组合逻辑。
- 特别要注意tkeep和tlast的配合。CMAC要求tlast拉高时,tkeep必须有效表示最后一拍的字节数。很多新手自定义发送逻辑时,数据长度不是整512bit对齐,如果没把tkeep按字节掩码设正确,CMAC会把多余的空字节当有效数据发出,造成帧长错误,CRC校验必挂无疑。
整个适配层写完后,我仿真验证了发送通路:构造了64字节、128字节、512字节、1500字节四种长度的UDP包,发送到协议栈后观察MAC输出帧的CRC字段和长度字段,全部正确。仿真没过就直接上板,大概率会浪费大量的调试时间。
3.3 引脚约束和上板验证准备
引脚约束这块,我提前规划了两套约束:一套用于纯逻辑测试,把千兆/百兆以太网的引脚都约束到FMC扩展口上的百兆PHY芯片,用于低速率验证协议栈的收发正确性;另一套才是真正的100G QSFP28约束,绑定到板卡背面的QSFP28连接器。上板前用Vivado Implementation跑一遍,确认时序通过、IO error为零,然后再生成bit文件。
上板前我习惯先在ILA里抓几个关键信号,比如:
- tx_reset_done和rx_reset_done是否拉高;
- CMAC的link_status、clock_data_lock是否正常;
- 协议栈收到的MAC帧计数器是否在设备发流前为0,发流后大于0。
如果这些都没问题,再开始接PC上位机和iperf3做吞吐测试。实践证明,提前留出ILA调试探针能节省2-3天的排错时间。
4. 上板测试全流程:从链路打通到线速打满
4.1 测试平台搭建与预检
测试环境我把FPGA板卡和一台带双口100G网卡的服务器用QSFP28 AOC线缆直连。服务器的网卡是Mellanox ConnectX-5双口100G,系统是Ubuntu 22.04,装好了mlx5驱动。注意网卡必须支持100G速率自适应,且线缆长度不要太长,短距离AOC线缆对上板调试最省心。
上电后第一件事不是打流,而是检查链路状态:
ethtool eth0 # 输出里看 Speed: 100000Mb/s, Link detected: yes如果链路没起来,优先查两个点:QSFP28模块的I2C是否正常识别,CMAC的gt_reset_done是否拉高。我一开始用了一根光模块转AOC的转接线,发现模块ID读不到,链路一直是Down,后来换了一根直连模块的AOC线才正常。
链路OK后,在FPGA侧查看CMAC状态寄存器,确认接收的包计数在增加,同时查看CRC错误计数器是否为0。如果CRC错误计数一直在涨,大概率是FPGA和线缆之间的信号完整性问题,这时候可以试着降低RS-FEC级别,或者调整TX的预加重参数。这个现象在实验室用劣质线材时非常常见。
4.2 用iperf3进行UDP打流测试
链路通之后,我用iperf3做了两组基础测试:接收测试和回环测试。
接收测试的思路是让服务器发出UDP流,FPGA侧用ILA采样或者内部计数器统计收到的字节数,对比服务器发送字节数看是否丢包:
iperf3 -c 192.168.1.10 -u -b 0 -t 30 -i 1 # -b 0 表示不限带宽,UDP模式打满速 # 192.168.1.10 是FPGA内部分配的IP地址第一轮跑出来的结果很有意思:测大包(1472字节payload,也就是1500字节的标准MTU)时,吞吐率达到98Gbps以上,丢包率几乎为0。但换到64字节小包,吞吐率直接掉到80Gbps都不到了,PPS大约只有一百多万。这个结果完全符合预期,因为小包场景下每包的处理开销固定,而CMAC和协议栈的流水线处理能力是有限的。
为了进一步确认大包线速,我还用两个FPGA互打(一块发、另一块收),发现即使payload=9000字节的巨型帧,吞吐率也能稳定在98.8Gbps以上,几乎逼近线速。这说明协议栈本身的处理能力足够,瓶颈主要在于PC端网卡的驱动和中断合并。
4.3 UDP回环测试与抓包验证
打流测试之外,我还单独做了一个UDP回环测试:FPGA把收到的UDP payload原样发回给服务器。服务器端用一段简单的Python脚本或者socat监听9000端口,发包后回收,比对数据一致性。
这里有一个非常关键的坑:必须在FPGA侧把从MAC接收到的帧按源MAC地址回复,而不是简单地从RX端口原样打回TX端口。如果直接回环,交换机会学习到错误MAC转发表,后面所有包都会走错。正确做法是协议栈在回复数据包时,把MAC源地址填成自己的MAC,目的地址填成接收帧的源MAC,IP地址同理。
服务器端用tcpdump抓包验证回环内容:
tcpdump -i eth0 udp and port 9000 -vv确认IP地址、UDP端口、payload数据完全一致后,说明整个收发通路已经完整打通。到这一步,基本可以断定开源UDP栈在100G环境下不仅能用,而且性能达标。
4.4 压力测试:连续打流24小时
我不建议只跑一两分钟就宣布测试通过。高速网络协议栈在长时间运行下的稳定性远比瞬时吞吐重要,内存泄漏、计数器溢出、状态机异常这几种问题都会在长时间运行后暴露。
我的压力测试方案是24小时连续发流,FPGA内部每30秒往板上DDR里写一次统计数据,便于事后复盘。我打开了PC端的系统日志和FPGA的UART日志,同时监测FPGA芯片温度,看是否有因功耗过高导致的热漂移和时序劣化。整体跑下来,24小时无丢包,温度稳定在72°C左右,DDR读写正常。
这里要特别提示一点:如果你在FPGA里做了流量统计,一定要用64bit计数器。100G线速下,每秒收发约3.5亿个64字节小包,32bit计数器在不到1.2秒内就会溢出一次,用32bit统计会发现数据“莫名其妙地回绕”。
5. 常见问题与排查技巧实录
整个移植和测试过程里我遇到了大概七八个比较典型的问题,整理成了一张速查表,按“现象→原因→解决方式”给出,方便你到时候对照:
| 现象 | 可能原因 | 解决方式 |
|---|---|---|
| CMAC链路起不来,link_status一直为0 | 参考时钟频率不对、QSFP28模块未正确配置、光口无光模块 | 检查GTY REFCLK频率、I2C读模块寄存器确认线速率、换AOC线 |
| FPGA收不到任何UDP包,但RS-FEC和MAC层错误计数器不为0 | CMAC RX的data位宽和协议栈不一致,或CRC校验在模块内已做但协议栈又算了一遍 | 复位后统一配置数据位宽,CRC字段如果是MAC层处理的就不需要用户再做 |
| ARP能通但UDP数据包全丢 | IP校验和、UDP校验和字节序不对,PC端口被系统丢包 | 抓包看checksum是否为0或正确,临时关闭UDP校验和测试 |
| 大包吞吐99Gbps,小包只有80Gbps | PC网卡中断合并、驱动吞吐上限 | 调大网卡RX/RQ队列、开启larger socket buffer,或者用FPGA对打 |
| 一跑高吞吐就出现偶发丢包 | 跨时钟FIFO过浅,突发流量填满FIFO后被强制丢弃 | 增加FIFO深度,设置watermark,启动背压信号 |
| UART打印的计数器值跳变不正常 | 计数位宽不够(32bit溢出) | 改用64bit计数器,且接时钟域隔离同步 |
| 长时间运行后链路偶尔Down | 光模块热漂移,长时间高功率下SerDes信号劣化 | 加强散热,检查光模块温度,必要时降低RS-FEC等级 |
| iperf3客户端显示丢包,但FPGA侧CRC错误为0 | PC网卡内部UDP接收缓冲区太小 | Linux下设置net.core.rmem_default和net.core.rmem_max为256MB |
5.1 小包PPS瓶颈:是FPGA不行还是PC不行
这是很多做高速网络的同学会面临的灵魂问题。100G以太网在64字节小包情况下的理论极值是148.8Mpps,但PC端的Mellanox网卡在单队列接收时,受限于PCIe带宽和中断处理,实际能处理的PPS大概在20M-40M之间。也就是说,如果你用PC打小包流量,一定是PC先成为瓶颈。
我在测试中发现,当我把iperf3的小包流量调到极限时,PC端CPU占用冲到了100%,而FPGA内部的帧计数远低于线速。这说明瓶颈在PC发送端。为了验证FPGA本身能跑多快,我改用FPGA对打:一块板卡用简单的内部发包器构造64字节包往另一块板上发,另一块板的计数器统计到约126Mpps,这个数字基本说明协议栈本身是可以接近线速小包的。
5.2 PC端UDP接收缓冲区调优
如果你只用iperf3测试PC到FPGA的接收性能,建议提前做两件事:
sysctl -w net.core.rmem_default=268435456 sysctl -w net.core.rmem_max=268435456Windows下也可以调整注册表里的UDP动态缓冲区上限,否则在高速收到UDP数据时,系统会因为缓冲区满而丢包,但是显示上你只会看到模糊的“网络丢包”,很难排查。
调整后,我测出的PC接收FPGA大包流量能达到99Gbps,小包则大约在60Gbps左右(受限于网卡驱动和协议栈处理)。
5.3 CRC字段的字节序是个隐藏大坑
FPGA的CMAC硬核会负责以太网FCS(CRC32)的生成和校验,所以这部分用户侧不需要操心。但如果你用的是10G/1G等需要用户自己计算CRC的模块,那就必须注意:以太网FCS的字节序在网络上是“反转后发送”的,和你在FIFO里的字节顺序可能正好相反。常见错误是把计算好的CRC按顺序填进帧尾,导致对方设备看到CRC错误直接丢包。
开源协议栈里通常已经做了正确顺序的处理,但在端口适配时容易把这层封装不小心绕过,问题表现就是抓包工具能看到完整帧,但对方网卡就是不收。
6. 实测数据汇总与后续扩展思路
测试全部完成后,我把数据整理成一张汇总表:
| 测试项目 | 包大小 | 吞吐率 | 丢包率 | 备注 |
|---|---|---|---|---|
| FPGA接收PC发送 | 1472B | 98.6Gbps | 0% | PC端iperf3发送 |
| FPGA接收PC发送 | 64B | 61Gbps | 0.02% | PC端PPS受限 |
| FPGA回环 | 1472B | 98.4Gbps | 0% | 收发一体 |
| FPGA对打 | 9000B | 98.9Gbps | 0% | 双FPGA直连 |
| FPGA对打 | 64B | 96.1Gbps(约126Mpps) | 0% | 接近线速小包 |
从结果看,开源UDP协议栈在100G环境下是完全可用的,只要做好时钟收敛、字节序和FIFO深度设计,线速收发不成问题。
后续如果有充足时间,我准备在这个基础上做两件扩展:第一是把接收到的UDP流直接透传到DDR4,通过PCIe DMA搬移到上位机,这样就能变成一个真正的100G高速数据采集卡;第二是加入多队列和RSS哈希,让PC端多核能并行处理小包,把小包PPS再往上推一推。
我个人在整轮移植中最深刻的体会是,高速网络项目能不能顺利跑起来,拼的其实不是写逻辑的能力,而是对时钟、复位、字节序、跨时钟域这些基本功的理解。把这四样做好,开源代码一旦和环境接好,剩下的上板测试就是水到渠成的事。希望这份记录能帮你少踩几个我踩过的坑。