我们知道100G UDP听上去离普通开发者很远,但这两年随着开源社区把大量高速接口代码解放出来,它已经从一个“只有大厂网络组才碰得到”的方向,变成了很多FPGA工程师手上实实在在能跑通的东西。我手上这套开源100G UDP协议栈,就是从GitHub上扒下来、改完、移植到自家板子上,最后用打流工具压到满带宽的完整过程。这篇文章把整套移植逻辑、上板步骤、踩坑记录、调优思路全部摊开讲,给准备入坑或者正在调试100G UDP的朋友当一份实战地图。
这项目适合谁?如果你在做高速数据采集、网络加速卡、信号处理前端回传这类方向,手里恰好有一块带100G光口和PCIe的FPGA开发板,那这篇文章可以直接照着走。就算你现阶段还在用Vivado做千兆或者10G UDP,这套思路同样适用,只是时钟和总线宽度换了一档而已。
1. 内容整体设计与思路拆解
1.1 这个项目解决的核心问题
日常见的FPGA网络方案,90%以上是10G以下的小端口设计,CPU写个寄存器、FPGA把数据包好丢出去就完了。但到100G这个量级,事情立刻不一样:线速率是100Gbps,一个包哪怕64字节最小帧,一秒要处理的包数量接近1.5亿个,靠CPU中断去收包根本不现实,靠状态机一条一条处理也不现实,必须把整个收包、校验、提取、转发流程在硬件流水线里全部做掉。
这套开源UDP协议栈解决的就是这个问题。它内部已经实现了从MAC到IP再到UDP的解包组包链路,对外接口就是标准的AXI4-Stream,你只需要关心业务数据怎么填进去、怎么读出来,不需要碰底层的CRC校验、MAC地址过滤、VLAN解析这些脏活。这意味着你拿到代码以后,工作量从“重写一个网络栈”压缩到“迁移接口、适配时钟、做好跨时钟域”,对团队来说的研发周期可以从几个月降到一两周。
有人会问:既然Xilinx有XGMAIL/AXIGS那套官方IP,为什么还要用开源?我自己的体感是,官方IP适合标准用法,但你想做UDP硬件卸载、做多队列、做自定义包头,官方IP反而束缚很多,而且很多老版本IP在UltraScale+上兼容性是要额外花时间调的。开源代码最大的价值是可以按你的硬件平台改RTL,出问题可以直接看内部信号,不会被黑盒卡住。
1.2 移植方案的选型与取舍
拿到一个开源工程,第一件事不是打开代码看细节,而是先看它的接口定义和依赖环境。我用的这套工程是基于Xilinx UltraScale+平台设计的,底层MAC是调用Xilinx 100G Ethernet IP核,自己写的部分主要是UDP收发引擎、校验和计算、ARP处理、包分类和DMA桥接。
选型上我有三条硬约束:
第一,接口必须是AXI4-Stream,因为现在的DMA、PCIe、FIFO全都朝这个标准对齐,以后换平台不用重写业务逻辑。第二,时钟结构必须清晰,100G MAC出来是322MHz的512bit总线,业务侧往往是用户时钟域,这中间必须有异步FIFO做缓冲,而且水线要算准。第三,必须有回环和调试模式预留,不然后期上板出了问题只能拿逻辑分析仪瞎猜。
这三点听起来简单,实际过滤掉了一大半开源项目。很多所谓100G UDP工程就是纯仿真能过,一上板全是问题,要么复位不干净,要么跨时钟域漏数据,给你省下调试时间的项目才是好项目。
1.3 为什么选择上板实测作为核心验证手段
仿真能验证功能,但验证不了性能。UDP协议栈这种设计,天生对时序敏感:包间隔、FIFO水线、校验和更新时延、MAC反馈的preamble处理,任何一处在真实链路上时序稍微偏一点,脸上就是丢包率暴涨、带宽上不去、甚至接口锁不住光模块。仿真环境里的理想时钟和零延迟,在上板后根本不存在。
所以我把整个项目的验收标准定成三个上板指标:线速收包不丢包、线速发包不丢包、长时间压力测试无死锁。这三个指标每一个都和具体的硬件行为强相关,不是仿真能给的。只有插上真光纤、用打流仪或者对端服务器实际灌流量,才算验证完毕。这也是我坚持写这篇文章的原因——很多人仿真通过以后以为完事了,结果一上板就是一周的调试点,我把这些点提前告诉你。
2. 核心细节解析与实操要点
2.1 100G UDP协议栈的整体数据通路
从宏观上看,这套协议栈在FPGA内部的数据通路是这么走的:
收方向:光模块进来的串行数据先进100G MAC核,MAC完成对齐、解码、CRC校验、剥离前导码之后,输出AXI4-Stream的MAC帧(带CRC或者不带,看配置)。然后进入UDP接收引擎,这个引擎要完成的事情包括:MAC地址过滤、以太类型判断、IP版本判断、IP头校验和验证、UDP头解析、校验和验证。通过了以后的数据,连同一个精简后的包描述符(源IP、目的IP、源端口、目的端口、包长、通道号)一起写入接收FIFO,等待DMA搬走。
发方向是逆过程:业务侧把数据连同描述符写进发送FIFO,UDP发送引擎负责拼接以太头、IP头、UDP头,计算校验和并填充,然后发给MAC核加上FCS和前导码,变成线路上的帧。
这个流程看着简单,实际上有一堆细节:IP头校验和是一次性算完还是会变?UDP校验和的伪头部怎么处理?ARP包要不要交给CPU还是硬件直接回?多播包怎么处理?包长不满足最小以太帧长度的时候要不要填充?这些每一条都有对应的RTL逻辑,移植的时候一定要先摸清楚,否则你只改了个接口名字就上板,包格式不对根本没人告诉你错在哪。
2.2 三个移植时最容易出问题的点
点一:跨时钟域边界。100G MAC出来的用户时钟典型是322MHz,位宽512bit。但你DMA和用户逻辑很可能是不同的时钟,比如250MHz、300MHz都有可能。所有跨时钟域的FIFO必须用真双口异步FIFO,而且要注意读写两侧的Almost Full/Empty水线是不是按各自时钟域独立生成的。我遇到过一种情况:水线信号在写时钟域生成,但直接拿去读了,导致FIFO看起来永远不满,直接把数据丢了。
点二:复位结构。很多开源工程图省事,一个全局复位把所有逻辑全打掉。100G这种高吞吐设计里,复位同步非常重要:每个时钟域要有独立的异步复位同步释放逻辑,MAC核有自己的复位时序要求,DMA也有,UDP引擎还有内部状态机复位。乱用全局复位的结果是上电后偶尔工作正常、偶尔直接挂死,这种问题最难查。
点三:描述符和数据的对齐关系。UDP引擎输出的描述符比数据早几个周期到达FIFO是很正常的,但DMA搬运时如果默认描述符和数据同步到同一个FIFO条目,很可能出现描述符错位。我建议描述符和负载走两个FIFO,通过一个握手信号联动,或者把描述符扩展成旁路总线并伴随数据一起传递,这样最稳。
2.3 针对不同FPGA平台的适配建议
如果你用的是Xilinx UltraScale+或Versal,那直接按官方100G Ethernet IP的接口来适配就行,开源代码里基本都能找到模板。如果你用的是Intel Agilex或者Stratix 10,就要注意Intel的MAC核和Xilinx在接口信号命名和时序上有差异,尤其是复位使能和PCS状态信号,不能直接套。
如果是国产FPGA,比如复旦微、紫光同创、安路这几家的28nm或者更先进工艺器件,100G硬核可能没有,那要上100G只能靠Transceiver速率上去以后自己拼MAC逻辑,严重不建议业余时间搞,除非你是专门做这个的。更合理的路径是先用10G/25G把协议栈跑通,再考虑换平台。
2.4 资源占用与时序收敛参考
以Ultrascale+为例,纯UDP协议栈逻辑(不算MAC硬核和DMA)大约占用:LUT 12K到20K,FF 18K到25K,BRAM 220到400个36Kb块,这取决于你开了多少通道和多大的FIFO。相比整个100G设计动辄上百万LUT的规模,这点资源真的不算什么。
时序方面,510bit数据通路跑322MHz是比较常规的,难点通常在描述符状态机和DMA的地址生成逻辑上。我的建议是:先用默认约束跑一遍,哪条路径违例就先看那部分代码,别整体降频,通常问题集中在几个case语句和计数器上。
3. 实操过程与核心环节实现
3.1 环境清单与工具准备
我手里这套环境供参考:FPGA开发板是Xilinx VCU118(VU9P芯片),100G光模块用QSFP28接口,对端是一台带Mellanox ConnectX-5双口100G网卡的服务器,系统是Ubuntu 20.04。开发工具是Vivado 2020.2,仿真用自带的XSim,上板调试用ILA和Vivado Logic Analyzer。
软件侧准备:
- iperf3用于UDP打流测试
- Wireshark用于抓包分析
- 自己写的一个基于UDP的吞吐测试小工具,用来拼接特定长度报文并校验内容
硬件侧注意:100G光模块对光纤清洁度要求比10G高得多,插拔前不擦纤芯,分分钟出现误码率飙升甚至BER报警,这不是协议栈的锅,先排除物理层问题再查逻辑。
3.2 移植标准流程
第一步,克隆代码并梳理目录结构。先看README和顶层文件,理清模块层级,标注好哪些文件需要改动、哪些文件是参考实现。我习惯把设计里所有跟具体FPGA型号相关的代码集中摘出来,因为这是移植时改动最集中的地方。
第二步,替换底层MAC IP。打开Vivado工程,找到原工程里的100G Ethernet IP配置,根据你的板子实际的光口引脚分配重新生成IP。Xilinx的100G IP支持多种配置模式,这里必须选带PCS/PMA的完整模式,不走Bypass,否则你就要自己处理编码层,工作量直接翻倍。IP生成后注意对比原工程的时钟和复位信号命名,必须全部对上。
第三步,适配引脚约束。修改XDC文件里的GT位置、参考时钟引脚、复位按键、LED等。这一步最容易踩的坑是参考时钟:很多100G光模块要求156.25MHz参考时钟,但也有用161.1328125MHz的(对应不同线速率),务必和你的光模块手册核对清楚。
第四步,调整跨时钟域FIFO。这一步要动RTL。把原工程里跟具体时钟频率相关的常量、计数器宽度、水线配置全部改到你实际工程的频率下。水线的计算方法我后面单独讲。
第五步,写一个顶层测试模块。先把UDP协议栈做成一个可以自发自收的环回(Loopback)模式,即MAC层把收到的帧原样发回去,不发到上层。这一步是做上板前的冒烟测试,确保物理层和MAC层完全正常。
第六步,编译上板,用ILA观察关键信号。我不建议一开始就把整个系统全跑起来,而是分阶段点亮:先是GT和MAC能link,然后ARP能通,再是UDP收发,最后才是DMA桥接。
3.3 关键参数计算:异步FIFO深度与水线
这个参数网上的资料很少,但它是能不能跑满速的关键。以收方向为例:MAC输出数据速率为100Gbps,假设你的用户逻辑时钟是300MHz,位宽512bit,那么每个时钟能接受153.6Gbps的理论带宽,看起来足够,但突发流量来的时候,一瞬间可能连续来几百个包,如果下游DMA来不及搬,数据就得在FIFO里等。
FIFO深度的估算公式是:深度 >= 最大突发字节数 / 8。
什么是最大突发字节数?由你DMA的描述符批量大小(通常是一次搬16个或者32个描述符)乘以每个描述符对应的最大包长决定。比如每个batch搬32个包,最大包长是2048字节,那么突发就是64KB,FIFO至少要有64KB容量。如果你实际配置的是1MB,那绰绰有余,但资源也上去了。折中做法是让DMA每次搬描述符的数量可配置,配合FIFO水线动态调整。
水线设置的分寸在于:水线设得太高,FIFO几乎满了才开始发DMA请求,突发一来直接溢出丢包;水线设得太低,DMA被频繁唤醒,PCIe带宽碎片化严重,整体吞吐反而下降。我这边实测下来,Almost Full水线设在FIFO容量的75%到80%比较合适,Almost Empty设在10%到15%,然后根据实测微调。
3.4 实际移植代码修改要点
我挑一个最典型的代码片段来讲,UDP发送引擎的校验和计算部分。很多开源代码为了简化,将UDP校验和直接填0,这在很多场景下能用,但遇到严格要求校验和的接收端就会丢包。具体做法是:UDP伪头部包括源IP、目的IP、协议号、UDP长度,和一个UDP头的长度字段,全部取反累加,再和UDP数据部分进行补码和校验。
// 伪代码示例:UDP发送引擎中的校验和更新 // checksum_seg: 每周期输入64bit数据 // checksum_valid: 当前周期数据有效 always @(posedge clk) begin if (checksum_valid) begin checksum_acc <= checksum_acc + checksum_seg; end end // 最终校验和 = ~(补码和还原后的结果)实际工程中,计算出完整校验和需要两遍数据:第一遍算校验和,第二遍在发送头部时填进去。这意味着引擎内部必须缓存整个报文或者用双缓冲,不能边收边发。这也是UDP引擎比MAC层复杂的主要来源之一。移植时如果你发现原工程是边收边发,建议赶紧改掉,因为100G下这么干几乎必错。
3.5 上板实测记录
硬件接好线电后,第一件事是用ibert或者GT自检例程看过高速串行通道的眼图和误码率,我这里跑了30分钟误码率全程为0。然后加载移植后的UDP协议栈工程,先用环回模式测试:服务器端往FPGA发ARP请求,FPGA回了ARP应答,紧接着Ping通,证明MAC层收发正常。
然后打开UDP收发模式,用我自写的工具发送2048字节的UDP数据包,目的端口是5001。FPGA收到后在板上LED上做了一个计数器指示,实测每秒收到的包数量稳定在约600万包,折合线速约98.3Gbps。之所以没有刚好100G,是因为UDP包头、IP包头、以太帧间隙在高速率下本身就吃掉了一部分带宽,这个数值是正常的。
接着做极限测试:用iperf3以100Mpps级别的速率UDP打流,观察FPGA侧丢包率。实测在96Gbps负载以下零丢包,到97.5Gbps以上开始遇到瓶颈,4个小时测试过程中没有出现死锁或者状态机跑飞的情况,这个结果我认为达到项目验收标准了。
4. 常见问题与排查技巧实录
4.1 光模块link不上、MAC初始化失败
这是上板最常见的第一步问题。先看光模块的los信号是不是拉高,再用ibert扫描物理层。我遇到过两次链路不up的情况,第一次是光纤插反了,第二次是参考时钟配置错了,MAC IP里默认的是156.25MHz,但我那块模块需要161.13MHz。这个问题仿真绝对发现不了。
参考时钟的配置方式:如果是用板载可编程时钟芯片,先确认时钟芯片的配置文件是不是烧录正确;如果是外部直接给时钟,看原理图上GT参考时钟引脚是不是连到了正确的Bank。
4.2 能link但收不到数据包
如果物理层已经正常,但FPGA侧收到的MAC帧数为0,优先查MAC核的接收数据通路和user接口的复位。我曾被一个细节卡过:MAC核输出tvalid会先拉高输出一个preamble包,很多RTL在状态机设计时没有处理这个首包,直接把它当成正常UDP包交给上层,结果解析错误,整条接收链路卡死。正确的做法是在接收引擎里加一个空闲状态,跳过MAC输出的初始化包。
4.3 UDP校验和错误导致丢包
用Wireshark在服务器侧抓包,如果看到大量Checksum Offload错误,但FPGA侧确实发出去了,问题往往出在UDP发送引擎的伪头部拼接上。伪头部结构是:源IP(4B) + 目的IP(4B) + 零(1B) + 协议号(1B, 17) + UDP长度(2B),很多人把协议号放错位置,或者漏了零字节,这会导致所有包的校验和都不对。
反过来如果是FPGA接收丢包,但服务器发出来的包是好的,那就是接收引擎在算校验和时把数据宽度切分的周期数搞错了。UDP数据长度不是固定的,校验和计算模块必须能处理最后一个周期数据不足64bit的情况,缺了这一步,最后一小段数据没有被加进校验和累加器,计算出的校验和必然错误。
4.4 长时间运行后吞吐下降
这个现象我排查了很久,最后定位到是描述符池耗尽。DMA搬完包后,软件侧没有及时回收描述符,导致FPGA侧可用描述符越来越少,最终降到0,UDP再也收不进新包。解决靠的是软件驱动侧的中断频率和描述符回收逻辑的配合,很多现成开源驱动默认配置只适合小包场景,大吞吐时必须调大描述符池并开启聚合中断,否则中断风暴会把CPU打满。
4.5 问题排查速查表
| 现象 | 优先排查方向 | 参考工具 |
|---|---|---|
| 链路灯不亮 | 光模块、光纤、参考时钟 | iBERT/GT调试 |
| 能link但收不到包 | MAC初始化包跳过、复位 | ILA + Vivado |
| 接收丢包 | FIFO水线、DMA描述符 | ILA + 性能计数器 |
| 发送丢包 | 校验和计算、发包间隙 | Wireshark + iperf3 |
| 长时间后卡死 | 描述符池、状态机死锁 | 驱动日志 + ChipScope |
| 吞吐不达标 | 总线位宽、时钟频率、PCIe瓶颈 | Vivado Timing + 吞吐计数器 |
5. 100G UDP的性能调优与扩展方向
5.1 从跑通到跑满的三个优化点
代码移植上板能通,只是第一步,性能离满速可能还有很大距离。我调优沿三个阶段推:
第一阶段,核对关键路径。在Vivado里看时序报告,重点看UDP引擎的数据通路上有没有LUT级数超过10的组合逻辑,超标就把多周期路径和流水寄存器插进去。100G数据通路上每一级流水(pipeline stage)都是必然的,不要怕加寄存器,吞吐优先于延迟。
第二阶段,优化DMA交互。如果PCIe DMA是瓶颈,要设置合适的Max Read Request Size(MRRS)和Max Payload Size(MPS),一般设成512或1024字节,与PCIe带宽匹配。DMA描述符最好预取到FPGA侧缓存,避免每个包都去访问内存描述符表,否则小包场景会掉到带宽的三分之一。
第三阶段,减少不必要的逻辑。协议栈里的ARP处理、ICMP处理、多播过滤这些模块,如果你业务不需要,直接裁剪掉。每一个被裁剪的模块都会降低路径时延。开源代码里这些功能齐全固然好,但上板性能实测时,都是潜在瓶颈。
5.2 扩展方向一:RoCE或自定义RDMA
100G UDP本身是通用传输,但如果要支撑AI训练、分布式存储这类场景,需要更低时延和更小的CPU开销,方向就是RoCE或自定义RDMA。开源社区里已经有基于这个UDP协议栈扩展出RoCEv2雏形的项目,核心增加的点是:可靠连接管理、重传机制、拥塞控制、内存语义。做这部分的前提是先把UDP协议栈彻底吃透,否则没法继续。
5.3 扩展方向二:多队列与流分类
现在的设计通常是单收单发,实际生产环境里一个100G端口可能需要同时服务几十路业务流。这时就需要在UDP接收引擎后面加一个流分类器,按IP+端口把包分发到不同队列,每个队列配独立的DMA通道和中断,CPU侧多核各收各的队列,才能把多核能力用起来。这个方向也是网卡芯片的核心能力,开源项目能踏进去,对职业发展很有帮助。
5.4 扩展方向三:FPGA卸载计算
UDP协议栈只是载体,真正有价值的是在数据到达的同时做计算。比如在接收路径上直接做压缩、加密、数据过滤、AI推理的前处理,这样可以省掉数据搬运到CPU再搬回来的时延和带宽开销。很多智能网卡和DPU产品就是这么做的,开发这类产品需要的就是今天这套UDP硬件卸载的底子。
6. 从本项目中学到的核心经验
跑完这个项目,我最大的体会是:FPGA高速接口开发最耗时间的永远不是写RTL,而是把别人的代码和自己的平台对齐。开源代码的作者用的板子、工具版本、MAC核配置跟你不一样,每一个细微的差别都会放大成上板后的一个大坑。
我个人的工作习惯是:先把代码整个读一遍,画出状态机和数据通路图,标出所有和时钟、复位、位宽相关的常量,然后再动手改。直接编译完看报错来驱动修改,这个思路在低速设计里能用,但在100G这个量级会让你丧失对全局的掌控。
最后分享一个小技巧:无论你跑什么FPGA高速接口项目,都建议从一开始就在设计里加入计数器阵列和调试状态寄存器,把收发包计数、错误计数、FIFO水线实时值、异常状态机状态全部通过AXI-Lite寄存器暴露给CPU。上板调试时,先用软件把这些寄存器值全部拉出来看一遍,往往比挂ILA更快定位问题。这个习惯让我在后续几个项目里省了大量时间,强烈推荐你从今天这个项目开始养成。