ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

开源100G FPGA UDP协议栈移植与上板调试实践

开源100G FPGA UDP协议栈移植与上板调试实践 最近刚把手头这块FPGA板子的100G UDP通路调通从开源IP移植到上板实测前后折腾了大概两周。整个过程踩了不少坑尤其是时钟和跨时钟域这两块几乎把常见的网上能搜到的问题都撞了一遍。这篇把整个移植和测试过程整理出来包括代码怎么改、IP怎么配、上板之后怎么验证以及那些文档里不会写但实际一定会遇到的坑。项目本身是基于GitHub上活跃度很高的开源FPGA以太网IP库把其中的100G MAC和UDP协议栈移植到目标板卡上然后用支持100G的网卡做对端通过UDP打流、回环和吞吐量测试来验证整条链路。这项工作适合已经在做FPGA网络开发、或者正准备上100G的同学参考尤其是那种“代码能综合但上板不通”的调试阶段应该能帮你省下不少时间。1. 项目概述与整体思路拆解1.1 这个项目到底在做什么先说清楚我们要实现的东西。FPGA上的100G UDP通路拆开来看就是一条完整的数据链路以太网物理层PCS/PMA负责把串行高速信号变成并行数据MAC层负责组帧和解析以太网帧再往上就是ARP、ICMP、IP和UDP协议处理。最终给用户的是一个简单的AXI-Stream接口用户把数据往里一塞加上目的MAC、目的IP、目的端口这些信息UDP协议栈就帮你把包发出去收包方向则是反过来解析完UDP之后把payload直接吐给用户。很多商用IP能做到同样的事但价格不便宜而且一旦涉及定制需求黑盒IP很难改。开源的方案就灵活得多代码全在你手里从MAC到UDP协议栈每一行都可以看可以做流量整形、过滤规则定制、协议扩展。这个项目参考的是verilog-ethernet这个开源库它的代码质量在FPGA开源圈子里算很不错的支持从1G到100G各个速率档位模块划分清晰接口标准化程度高移植到不同厂商芯片上的工作量相对可控。1.2 为什么选择开源方案选择开源而不是直接拿Xilinx或Altera的商用Ethernet IP主要出于三个考虑。第一个是成本。100G以太网IP的授权费用并不低尤其如果你的应用场景比较特殊比如只想要其中一部分功能、需要深度裁剪商用IP反而显得笨重。开源方案没有授权限制拿来就能用自己做评估和原型验证非常方便。第二个是可控性。商用IP基本就是一个黑盒里面的时序、缓冲策略、错误处理机制都是由IP厂商定好的出了问题除了看日志几乎没有往深处排查的手段。开源代码就不同了丢包时可以通过添加计数器、抓内部信号来定位是MAC层的checksum校验失败还是ARP表老化还是用户侧背压没拉起来。这个在调试阶段价值非常大。第三个是学习和知识积累。FPGA工程师如果只停留在“例化IP、配参数、连信号”的层面很难真正理解以太网协议栈的实现。读开源代码能把整个UDP/IP的实现细节吃透下次哪怕遇到完全不同的芯片和项目也能快速迁移。当然开源方案也有代价最大的代价就是“没有技术支持”所有问题都得自己扛。这就意味着移植前必须把代码结构和时序要求吃透否则排查问题的过程会很痛苦。1.3 整体架构与数据流向整个系统的数据流向可以这么理解FPGA内部有一个用户逻辑模块它通过AXI-Stream接口和UDP协议栈交互。发数据时用户逻辑把payload和包头描述符一起发给协议栈协议栈负责加上以太网头、IP头和UDP头做checksum计算然后交给MAC层组帧再经过PCS/PMA转换成高速串行信号通过光模块发出去。收数据时反过来光信号进来之后经过PCS/PMA恢复成并行数据MAC层做帧校验协议栈解析出UDP后把payload送给用户逻辑。在硬件层次上100G以太网物理层通常使用4路25G的高速收发器也就是QSFP28光模块的4个通道。每路25G内部走的是64B/66B编码PCS层会把数据分发到4个通道上接收端再做通道对齐和去偏斜。这一整套流程在开源代码里都有对应的模块整体来看是一份麻雀虽小五脏俱全的实现。2. 移植前的准备硬件、工具链与源码梳理2.1 硬件平台和工具链选型移植之前先得把家底盘清楚。我这里用的板卡FPGA芯片带16路GTM/GTY级高速收发器单路能跑到25.78125Gbps刚好满足4路25G拼100G的需求。板载一个QSFP28光模块接口通过这个接口连接光模块和光纤跳线对端用支持100G的网卡连接。网卡这边用的是常见的Mellanox ConnectX-5系列操作系统做了驱动更新确保支持100G速率和UDP的RX/TX能力。开发工具用的是Vivado版本比较新。如果要用开源代码里的仿真环境还需要Modelsim或VCS做RTL级仿真。我的建议是上板之前先把仿真认真跑几遍尤其要跑完MAC层的回环测试用例确认代码基本功能没问题再上板。否则板子上如果不同时出现好几个问题排查起来会很崩溃。2.2 开源代码目录结构与关键模块把开源库拉下来之后第一步就是熟悉目录结构。这个库的组织方式很清晰核心代码在lib/eth/rtl、lib/axis/rtl、lib/axi/rtl这几个目录下。lib/eth/rtl里是以太网相关的MAC和协议栈模块比如eth_mac_100g、eth_udp、eth_arp、eth_ip等。lib/axis/rtl里是AXI-Stream总线相关的通用组件FIFO、宽度转换、帧对齐这些模块都在里面。lib/axi/rtl则是AXI4总线的组件库里面有些高性能模块会用到AXI接口。移植的时候重点关注eth_mac_100g这个100G MAC核心以及以太网协议栈相关的模块。eth_mac_100g内部实现了100G以太网MAC层功能包括帧的封装、FCS校验和生成、preamble处理、流量控制等对外接口是标准的AXI-Stream。它内部还集成了和PCS/PMA的交互逻辑如果你的芯片用的是Xilinx的100G Ethernet IP来做物理层适配那需要把这部分接口对接好。UDP相关模块也在这个库里提供ARP请求和响应处理、IP层收发、UDP的封装和解析、以及一个简单的ARP缓存表。各模块之间通过AXI-Stream接口连接非常规范。2.3 时钟与复位的设计基础以太网是一个时钟要求很高的应用100G尤其如此。物理层需要156.25MHz的参考时钟给高速收发器使用这是100G以太网的标准参考时钟频率。PCS层通常工作在322.265625MHz左右MAC层用户接口在这个频率上跑512位宽的数据总线。为什么是322.265625MHz和512位算一下就清楚了100Gbps去掉编码开销后大约是103.125Gbps的有效速率除以512位就得到约201.4MHz再考虑到流控和时钟域转换实际用256位或512位的位宽配合适当频率。Xilinx的100G IP采用的是用户侧512位数据总线频率大约在322MHz左右通过设计内部缓冲来吸收速率差异。复位设计也是一个容易被忽视的坑。以太网MAC和物理层的复位时序要求比较多一般需要等收发器复位完成之后才能释放MAC复位。如果复位释放的时序不对会出现链接起来但收不到数据或者收到的数据全是错帧这类问题。3. 移植实施从RTL到烧录3.1 时钟树与收发器配置移植的第一步是先把时钟树搭起来。我的做法是分三条时钟路径来处理第一条是GT参考时钟用板上的156.25MHz可编程晶振或者外部时钟源通过IBUFDS_GTE4原语接入高速收发器第二条是PCS/MAC用户时钟由GT的恢复时钟或者内部PLL产生负责MAC用户逻辑的工作时钟第三条是普通逻辑时钟给ARP缓存、寄存器配置等慢速逻辑使用。收发器的配置是整个移植过程中最容易被低估的一步。100G模式下4个GT通道需要被配置成同一个Quad并且要做通道绑定channel bonding这样才能保证4个通道的数据在接收端对齐。Xilinx的GT工具里面可以配置为100G Ethernet模式会自动处理PCS的64B/66B编解码、加扰、对齐标记插入和移除、通道绑定等功能。在配置收发器参数时有几个值必须反复确认数据位宽是64B/66B模式下的66位还是64位参考时钟频率是不是156.25MHzTX和RX的极性要不要翻转取决于PCB走线这个要对照板卡原理图确认以及CDR的环路带宽等参数。3.2 AXI-Stream用户接口适配开源库里的UDP协议栈对外接口是AXI-Stream协议信号包括tdata、tvalid、tready、tlast、tkeep、tuser等。移植时要把这个接口和自己的用户逻辑对接起来。AXI-Stream的握手规则要牢记在心tvalid和tready同时拉高时数据才有效传输tlast表示一帧的结束。tkeep用于指示最后一拍数据有效字节数。如果tvalid拉高之后tready一直不拉高发送方必须保持数据不变。我这边用户逻辑的数据位宽和协议栈不一致原来用的位宽是256位协议栈处理100G的位宽是512位所以中间插了一个位宽转换FIFO。建议直接用库里现成的模块来做位宽转换比如lib/axis/rtl下的axis_adapter。使用时要特别小心它的数据对齐模式以及是否需要在帧首和帧尾插入额外的字节对齐处理。还有一个细节UDP协议栈通常要求用户以整帧为单位来处理数据也就是一次涉及到一整个UDP报文不能把payload拆成多次不完整的发送。如果用户逻辑是流式数据可能需要在端侧加一个组帧模块等数据攒成一整帧之后才启动发送。我这边的做法是加了一个简单的组包缓存收到一个完整的用户业务报文后生成以太网帧并发送。3.3 UDP协议栈的例化与参数配置协议栈例化这一步要仔细看代码里的参数说明。主要参数包括MAC地址、IP地址、端口号、ARP缓存大小、超时时间等。需要注意IP地址和MAC地址通常需要在上电后由用户通过寄存器配置进去而不是在RTL里写死。开放代码里通常会留一个简单的寄存器配置总线接口可能是AXI-Lite或者简单的寄存器读写信号。移植时把这组信号接到自己的控制模块上即可。在使用UDP发送接口时需要给协议栈提供目的MAC地址、目的IP地址和目的UDP端口。目的MAC地址一般通过ARP协议自动获取协议栈会缓存一个ARP表发数据时查询表项如果没有就会发起ARP请求并等待响应。这是整个过程中一个典型的延迟点首次通信时会有RTT的等待时间如果不想等可以手工预填充ARP表项。我测试时用了一个比较笨但有效的办法先把对端的静态ARP条目写在代码里减少首次通信时的ARP解析延迟。等到功能都验证通了再把静态条目去掉测试动态ARP解析流程。4. 上板测试从链路点亮到打满100G4.1 基础连通性测试上板之后第一步要做的是确认物理链路是通的。把光模块插好光纤跳线连接FPGA板卡和对端网卡然后用ethtool查看对端网卡的链路状态。如果link是up说明物理层协商已经完成光模块信号质量、收发器的CDR锁定、PCS对齐这些都正常。如果link是down问题往往出在光模块兼容性、光纤是否接反、收发器参数配置这几个方面。确认link up之后先在FPGA这边做MAC内部回环测试。具体做法是让MAC层在发送侧直接把数据环回到接收侧不经过物理线路。这个测试可以证明MAC和协议栈内部的数据通路没有问题。通过之后再做外部回环用光纤跳线把光模块的TX和RX短接这样数据从FPGA发出去经过光模块、光纤再回到FPGA接收端。这个测试可以检验物理层收发通道。等到外部回环也通了再连接真实对端网卡。此时可以在FPGA侧配置好IP地址对端网卡也配置同一个子网的IP地址然后用ping命令测试ICMP是否通。ping的通畅说明ARP、IP和ICMP协议栈这几个环节基本工作正常。这里会遇到一个典型问题如果对端网卡的驱动或系统防火墙拦截了ICMP就会ping不通所以要在系统层面把防火墙规则清理干净或者直接用UDP打流工具来测试。4.2 抓包验证UDP数据ping通之后进入真正和UDP相关的验证环节。我在对端主机开启Wireshark抓包然后在FPGA侧构造一个UDP报文发往对端。抓包需要确认三件事第一报文能够正常到达对端网卡第二帧的MAC地址、IP地址、UDP端口和checksum全部正确第三payload内容和FPGA发送的数据一致。第一次抓到畸形帧也别慌先看checksum。很多时候UDP/IP checksum的计算方式容易弄错IP头校验和是必须计算的UDP校验和在IPv4下可选但在IPv4下如果UDP源端口或目的端口是正确的而校验和计算错误Wireshark会打上bad checksum的标记。你可以临时关闭UDP校验和检查来缩小问题范围但正式使用时还是建议把checksum逻辑做对。还有一次我碰到个诡异问题ARP能通、ICMP也能通但UDP发出去就不通。后来抓包发现UDP报文长度字段比实际payload长度大了一个固定数值导致接收端认为数据不完整。排查下来是UDP协议栈生成UDP长度字段时默认计算了UDP头长度而我自己的组帧模块也把UDP头长度加了进去重复了一次修正这个后问题就消失了。4.3 吞吐量测试与速率对齐能让UDP包通起来只是第一步还要验证100G的吞吐量是否达标。这里我用了iperf3这个工具UDP模式打流。命令大概是这样iperf3 -c 192.168.1.2 -u -b 0 -t 30其中-b 0表示不限带宽尽量发满。实际上iperf3在-UDP-C用默认方式发送时会受到系统UDP缓冲区大小限制需要把系统缓冲区调大否则发不满。可以先用sysctl把对应的缓冲区调到256MB以上或者用iperf3自带的窗口参数调整。打流结果主要看两个数据带宽Mbits/sec和丢包率。如果你看到带宽稳定在99Gbps以上且丢包为零那说明整条链路的数据通路是健康的。如果丢包严重问题基本出在接收侧FPGA这边可能是接收FIFO溢出后没有及时告诉用户逻辑去取数据网卡这边可能是系统缓冲区太小导致内核丢包。比较实用的一个技巧是同时抓两边数据在网卡侧用ethtool -S查看rx_dropped等计数器在FPGA里用ILA抓内部计数器两边对照就能快速定位丢包是在物理层、MAC层、协议栈还是系统层。5. 常见问题与排查技巧实录5.1 链接起不来先查物理层再怀疑代码碰到link起不来不用慌按照从底层到高层的顺序排查。第一看光模块和光纤QSFP28光模块如果插不到位或者光纤接头脏了link一定起不来。很多时候表面看着插紧了实际上Lock Pin没有卡到位。这个在板卡上没有明确提示时只能靠仔细检查。第二看GT参考时钟用Vivado里的眼图工具看一下接收端信号质量如果眼图完全不开说明时钟或者信号完整性有问题。参考时钟频率不对是一个特别容易忽视的原因你可以用IBERT工具先跑一遍确认收发器的收发通路都能锁定。第三看PCS状态打开收发器的状态寄存器看RX的CDR是否锁定、deskew是否完成、alignment marker是否找到。如果deskew失败多半是一个通道的极性接反了或者PCB上通道之间的延迟差太大需要通过配置deskew FIFO把延迟补偿窗口调大。5.2 丢包问题缓冲策略是核心100G场景下丢包问题的根源基本都是缓冲不足而不是计算能力不够。FPGA内部的BRAM容量是有限的当UDP流量速率接近满速并且突发的数据总量超过缓冲深度时必然丢包。优化的思路有三条第一加大FIFO深度。这个方法简单直接但BRAM资源有限治标不治本。第二优化背压机制。确保上游模块能及时收到FIFO满的信号不要让数据在FIFO后面堆积。第三把数据直接写到DDR里。在用户逻辑侧直接通过AXI接口把收到的UDP数据写入DDR这样缓冲深度只受DDR容量限制。我这里实测下来如果只是做功能验证用BRAM FIFO足够了但要做满速压测并且跑长时间稳定性测试还是得把接收侧的数据导向DDR否则跑个几分钟就会因为某个瞬间的拥塞导致丢包。5.3 跨时钟域和位宽转换的坑100G设计里到处都是跨时钟域问题。GT的恢复时钟和MAC的用户时钟往往不是同源的用户逻辑又工作在另一个时钟域。如果直接用一个时钟域的寄存器去采样另一个时钟域的信号偶尔会出现亚稳态表现出来就是偶发的错帧、checksum错误、甚至数据重复。解决跨时钟域问题的标准方案是异步FIFO开源代码里已经提供了不少现成模块直接用就行。但要注意异步FIFO的读写指针同步是格雷码实现的如果复位时序不对可能会产生伪空伪满的情况。我建议在复位释放之后做一次空满状态检查确保FIFO状态拉回到初始值再开始数据传输。位宽转换方面如果从64位转到512位还要额外注意tkeep信号的生成。tkeep用于标注最后一拍的有效字节数如果计算错误数据尾部的对齐会乱掉。一个经常犯的错误是把所有字节看作有效字节没有按实际payload长度来生成tkeep导致收端收到一些垃圾尾字节。5.4 开放代码移植时的常见编译错误移植开源代码时最常见的编译错误是参数宽度不匹配和模块例化名称不同。不同版本的Vivado对Verilog语法支持有差异尤其是一些较新的SystemVerilog特性在老版本里可能不支持。遇到编译错误时先看错误信息里涉及的是哪个文件再检查是不是参数宽度或者接口名称不匹配。开源代码里很多接口宽度是参数化的例化时必须明确指定正确的参数值。否则会默认生成和实际需求不符的数据路径综合出来的资源使用量也会异常。另外一个比较隐蔽的问题是IP核版本差异。如果你的工程已经有其他版本的AMD/Xilinx 100G IP核可能会和开源代码里的PCS/PMA接口不一致导致信号名称对不上。解决的办法是严格按照你使用的IP核版本生成接口然后做一层适配逻辑把IP核的接口信号映射到MAC模块期望的信号上。写在最后把开源100G FPGA UDP协议栈移植并在板子上跑通说实话是个很磨人的过程。但调通之后回看从MAC层的帧收发到UDP协议栈的完整报文处理再到对接物理层的收发通道整个链路在自己手里逐渐清晰起来这种收获是拿商用IP黑盒实现完全体会不到的。如果你也在做类似的事情建议先花时间把开源代码的仿真用例跑明白不要急着上板。仿真和上板结合才能真正做到心里有数。遇到问题不要只盯着某一个模块查而是要带着“数据在整个链路里是怎么流动的”这个全局视角去排查。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进