做FPGA高速网络这块,100G UDP一直是很多团队既向往又发怵的方向。向往的是跑满400Gbps甚至更高带宽时的成就感,发怵的是SerDes通道协商、MAC IP配置、跨时钟域处理、DDR带宽匹配这些环节随便一个出问题,都能让你在示波器和日志里泡上一整周。前段时间我把一套开源的100G UDP协议栈方案在实板上完整走了一遍,从IP配置到上板打流,从单板回环到双机对传,整个过程踩了不少坑,也沉淀了一些可以复用的经验。这篇文章就把整个上板测试的来龙去脉、关键参数、避坑点一次性讲清楚,给正在做或者准备做高速网络测量、数据中心互联、高性能计算卸载的FPGA工程师一个参考。
1. 方案选型与整体设计思路
1.1 为什么用FPGA做100G UDP协议栈
先聊一个很多人会问的问题:同样是收发UDP报文,用CPU加网卡不就行了,为什么非要FPGA?
CPU处理UDP报文的核心瓶颈在协议栈和中断开销。即使使用DPDK这类用户态协议栈,单核处理100G线速的小包依然非常吃力,尤其是64字节小包场景,每秒需要处理近1.48亿个报文,CPU的cache miss和锁竞争会直接把吞吐打到地板。而FPGA的优势在于数据通路天然是并行流水线结构,MAC层、IP层、UDP层、用户逻辑可以各自工作在同一个数据通路上,一拍数据进来,一拍数据出去,没有调度开销,也没有系统调用的延迟。
这里有一个最直观的对比:用x86软协议栈做100G线速UDP转发,通常需要8到16个物理核心做RSS负载均衡,而且延迟抖动在几十微秒级别;用FPGA做同样的事,只需要一个不到50万逻辑单元的中等规模芯片,延迟可以稳定控制在1微秒以内,抖动甚至能达到纳秒级。这正是网络测量、流量回放、硬件加速等场景偏爱FPGA的本质原因。
1.2 开源方案怎么选
目前圈子里的开源100G UDP方案主要有三类:一是基于Xilinx 10G/25G High Speed Ethernet IP(俗称CMAC)自行封装UDP逻辑,二是使用开源社区的P4-Nic、Corundum这类完整网卡框架,三是基于纯RTL从零写GT收发器加MAC。三者的取舍差异非常大,直接决定你的开发周期和可维护性。
Corundum是目前功能最完整的开源FPGA NIC方案,支持多队列、DMA、PCIe主机接口,但它的代码规模很大,依赖复杂的AXI4-Stream和DMA引擎,如果你只是想验证UDP数据通路,学习成本就偏高了。自己从GT层开始写则完全不推荐,100G SerDes的复位序列、时钟恢复、链路训练、FEC协商,每一个子模块都是数月级别的工程量,除非你想做学术研究,否则不要重复造轮子。
我最终选择的是第二类思路:官方CMAC IP做物理层和MAC层,UDP/IP收发逻辑自己用状态机加AXI4-Stream流水线实现。这样一来,GT收发器、PCS、MAC帧校验这些最麻烦的部分由IP核保证,我们只需要聚焦在IP层和UDP层,工程量和可控性都最好。
1.3 板卡资源和硬件环境
实测使用的板卡是Xilinx Virtex UltraScale+ VU7P,板载4路QSFP28光口,每路支持100G速率,参考时钟为156.25MHz。VU7P的逻辑资源对于这个项目来说非常充裕,实际工程跑完综合后LUT用量大约在12万左右,FF用量8万,BRAM用了大约200个,DSP基本没有多少消耗。
这套资源消耗对于绝大多数中高端FPGA都是可以接受的。如果你的板卡是KU15P、KU5P或者VU9P,资源上也不会有压力。真正需要重点确认的是板卡的GTH/GTY/GTM位置是否支持100G速率要求,以及参考时钟频率是否匹配。VU7P的GTY支持到25.78125Gbps的SerDes速率,4路GTY组合起来刚好是100G,所以链路层工作模式是4x25G NRZ,这是目前最主流的100G实现方式。
2. 工程搭建与核心逻辑实现
2.1 CMAC IP配置的几个关键参数
Vivado中搜索10G/25G High Speed Ethernet IP核,配置模式选择1.0,也就是4通道25G模式。这里有几个参数直接决定后续能否正常跑通,需要特别留意。
第一个是Datapath Width,CMAC在100G模式下固定为512位,这是由MAC层64字节最小帧决定的。512位意味着一个时钟周期内可以承载一个完整的最小以太网帧,简化了数据通路的处理逻辑。第二个是Clock Rate,配置为322.265625MHz,对应512位数据通路在100G线速下的时钟频率,计算公式是100e9/512。第三个是参考时钟,Xilinx官方要求GT参考时钟必须是156.25MHz,由板上可编程时钟芯片提供。
还有个容易忽略的选项是RS-FEC。100G SR4光模块和直连铜缆在长距离传输时需要开启RS-FEC(Reed-Solomon Forward Error Correction),它会在MAC层和PCS层之间插入前向纠错编码,消耗约2.4%的带宽,但能显著降低物理链路的误码率。如果你的测试环境是实验室内的短距离光纤直连,有时可以不开启FEC以获得完整的100G吞吐,但如果你是接交换机或者跨机柜传输,强烈建议打开。我实测中开启FEC后链路误码率从10的负8次方量级降到了完全清零。
2.2 数据通路架构
整个数据通路分为发送和接收两条独立的流水线。
发送方向,用户逻辑通过AXI4-Stream接口把待发送数据送入CMAC,数据宽度512位。CMAC会自动添加前导码、帧起始符、帧间隙并计算CRC32,用户只需要保证送入的数据是从有效帧的第一个字节开始即可。我的实现中在用户逻辑和CMAC之间加了一个FIFO做速率匹配,避免用户逻辑的突发写入和CMAC的均匀发送之间产生背压冲突。
接收方向,CMAC完成CRC校验、MAC地址过滤后,把去掉前导码和FCS的有效帧数据以512位AXI4-Stream送到用户逻辑。这里有个天然优势:CMAC已经帮你把错误帧丢掉了,用户逻辑只需要处理完整且校验正确的帧,协议栈实现时省掉了一个大坑。
UDP/IP层我自己写了一个精简协议栈,核心模块划分为三个状态机:接收解析状态机、发送组装状态机、ARP/ICMP回环处理状态机。接收解析模块逐字段提取以太网类型、IP头部、UDP头部,校验通过的载荷数据写入用户侧FIFO。发送组装模块则从用户侧FIFO读取数据,自动填充MAC地址、IP地址、UDP端口,并计算IP首部校验和和UDP校验和。
2.3 ARP和ICMP的取舍
在实现UDP协议栈时,很多人会忽略ARP和ICMP,结果上板后发现数据能发出去,但对面主机根本不回包。原因很简单:主机侧在通信之前需要先通过ARP解析FPGA的MAC地址,如果FPGA不回ARP请求,主机的ARP缓存表中就没有对应条目,UDP数据包根本不会发出。
我特意实现了ARP请求响应和ICMP Echo回环功能。ARP响应逻辑很简单,接收到广播的ARP请求后,判断目标IP是否是本FPGA的IP,是则构造ARP响应单播返回。ICMP回环则是把收到的Ping请求原样返回,这个功能对后续排查链路问题非常有用——只要主机能Ping通FPGA,就意味着MAC层、IP层、物理链路都是通的,UDP的问题就只可能在UDP层和用户逻辑。实测中这个设计省去了大量排查时间。
2.4 最小完备工程速览
整个工程在Vivado 2022.2下编译,从创建工程到生成bit文件大约需要40分钟。在逻辑设计上,顶层模块例化了CMAC IP、UDP协议栈、用户数据FIFO、ILA调试核以及一个简单的pattern generator和checker。
功能验证的逻辑非常简单粗暴——发送端生成递增计数数据包,每包长度可配置;接收端解析后检查数据是否连续递增,如果不一致则错误计数器加一,这个过程可以完全自动化验证UDP收发的正确性。PCIe、DDR、DMA这些外围功能我一个都没做,先把数据通路跑通,证明物理层和协议层没有问题,后续再往工程里加其他模块就不会有底层隐患了。
3. 上板测试的完整流程与操作细节
3.1 板卡管理和bit文件加载
上板第一步是给板卡上电,连接JTAG。我使用Vivado Hardware Manager加载bit文件,加载完成后确认GTY参考时钟的LOCK状态。这里有一个容易踩的小坑:上电后如果时钟芯片没有正确配置,GTY的参考时钟会丢失,导致CMAC的tx/rx status一直停在复位状态。
建议在加载bit之前先通过IIC或者SPI接口确认板上时钟芯片的输出频率是否稳定在156.25MHz,这个步骤虽然基础,但能帮你排除掉很大一部分链路问题是时钟导致的假象。
3.2 光模块与物理链路确认
板卡上电、bit加载完成后,插上光模块和光纤。我使用的是QSFP28 SR4光模块配合MPO光纤,短距离实验室环境完全可以满足。插好光纤后观察CMAC IP的status接口——rx_link_status信号拉高表示物理链路已经完成协商,此时光纤另一端的交换机或者另一块FPGA板卡应该能检测到链路up。
如果rx_link_status一直为低,优先检查光模块的los信号,这表示有没有光信号进来。如果los是低电平但有光,接着看gtpowergood和gtrefclklck信号是否正常。经过实测,最容易出问题的反而是光纤插反方向——MPO光纤有方向性,插反了A/B端光模块完全接收不到信号,排查起来如果不注意合约号会浪费不少时间。
3.3 主机侧UDP连通性测试
物理链路up之后,进入主机侧测试阶段。我的测试环境是一台安装Ubuntu 22.04的服务器,网卡为Mellanox ConnectX-5单口100G,与FPGA板卡通过光纤直连。
第一件事是配置主机网口IP和FPGA侧IP在同一网段,比如主机为192.168.1.10,FPGA为192.168.1.20,子网掩码255.255.255.0。然后ping一下FPGA地址,如果能ping通,说明ARP和ICMP都正常工作,协议栈的基础功能是好的。
这里有个细节值得注意:有的网卡驱动默认开启rx-flow-hash,会把收到的UDP包按五元组哈希分配到不同RSS队列,这本身不影响连通性,但在后续用Wireshark抓包时,你可能需要同时抓多个队列才能看到全部报文,建议测试时临时关闭RSS,让所有报文进入同一队列。
3.4 iperf3打流验证吞吐
连通性验证通过后,用iperf3做流量压力测试。iperf3默认使用TCP,UDP测试要加-u参数,带宽限制用-b参数指定,在100G链路上建议直接写-b 100G,带宽值可以设置成比实际链路速率高一些,确保网卡不会主动限速。
打流的过程中重点观察两个指标:接收端的吞吐和丢包率。iperf3 -u模式下服务端会统计接收到的包数和丢失的包数,能直观反映FPGA协议栈的处理能力。我实测配置了最小帧64字节时,单UDP流吞吐大约能到77Gbps左右,因为小包场景下帧间隙和前导码消耗了大量带宽;切到1518字节大帧时,吞吐接近线速99.4Gbps,丢包率完全为零。
这组数据说明100G UDP的关键瓶颈不在协议栈,而在包长分布。如果你的场景以小包为主,就需要考虑在数据通路里做多帧合并或者使用64B字对齐的DMA优化,这个后面单独聊。
3.5 用Wireshark和网络调试助手做包级验证
iperf3只验证了带宽,要确认FPGA发出的报文格式完全正确,需要抓包检查。在主机侧打开Wireshark,抓取对应网卡,对每个FPGA发出的UDP报文检查以下几项:源MAC是否为FPGA侧配置的MAC,目的MAC是否为主机网卡MAC,IP版本和TTL字段是否合理,UDP源端口、目的端口和包长字段是否和配置一致。
我之前就遇到过一个经典问题:UDP校验和字段全为零。是因为FPGA侧逻辑发送时将校验和字段置零,而主机网卡如果开启了UDP checksum offload,会对接收到的报文做校验,看到校验和为零会直接丢包。抓包时能看到包,但应用层就是收不到。解决办法是在FPGA侧正确计算UDP校验和,或者在主机侧关闭checksum offload。从此之后我再也没有为了省这点逻辑资源把校验和置零。
网络调试助手也很有用。我的方案实际上同时支持了固定包长的连续发包和交互式收发的双向模式,用于测试小包双端交互的场景。测试时,主机侧用网络调试助手向FPGA发送一帧自定义数据,FPGA协议栈解析后由用户逻辑原样返回,主机侧能立即收到回包,这就验证了双向数据通路都是通的。
4. 常见问题与排查技巧实录
4.1 接收方向丢包问题排查
上板测试中遇到最多的就是接收方向丢包。硬件环境没问题,告警计数器正常,但主机侧iperf3统计总有丢包。这种问题通常有三类原因,我按出现概率排序梳理一下。
第一类是CMAC接收侧背压导致的FIFO溢出。CMAC的接收接口和用户逻辑之间的FIFO深度不够,用户逻辑处理不过来时FIFO满,背压传递到CMAC导致丢包。这个问题在帧长较大、用户逻辑需要额外处理时间的场景下特别明显。我的做法是把接收FIFO深度调到最大,并在用户逻辑里避免在解析包头时产生过多等待周期。
第二类是PCIe或DMA接口的带宽不匹配,但本测试工程没有涉及DMA,所以这类问题在我这里不存在。如果是带DMA的设计,优先检查DMA的写带宽是否大于网络接收带宽,这是接收方向丢包的经典原因。
第三类是物理层的误码导致的偶发丢包。此时收到的帧会因为CRC错误被CMAC直接丢弃,主机侧表现为丢包率很低但持续存在。可以通过CMAC的rx_error_counter信号确认,如果计数器在持续递增,就需要检查光模块和光纤质量。
4.2 发送方向带宽上不去
发送方向的问题通常表现为配置了100G带宽,实际吞吐只有70到80G。第一步检查CMAC的tx_axis_ready信号是否频繁拉低,如果拉低说明用户逻辑的数据供给速率不足,肯定是FIFO或者上游逻辑没有跟上。
第二步检查数据帧的帧间隙配置。CMAC的帧间隙默认配置为96纳秒,这是标准以太网要求的最小值。如果上游数据以背靠背方式发送,CMAC会自动插入帧间隙,不会导致带宽损失。但如果是用户逻辑自己控制帧发送节奏,就得确认帧间隙是否过大。有些实现为了简单,每个包之间等待了几百个时钟周期,带宽自然上不去。
第三步也是最容易被新手忽略的:数据位宽拼接逻辑是否正确。512位数据通路在时钟频率322.265625MHz下每个周期都能发送一个数据,理论上可以做到线速。但如果你的用户逻辑只有256位数据位宽,则数据通路内部要做位宽转换,这个转换本身可能成为瓶颈。我当时就是用两个256位FIFO做乒乓拼接成512位,实测下来没有任何吞吐损失。
4.3 双机对传与单板回环的选择
上板测试有两种拓扑:第一种是FPGA板卡和主机网卡直连,第二种是两块FPGA板卡之间通过光纤直接连接。两个场景对应“验证协议栈是否标准”和“验证用户数据通路是否一致”两种不同目的。
如果是和主机网卡直连,建议把主机侧网卡的流控关闭,否则网卡在接收缓冲区满时会主动发pause帧,FPGA侧如果没实现流控,可能会造成MAC层的冲突。Xilinx CMAC默认支持pause帧处理,但要注意配置,避免死锁。
如果是双FPGA对连,有一个非常有用的调试技巧:在一个FPGA中例化二选一逻辑,即对端发送的数据不经过用户逻辑,直接在MAC层回环回发送端,然后再从对端抓包验证。这样做可以快速确认物理链路和MAC层的工作状态,排除掉用户逻辑的影响。
4.4 常见问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 链路协商失败,rx_link_status一直为低 | 光模块质量差或光纤反插 | 检查los信号,更换光纤和模块 |
| 主机Ping不通FPGA | ARP未响应或ICMP未实现 | 抓包确认是否收到ARP请求,检查IP配置 |
| 能收到包但应用层数据为空 | UDP校验和或目的端口错误 | Wireshark检查校验和字段,核对端口号 |
| 吞吐远低于线速 | 帧间隙过大或上游供给不足 | 检查tx_axis_ready时序,确认帧间隙配置 |
| 接收方向持续丢包 | FIFO溢出或校验失败 | 增大FIFO深度,检查rx_error_counter |
| 偶发CRC错误 | 物理链路误码或参考时钟不稳 | 检查光模块质量,确认GT参考时钟锁定 |
4.5 时钟域处理的一个经验
100G数据通路的典型时钟是322.265625MHz,而用户逻辑如果工作在用户自定义的时钟域,两边的跨时钟域处理会成为一个隐藏杀手。我采用的方法是统一使用CMAC的tx_clk和rx_clk作为发送和接收数据通路的唯一时钟域,用户逻辑内部再通过异步FIFO做时钟域转换。
这个做法的好处是整个网络协议栈都在同一个时钟域内,不存在亚稳态风险,也不需要在中间加CDC同步逻辑。缺点是用户逻辑如果原本工作在较低时钟频率,需要接受时钟频率上升到322MHz,对时序收敛压力略大,但VU7P这种芯片的时序余量足够,完全不用担心。
5. 性能优化方向与扩展思路
5.1 小包场景能压榨的极限在哪里
前面提到64字节小包只能跑到77Gbps左右,这是受以太网帧间隙和前导码开销限制的物理极限。64字节帧,实际线上传输占用84字节时隙,有效载荷占比76%,所以在100G线速下理论最大有效带宽是76Gbps,我的实测值77Gbps已经接近理论极限。
如果想进一步提升有效带宽,可以走的路径是减少帧数量,例如在应用层做多包聚合,把多个小UDP报文拼装到同一个以太网帧中,用自定义封装格式实现,这能让UDP有效载荷在单帧中占比大幅提升,代价是对端也需要对应的解封装逻辑。如果是FPGA对FPGA的私有链路,这条路完全可行。
5.2 从单流到多通道的演进
目前的FPGA协议栈默认工作在单通道,适合验证和原型开发。如果要支撑更高的吞吐或者多业务并发,可以通过实例化多个CMAC和协议栈实现多通道,每个通道独立处理一个光口的数据,再通过一个全局调度器做流量的分发和汇聚。
多通道设计中的核心难点在于全局流表的维护和拥塞控制。每个通道的ARP表、路由表需要统一管理,否则不同通道的流量会互相干扰。这里如果使用嵌入式CPU配合软件管理流表,开发效率会大幅提升,但不加锁的并发编程在FPGA里要格外小心。
5.3 卸载引擎和状态化处理的想象空间
现在很多云计算场景需要UDP协议栈支持有状态处理,例如NAT、负载均衡、灵活的五元组转发。FPGA的优势在这里能进一步发挥:通过TCAM或者哈希表实现流表查询,单周期完成匹配,效率远高于CPU侧查表。
我后续计划在现有协议栈上增加一个简单的流表匹配模块,使UDP报文能够根据五元组做规则转发。这样整个链路就从简单的收发,真正向一个有智能的硬件转发面演进。硬件卸载的能力上限,在这个方向上才有更大的发挥空间。
6. 写在最后的一些体会
这次100G FPGA UDP上板测试做到最后,最大的心得是硬件调试中最有价值的工具其实是最简单的工具。很多问题看似复杂,追根溯源都是基础链路上的问题——时钟锁定了没有,链路协商上了没有,CRC对不对,回环通不通。把这些基础动作做扎实,复杂问题就已经解决了一大半。
另外一个体会是开源项目在FPGA高速网络领域的价值正在快速提升。以前100G协议栈是少数大公司的私有资产,但如今CMAC、Corundum等一系列开源或者免费可用的IP和参考设计,让个人开发者也有机会触碰这个曾经高高在上的领域。如果你准备入坑,建议先从最小闭环做起,别一上来就追求完整功能,先把一块板卡的两个光口之间用最简逻辑跑通,后面做任何扩展都会有底气。