ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

FPGA实现100G UDP协议栈移植:从CMAC适配到线速收发验证

2026/9/5 6:15:18 拓冰建站 浏览量
FPGA实现100G UDP协议栈移植:从CMAC适配到线速收发验证 做100G网络这块有一段时间了最近把一个开源的UDP协议栈移植到了Xilinx UltraScale平台上跑通了100Gbps的线速收发。从拿到开源代码到上板测出满速中间踩了不少坑也梳理出不少关键点。今天把这套移植上板测试的完整过程整理出来包括方案选型、工程搭建、代码适配、打流验证、问题排查希望能给正在搞100G以太网和FPGA的朋友一些参考尤其是那些准备拿开源UDP方案快速起步的同学这篇文章应该能帮你少走不少弯路。这个需求在数据中心、高性能计算、高速数据采集、信号处理板卡互联这些场景里特别常见一块FPGA要把高速数据从板卡上搬出去或者从网络里收进来UDP是最实用、最少开销的协议选择。很多自研交换机、网络加速卡、金融行情加速、雷达信号处理板卡底层跑的都是这套东西。对于刚接触高速以太网的工程师来说开源UDP协议栈加100G CMAC核是一个门槛相对低、验证充分的起点。接下来我直接按整个项目的推进顺序来写从设计思路到最后的实测定型尽量把细节都交代清楚。1. 项目整体设计与思路拆解1.1 100G UDP协议栈移植的本质是什么很多朋友一听“100G UDP”第一反应是拿MicroBlaze或者Zynq软核去跑LwIP但这条路在100G带宽下根本走不通。通用处理器跑协议栈单核能跑到千兆线速已经很吃力了到了10G就必须多队列加硬件卸载100G基本是纯硬件电路的活儿。所以我们这颗板卡上所有的UDP收发逻辑都是用Verilog在可编程逻辑里写死或者用IP核生成的整个数据路径上没有一个CPU参与。那“移植”到底是移什么简单说把开源的UDP协议栈代码拿过来接到商用或开源的100G以太网MAC上再配上板卡自己的用户逻辑接口。具体拆开是这样几块物理层100G光模块加高速SerDes这部分由Xilinx集成在以太网IP核里我们基本不碰物理信号。数据链路层100G以太网MAC负责成帧、CRC校验、流控、前导码处理。UltraScale上叫CMAC是硬核不需要额外付费。网络层和传输层UDP/IP协议栈负责组IP包头、算校验和、做ARP、ICMP回应、IP分片重组这些逻辑。这部分是开源代码的核心也是移植工作量的集中地。用户接口协议栈和用户逻辑之间的数据通道通常是AXI-Stream一端连MAC一端连用户应用。整条链路的数据流是用户逻辑产生数据封装成UDP包发出去收到的网络包拆掉以太网和IP头把载荷交给用户逻辑处理。1.2 为什么选择开源方案省了什么又费了什么选开源方案核心原因是时间成本。商用UDP协议栈IP一般按带宽计费100G级别的授权费不低而且商务流程、NDA、评估授权往往要几周时间。开源方案随手就能拿到完整的RTL代码GitHub上拉下来直接看逻辑心里踏实。另一个原因是可定制性很多商用IP是网表或者加密代码出了问题只能找原厂FAE开源代码你能自己加探针、加统计、改接口时序这对后面调板子特别重要。但那句老话得说在前面开源不等于免费。省了授权费费的是集成和调试的精力。你拿到的开源代码是针对某种MAC接口、某个时钟域、某种位宽写的你的平台上CMAC核的接口位宽可能是512bit开源代码可能只适配了64bit或256bit整个AXI数据通路的时序、跨时钟域处理、FIFO深度都得重新审视和调整。更别说上板之后遇到丢包、校验错误、时序收敛问题调试难度远高于仿真。我选的方案是Alex Forencich的开源verilog-ethernet项目这个在业内认可度很高代码风格干净、结构清晰、文档也还凑合。它包含以太网MAC、UDP协议栈、ARP、ICMP、CRC等等一堆模块最重要的是它提供了一个独立成型的udp_ip_stack顶层模块接口是标准AXI-Stream做适配非常方便。另外我B站上还看到同济子豪兄的开源机器鸭那类项目用的是国产开发板做的网络通信虽然速率没有100G那么夸张但整个开源思路和学习路径是相通的对刚入手的朋友可以参考参考。1.3 目标架构从MAC到用户的完整数据通路在动代码之前先把整条数据通路画清楚这一步决定了后面所有接口的定义和时序约束。我的目标架构是这样的100G QSFP28光模块四路25G SerDes接进FPGACMAC硬核配置成100G模式数据位宽512bit用户时钟322.265625MHzCMAC的AXI-Stream接口接到udp_ip_stack顶层UDP协议栈输出用户侧的AXI-Stream接到DMA模块或者自定义的用户逻辑用户逻辑负责产生测试数据和消费数据以及统计收发帧数、字节数。这里有一个关键点CMAC的收发时钟和用户逻辑的时钟可能不在同一个时钟域UDP协议栈内部也会重新打拍同步所以跨时钟域的处理是移植里最容易出问题的地方。我在协议栈和CMAC之间加了异步FIFO在协议栈和用户逻辑之间也加了异步FIFO确保两边的时钟域隔离同时用FIFO的almost_full信号反压用户逻辑。为什么UDP能跑线速而TCP不行这个也得提前说清楚。UDP协议栈是无连接的不需要维护连接状态、不需要序号确认、不需要窗口管理和重传所以状态机非常简单每个包的处理延迟是固定的流水线级数。TCP协议栈在FPGA里做得好的也有但复杂度高一个数量级要维护每条连接的收发状态、定时器、重传缓冲区100G线速TCP基本需要多连接并行处理。在高速板卡互联场景里可靠传输通常由上层应用自己处理或者靠链路层的流控所以UDP是绝对主流的选择。2. 硬件平台与工具链准备2.1 开发板选型UltraScale是100G的底线不是说其他平台不能做而是UltraScale这个级别做100G最顺手。主要是两个原因一是内置CMAC硬核二是SerDes速率和数量足够。如果你用的是Kintex-7这代器件100G需要外接PHY芯片或者用软核MAC加GTY不仅时序压力大而且逻辑资源占用非常夸张几乎没有实用价值。到了UltraScale这一代100G MAC直接在芯片内部GTY SerDes也支持100G速率一片VU9P就能搞定4个100G端口。选型的时候重点看这几项资源逻辑资源LUT和FF至少要到30万以上UDP协议栈加用户逻辑再加缓存控制大概能吃掉一两万LUT留足余量给后续扩展。BRAM/URAM跨时钟域FIFO和包缓存是BRAM大户尤其100G带宽下FIFO深度不能太小不然瞬间拥塞就丢包。高速收发器QSFP28端口对应的GTY通道数量要够。硬核MACUltraScale上叫CMACVU9P这种大器件自带多个CMAC实例。DDR控制器如果要在板卡上做大缓存或者协议处理DDR4的控制权也要规划好。我用的是一块自研板卡VU9P主芯片板载两个QSFP28光口一个DDR4 SODIMM插槽整板供电和时钟设计都比较完善。这块板子最大的好处是CMAC的参考时钟走的是专用的低抖动时钟树对100G的SerDes眼图影响很大换板子一定要确认这一点。2.2 Vivado工程搭建版本、IP配置和约束Vivado 2022.2这是当前比较稳定的版本对UltraScale支持成熟。工程结构上我是按RTL、IP、约束、仿真四块分开放的这样多人协作和后续版本管理都方便。CMAC的配置有几个关键项得仔细说Line Rate选100G接口位宽选512bit这时用户时钟是322.265625MHz这是一个派生时钟必须用PLL生成不能直接在约束里乱写。数据接口选AXI-Stream不带TUSER还是带TUSER会影响后面协议栈适配。我习惯带TUSER因为TUSER里可以带CRC结果和错误标记方便调试。CRC处理和Preamble PassThrough这些选项按CMAC的默认设置来协议栈代码自己会重新处理校验。流控选项100G下建议开启Priority-based Flow Control或者至少让CMAC的RS-FEC处于开启状态。这里有个坑如果光模块链路质量不够好丢包可能是物理层误码导致的和协议栈一点关系都没有所以RS-FEC能开就开。约束文件方面除了时钟约束最需要注意的是异步FIFO的时钟域约束。Vivado会自动推断CDCClock Domain Crossing路径但建议手动设置set_clock_groups -asynchronous把CMAC时钟域、用户逻辑时钟域、DDR时钟域分开避免时序分析报出大量跨时钟域违例。我见过很多新手的工程就是没设这组约束Vivado时序报告里全是红的FIFO跨时钟路径根本没法收敛。工具链还有一个小建议仿真和上板分开建工程不要用一个工程又仿又跑。仿真要用仿真专用的IP配置比如CMAC会变成behavioral model上板要用综合后的IP配置两套混在一起很容易出髒版本。我是直接复制工程文件夹仿真和上板各用各的省了很多麻烦。3. 开源UDP协议栈移植实操3.1 开源IP结构解析先摸清每一级模块从GitHub拉下来的代码目录结构清晰度很重要我用的verilog-ethernet项目顶层是rtl/udp_ip_stack.v里面例化了UDP、IP、ARP、ICMP这些子模块。刚开始不要急着改代码先把每个模块的接口信号和用途搞清楚。我习惯把每个模块的例化关系画成树状图标清楚每个端口的来源和去向这样后面改一处地方能快速评估影响。我的分析顺序是先从接口上看udp_ip_stack对外有哪些端口哪些是接MAC的哪些是接用户逻辑的然后看内部数据流发送方向用户数据怎么打包成IP包接收方向网络包怎么解包最后看配置和管理接口比如MAC地址、IP地址、ARP表、校验和控制怎么配置。这个项目里udp_ip_stack对外接口大致分为三类接MAC的AXI-Stream收发接口发送和接收各一组接用户逻辑的AXI-Stream收发接口发送和接收各一组配置端口包括本机MAC地址、本机IP地址、子网掩码、网关IP、ARP缓存操作等。接口信号名字很规范mac_tx_、mac_rx_、udp_tx_*、udp_rx_*这样的命名一眼就能认出来。需要注意的是AXI-Stream的握手信号tvalid/tready/tlast/tdata/tkeep这套是标准接口但不同版本vaild和ready的时序策略可能有细微差别比如是否允许在tlast之后立即开始下一包。3.2 移植过程中的核心改造点接入CMAC的AXI-Stream位宽匹配是第一个要解决的问题。开源ip_stack通常用的是64bit数据位宽而CMAC是512bit位宽不一致导致整个接口适配层必须改。我采用的方案是在中间加一个位宽转换模块把512bit转成64bit或者反过来配一个异步FIFO做时钟域隔离。这个转换模块的难点是tkeep和tlast的处理跨位宽转换时tlast的位置会变tkeep也要跟着调整写不好就会把包长度弄错。我实测下来如果只是纯位宽转换不加FIFO逻辑资源会省一点但时序很难收敛尤其是512bit到64bit这种大跨度转换组合逻辑太长跑到322MHz基本不可能。所以老老实实加FIFO用两级流水换时序收敛。第二个改造点是用户侧接口的协议适配。开源项目的用户侧接口是AXI-Stream read和write分离的每个方向都有自己的tdest/tuser这些信号。我的用户逻辑是DMA写的数据格式是按descriptor组织的必须做一个小的转换桥把DMA的通道映射到UDP协议栈的AXI-Stream上。这里我多花了一些时间因为DMA的burst长度和UDP包长不是对齐的需要在发送方向把DMA burst拆成多个UDP包接收方向把UDP包拼成DMA burst。第三个改造点是出端口逻辑。ip_stack顶层默认只有一组MAC接口但我的板卡有2个QSFP28光口要做端口选择。我的做法是在MAC接口层做一个简单的交叉开关crossbar根据目的MAC或者用户指定的端口号把数据路由到对应的CMAC。这个功能虽然小但涉及收发包的两条路径状态机写起来要特别小心不然容易丢包或者错序。第四个需要注意的点是ARP和ICMP。100G UDP能跑通但你在局域网里和PC通信PC首先要发ARP请求解析MAC地址UDP协议栈必须正确回应ARP不然数据包根本发不过去。开源代码里ARP模块是有的但需要对一下它的接口和你的配置方式。ICMP回应用于ping测试建议保留排查链路不通时特别有用一个ping通了就说明物理层、MAC层、IP层基本没问题。3.3 关键代码和参数设置示例这里记录一段我当时最常改的参数配置方便大家参考。在本机MAC和IP初始化部分开源工程通常用一个parameter或者寄存器来配置。我用的是寄存器方式上电时由软核写入// 配置本机MAC地址 00:11:22:33:44:55 assign local_mac[47:0] 48h001122334455; // 配置本机IP地址 192.168.1.10 assign local_ip[31:0] 32hc0a8010a; // 配置子网掩码 255.255.255.0 assign subnet_mask[31:0] 32hffffff00; // 配置网关IP 192.168.1.1 assign gateway_ip[31:0] 32hc0a80101;发送一个UDP包的用户逻辑侧控制信号大致是这样的格式// 用户侧发送UDP包 assign udp_tx_tvalid user_send_valid; assign udp_tx_tdata user_send_data; assign udp_tx_tkeep user_send_keep; assign udp_tx_tlast user_send_last; assign udp_tx_tdest 8h01; // 目标端口选择 assign udp_tx_tuser user_send_user;目标端口的选择要看协议栈的端口定义我用的这个工程是通过tdest来区分不同的目标IP和端口需要在代码里自己维护一张端口映射表。这个设计用起来很方便用户逻辑只需要设置一个tdest值协议栈内部就会自动查表封装对应的目标MAC、目标IP、源端口、目的端口。4. 上板验证与性能测试4.1 上板前的准备时钟约束和ILA调试上板不是代码编译过了就直接跑的尤其是100G这种高速接口物理层的准备必不可少。我先用IBERT集成误码率测试工具验证光模块和SerDes链路确保误码率为零或者极低。IBERT通过JTAG或者PCIe接口访问GTY收发器直接做PRBS测试几分钟就能确认物理层是否可靠。这一步非常关键很多时候链路速率跑不上去并不是逻辑问题而是SerDes参数不对IBERT能帮你排除掉这个变量。逻辑上板之前我习惯在关键节点插入ILA集成逻辑分析仪探针CMAC到协议栈的发送路径上观察tvalid/tready/tlast/tdata的时序协议栈到用户逻辑的接收路径上观察包头的字段解析是否正确ARP请求处理的状态机观察是否有效响应。ILA的采样深度根据FPGA BRAM资源来定100G速率下采样深度太小根本抓不到完整的包我一般采样深度设成65536采样时钟用对应接口的时钟。有一点要注意ILA本身会占用大量布线资源插入之后对时序收敛可能有影响所以上板验证时先用一个低占用版本等确认功能正常后再去掉ILA做完整时序收敛。4.2 iperf3 UDP打流测试从千兆到100G逐级验证平台准备好之后先用小带宽测试再用大带宽测试。这一步我用iperf3在服务器和FPGA之间打UDP流服务器装的是Mellanox ConnectX-5 100G网卡FPGA侧用ILA统计收发包数量。iperf3的命令大概是# 服务器作为接收端 iperf3 -s -p 5201 -i 1 # FPGA侧通过PC端发起UDP打流 # 这里注意带宽参数先从小带宽开始 iperf3 -c 192.168.1.10 -u -b 100M -p 5201 -l 1400 -t 30 iperf3 -c 192.168.1.10 -u -b 1G -p 5201 -l 1400 -t 30 iperf3 -c 192.168.1.10 -u -b 10G -p 5201 -l 1400 -t 30 iperf3 -c 192.168.1.10 -u -b 50G -p 5201 -l 1400 -t 30测试下来发现小带宽下丢包率都是0一到50G以上就开始在服务器侧看到接收丢包。一开始我怀疑是FPGA协议栈问题后来排查发现是服务器侧的UDP接收缓冲区不够大。Linux默认的UDP接收缓冲对百Gbps来说太小了需要调大系统参数sudo sysctl -w net.core.rmem_max67108864 sudo sysctl -w net.core.rmem_default67108864 sudo sysctl -w net.core.wmem_max67108864 sudo sysctl -w net.core.wmem_default67108864调整之后50G掉包的现象基本消失。这里也提醒一下用iperf3做100G打流测试服务器的网卡设置也很关键。如果网卡的RSS、GRO、LRO这些卸载功能没有正确配置也会造成接收端CPU处理不过来而丢包即使缓冲区调大了也没用。我当时的做法是关闭GRO和LROsudo ethtool -K enp3s0f0 gro off sudo ethtool -K enp3s0f0 lro off最终测到接近满速FPGA转发方向的线速数据率达到98.5Gbps按1500字节帧长计算pps大概是800万左右。这个速度意味着每秒钟处理800万个UDP包CMAC和协议栈之间的握手效率至关重要任何一个慢下来都会成为瓶颈。4.3 Wireshark抓包验证协议栈行为分析除了打流还要用Wireshark在PC侧抓包确认协议栈封装的包格式完全正确。抓包以后主要看几个点以太网头目的MAC、源MAC是否正确IP头版本、IHL、总长度、协议字段应为17即UDP、校验和是否有效UDP头源端口、目的端口、长度、校验和UDP校验和可以为零具体看实现ARP交互PC发ARP请求FPGA是否正确回ARP响应ICMPPC ping FPGA是否能正常回ICMP echo reply。Wireshark的显示过滤器可以这样用udp arp icmp ip.addr 192.168.1.10有一次抓包发现UDP校验和总是被Wireshark标记为错误的找了半天发现不是协议栈问题而是网卡的UDP checksum offload在作怪。Mellanox网卡默认开启了UDP checksum offload抓包的时候显示的是网卡写入的伪校验值和实际线上的包不一样。关掉网卡的checksum offload再看就正常了sudo ethtool -K enp3s0f0 tx-checksum-ip-generic off sudo ethtool -K enp3s0f0 rx-checksumming off这个坑非常经典提醒大家抓包验证协议栈时一定要关掉网卡的硬件校验和卸载否则会得到很多误报。5. 常见问题与排查技巧实录5.1 一张问题速查表先对着查一遍现象排查方向解决建议端口link up但打流无流量物理层/CMAC配置先用IBERT测试SerDes再用CMAC自带计数器看收发包数量小带宽正常大带宽丢包接收端缓冲区/AXI流控调大UDP收发缓冲检查FIFO的almost_full反压是否生效PC ping不通FPGAARP/ICMP/Routing抓包看ARP是否响应确认本机MAC/IP配置是否正确Wireshark报UDP校验和错误网卡checksum offload关闭网卡硬件校验卸载重新抓包时序收敛不过跨时钟域FIFO/ILA占用加set_clock_groups异步约束去掉或减小ILA采样深度100G线速跑不满数据路径瓶颈分析CMAC tready拉低的占比检查位宽转换和FIFO深度丢包是周期性或者随机的光模块误码/SERDES参数用IBERT长时间PRBS测试调整TX emphasis参数5.2 时序收敛不了是常态心态和方法都要调整100G工程的时序收敛是出了名的痛苦。主要压力是CMAC用户时钟322.265625MHz这个速率下任何一条关键路径的组合逻辑都不能太长否则直接红。我的经验是按模块分别约束、分组收敛不要指望一次place and route全部通过。常见的几个优化手段在数据路径上插入流水寄存器用面积换时序。比如位宽转换逻辑拆成多级流水每级只做一小段组合逻辑。不要用纯组合逻辑做复杂的包头解析改成每个周期只处理一个字段分多个周期完成。虽然增加了延迟但时序好很多。使用同步FIFO而不是异步FIFO来分担时序压力跨时钟域只在少数几个固定点做。关键路径上避免使用高扇出的控制信号比如复位信号要用同步复位配合全局复位树并且尽量让复位信号的扇出降低。复杂状态机拆成多个小状态机每个状态机只负责一个简单功能避免一个大状态机组合逻辑链太长。我在做位宽转换模块的时候第一次跑综合到322MHz直接时序违规关键路径长度超出0.6ns。后来把位宽转换拆成多级流水每级位宽依次递减比如512→256→128→64每一级之间插寄存器时序瞬间就收敛了。所以遇到时序过不去优先考虑加流水不要一上来就调布局布线选项。5.3 丢包问题的系统性排查思路丢包是最头疼的问题因为可能的原因太多。我总结了一套排查顺序从物理层往应用层查第一步物理层。用IBERT做PRBS测试长时间跑至少10分钟如果误码率高说明是光模块、光纤或者SerDes参数问题。这一步不过后面查什么都是白搭。第二步MAC层。看CMAC的统计计数器Xilinx的CMAC核自带一组收发统计寄存器可以查CRC错误帧数、超长帧数、超短帧数、溢出丢弃数。如果CRC错误帧很多大概率还是物理层误码而不是协议栈问题。第三步协议栈内部。在ILA里观察UDP协议栈的输入和输出端口看tvalid/tready的握手情况。如果tready长期为低说明下游堵了如果tvalid和tready都高但数据没出来说明协议栈内部状态机卡住了。第四步用户逻辑。看用户侧FIFO的almost_empty/almost_full信号确认用户侧是不是消费不过来。丢包时序上还有一个小细节偶尔丢一两个包可能是CMAC接口的tkeep处理有误导致包长计算错误协议栈把帧当成错误帧丢弃。这种问题概率不大但一旦出现排查起来极其费劲需要仔细核对tkeep在每个周期的取值是否正确。5.4 100G UDP的几个流传误区“UDP丢包是因为UDP不可靠”——这句话在软件协议栈里是对的但在FPGA硬件协议栈里不一定。FPGA里UDP不收发丢包主要是FIFO溢出和物理层误码只要反压机制做得好UDP在硬件上也可以做到非常可靠。“100G必须要用RDMA”——RDMA是infiniBand和RoCE那套东西和UDP是不同的技术路线。很多场景不想用RDMA就是因为不想引入管理面和控制面的复杂度纯UDP简单直接反而更好维护。“开源UDP协议栈性能不如商用IP”——我实测下来的结论是开源方案在纯数据路径性能上并不输给商用IP区别主要在可管理性、诊断能力、多队列支持这些辅助功能上。如果你的应用只需要简单的UDP透明转发开源方案完全够用。“板卡逻辑时钟越快越好”——不是。322MHz的CMAC用户时钟是固定的用户侧逻辑如果跑不到这个频率可以用异步FIFO隔离用户侧跑自己的低频率时钟只要FIFO深度和读写速率匹配就行强行让用户逻辑也跑到322MHz反而得不偿失。6. 后续扩展方向与经验总结6.1 从UDP到更高价值的应用100G UDP跑通只是第一步真正值钱的是在这个基础上叠加业务。我目前正在做的是把UDP协议栈和自定义DMA引擎对接实现一个完整的100G网络数据加速卡。具体来说CPU只需要配置DMA描述符数据搬运全靠FPGA完成网卡带宽完全不吃CPU。这个架构非常适合金融行情加速、HPC数据交换、数据采集存储一体机这些场景UDP只是基础通道业务逻辑才是核心。另外一个可以扩展的方向是多端口汇聚。一块FPGA如果有4个100G端口可以在UDP协议栈之上做负载均衡、链路聚合、环网保护这些功能。开源项目里已经有了很多以太网交换相关的模块比如verilog-ethernet里就有交换机核心和去重模块可以在这个基础上做更复杂的多端口应用。6.2 踩坑几年后的一些实在建议最后再分享几点个人体会。开源代码拿下来先看仿真不要直接上板。我见过太多人就是不做仿真一上板遇到问题就抓瞎。仿真跑通了至少证明代码本身逻辑没问题剩下的只是集成问题。我用Vivado自带的xsim跑仿真写了一个简单的testbench模拟MAC侧和用户侧的数据收发大概跑了几万个包确认协议栈行为正确之后才上板节省了大量调试时间。上板之前把所有的初始化配置固化死尽量减少上板后的变量。比如本机MAC、本机IP、UDP端口映射表这些在上板之前全部写成参数或者初始化逻辑不要在调试时候靠软件去改这样出了问题原因更可控。多做自动化的数据校验不要只用iperf3的丢包率来判断。我在FPGA里做了一个简单的CRC校验计数器用户逻辑在收包时会对UDP载荷做累加校验然后和发送端的累加值对比一旦不一致立刻打标记。这样即使iperf3显示不丢包也能发现有没有数据被意外改坏的情况。100G UDP的移植上板说起来就是“接口适配加FIFO、时序收敛加验证”这十几个字但实际做起来每一个环节都需要仔细打磨。希望这篇记录能帮大家少填一些我踩过的坑尤其是那些正准备从千兆跳到100G的朋友。如果你也在做类似的方案欢迎多交流。