news 2026/9/7 3:07:46

FPGA 100G UDP协议栈开源移植实战:从CMAC适配到线速打流

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
FPGA 100G UDP协议栈开源移植实战:从CMAC适配到线速打流

这个项目的起因其实很直接——手头拿到了一个带双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-ethernet100GAXI4-Stream / 自定义总线无(MIT类)支持CMAC硬核适配更新活跃
COREnite UDP/IP25G自定义需商业许可证稳定但更新慢
自研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接口非常标准,但数据位宽和信号命名可能有差异,所以需要一个适配层把两者对齐。

我按照下面的方式处理:

  1. 先把CMAC的s_axis_tx接口直接拉到顶层,注意CMAC的tuser信号里有帧错误标记,如果不需要可以忽略。
  2. 协议栈内部的udp接口则通过一个宽度转换FIFO适配。因为协议栈的内部数据通路是64bit(10G直出)或者512bit(100G直出),你需要根据实际选型把位宽对齐。我用的是512bit通到CMAC,所以协议栈内部给MAC层的数据接口也保持512bit,避免每拍还要做位宽拼接的额外组合逻辑。
  3. 特别要注意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层错误计数器不为0CMAC RX的data位宽和协议栈不一致,或CRC校验在模块内已做但协议栈又算了一遍复位后统一配置数据位宽,CRC字段如果是MAC层处理的就不需要用户再做
ARP能通但UDP数据包全丢IP校验和、UDP校验和字节序不对,PC端口被系统丢包抓包看checksum是否为0或正确,临时关闭UDP校验和测试
大包吞吐99Gbps,小包只有80GbpsPC网卡中断合并、驱动吞吐上限调大网卡RX/RQ队列、开启larger socket buffer,或者用FPGA对打
一跑高吞吐就出现偶发丢包跨时钟FIFO过浅,突发流量填满FIFO后被强制丢弃增加FIFO深度,设置watermark,启动背压信号
UART打印的计数器值跳变不正常计数位宽不够(32bit溢出)改用64bit计数器,且接时钟域隔离同步
长时间运行后链路偶尔Down光模块热漂移,长时间高功率下SerDes信号劣化加强散热,检查光模块温度,必要时降低RS-FEC等级
iperf3客户端显示丢包,但FPGA侧CRC错误为0PC网卡内部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=268435456

Windows下也可以调整注册表里的UDP动态缓冲区上限,否则在高速收到UDP数据时,系统会因为缓冲区满而丢包,但是显示上你只会看到模糊的“网络丢包”,很难排查。

调整后,我测出的PC接收FPGA大包流量能达到99Gbps,小包则大约在60Gbps左右(受限于网卡驱动和协议栈处理)。

5.3 CRC字段的字节序是个隐藏大坑

FPGA的CMAC硬核会负责以太网FCS(CRC32)的生成和校验,所以这部分用户侧不需要操心。但如果你用的是10G/1G等需要用户自己计算CRC的模块,那就必须注意:以太网FCS的字节序在网络上是“反转后发送”的,和你在FIFO里的字节顺序可能正好相反。常见错误是把计算好的CRC按顺序填进帧尾,导致对方设备看到CRC错误直接丢包。

开源协议栈里通常已经做了正确顺序的处理,但在端口适配时容易把这层封装不小心绕过,问题表现就是抓包工具能看到完整帧,但对方网卡就是不收。

6. 实测数据汇总与后续扩展思路

测试全部完成后,我把数据整理成一张汇总表:

测试项目包大小吞吐率丢包率备注
FPGA接收PC发送1472B98.6Gbps0%PC端iperf3发送
FPGA接收PC发送64B61Gbps0.02%PC端PPS受限
FPGA回环1472B98.4Gbps0%收发一体
FPGA对打9000B98.9Gbps0%双FPGA直连
FPGA对打64B96.1Gbps(约126Mpps)0%接近线速小包

从结果看,开源UDP协议栈在100G环境下是完全可用的,只要做好时钟收敛、字节序和FIFO深度设计,线速收发不成问题。

后续如果有充足时间,我准备在这个基础上做两件扩展:第一是把接收到的UDP流直接透传到DDR4,通过PCIe DMA搬移到上位机,这样就能变成一个真正的100G高速数据采集卡;第二是加入多队列和RSS哈希,让PC端多核能并行处理小包,把小包PPS再往上推一推。

我个人在整轮移植中最深刻的体会是,高速网络项目能不能顺利跑起来,拼的其实不是写逻辑的能力,而是对时钟、复位、字节序、跨时钟域这些基本功的理解。把这四样做好,开源代码一旦和环境接好,剩下的上板测试就是水到渠成的事。希望这份记录能帮你少踩几个我踩过的坑。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 3:07:17

OpenGL与C++图形学入门:渲染管线、着色器与光照模型实战解析

简介:计算机图形学编程(使用OpenGL与C)课程配套辅助材料,面向正在学习图形学基础与OpenGL开发的学生,可配合教材边读边翻阅。压缩包共1035个文件,约470.45MB;内容以pptx课件、219个cpp与136个h源…

作者头像 李华
网站建设 2026/9/7 3:05:29

开源电路在工训赛中的价值:从需求拆解到系统集成

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 3:04:17

FastAPI 进阶实践:直接注入并使用 Request 对象

FastAPI 进阶实践:直接注入并使用 Request 对象 【免费下载链接】fastapi FastAPI framework, high performance, easy to learn, fast to code, ready for production 项目地址: https://gitcode.com/GitHub_Trending/fa/fastapi 在 FastAPI 中,…

作者头像 李华