之前一直在10G的XGMAC上写UDP offload,今年项目要求吞吐直接上100G,我第一反应是:这有什么难的,找一个开源的100G UDP核,改改接口就上板。等真做完一整轮移植加测试,我必须承认这个想法过于乐观。100G以太网和10G以太网虽然都叫以太网,但从CMAC位宽、小包线速到DMA描述符深度,几乎没有一层能沿用原来的假设。这篇文章把整个移植和上板调试过程完整记录下来——怎么选型、改了哪些东西、测试怎么搭、以及三个让我改了多个版本的坑,希望对正在从10G/25G往100G迁移、或者刚开始接触FPGA 100G UDP的同学有帮助。
1. 100G UDP移植到底在移什么:从10G到100G不是改参数
1.1 名义差异之外的三个数字差异
很多人觉得100G只是把数据位宽变宽、时钟变高,其实背后的设计压力完全不同。看一组我在设计时反复算过的数字:
| 项目 | 10G以太网 | 25G以太网 | 100G以太网 |
|---|---|---|---|
| 常见MAC用户接口 | 64bit @ 156.25MHz | 64/128bit @ 312.5MHz | 512bit @ 322.265625MHz |
| 线速小包(64B)速率 | 14.88 Mpps | 37.2 Mpps | 148.8 Mpps |
| 常见光模块形态 | SFP+ | SFP28 | QSFP28 / QSFP-DD + RS-FEC |
| 典型PHY编码 | 64B/66B | 64B/66B | 64B/66B + RS-FEC(544,514) |
如果用户逻辑每拍处理一个以太网包,10G时代157MHz时钟处理14.88Mpps还比较轻松,25G时代312MHz处理37.2Mpps也勉强能跑。到100G,322MHz时钟对应148.8Mpps,每个包的时钟预算只有2.17拍;更要命的是,512bit宽的数据接口每一拍最多能装下8个64B小包。用户逻辑如果还维持“一拍处理一个完整包”的思路,虽然名义上算力有余,但描述符分配、校验和计算、包边界记录这些周边操作会迅速变成新的瓶颈。
1.2 小包线速的乘法效应
100G线速小包速率148.8Mpps这个数字,是整个移植过程中反复出现的“噩梦常数”。它不仅约束FPGA侧包处理逻辑,还一路传导到DMA描述符的提交速率、PCIe中断的触发频率、Host侧网卡驱动的事件吞吐能力。10G时候一个策略在14.88Mpps下没问题,到了100G完全不适用,因为每个子系统的压力都放大了十倍。
1.3 所以“移植”这个词应该拆成四件事
做完一轮之后,我把“把10G UDP移植到100G”这件事拆成了四个子任务:时钟与位宽适配、包处理节奏重调度、DMA描述符与中断深度重配、Host侧驱动参数重调。后面所有的排查和调优,基本都围绕这四件事展开。
2. 开源核怎么选:社区方案与自研的边界
2.1 我调研时的三个候选
开源生态里100G UDP相关的东西不算多,但也不是空白。当时我重点看了三个方向:
- XUP Vitis Networking:Xilinx官方的网络参考设计,包含CMAC + UDP offload + XDMA/QDMA的完整链路,FPGA侧代码和Host侧驱动都有示例,社区活跃度高。
- Verilog-Ethernet:Alex Forencich维护的经典以太网开源栈,模块划分清晰,10G/25G上很成熟,但100G需要自己拼装多个模块,没有现成的完整工程。
- 自研mini UDP core:只做CMAC原语集成 + UDP解析/组包 + 简单DMA搬移,工作量可控但需要从零解决一堆坑。
2.2 选型逻辑:能用现成的就别从零造
项目时间紧张,最终选择了XUP Vitis Networking的100G UDP参考设计作为基线。理由很直接:CMAC IP的集成、RS-FEC配置、XDMA中断链路这些基础设施都是现成的,省掉了我最不擅长的部分。而UDP业务相关模块,比如校验和计算、ARP表、ICMP回应逻辑,我用自己的实现替换掉原来的example代码。
选型时候最容易踩的坑是看README说支持100G就以为开箱即用。实际代码里可能默认example是针对10G/25G编译的,DMA也可能是AXI-Stream而不是Memory-Mapped,Host侧驱动没有对应的DPDK PMD,甚至license不允许商用。这些都要在动手移植前确认清楚。
2.3 三个候选的横向对比
| 维度 | XUP Vitis Networking | Verilog-Ethernet | 自研mini UDP |
|---|---|---|---|
| 原生100G支持 | CMAC + RS-FEC完整流程 | 需要自己组合 | 需要完全自建 |
| DMA子系统 | XDMA/QDMA,可配置 | 无,需外接 | 可裁可简 |
| Host侧软件生态 | DPDK/testpmd示例 | 无 | 需自研驱动 |
| 集成难度 | 中等,example多 | 高 | 高 |
| 文档活跃度 | 高 | 高(但100G资料少) | 完全看自己 |
三个方案里我最后选了第一个,核心原因是它把“能上板跑通”的概率拉到了最高,剩下要花时间的只是把业务逻辑改顺手。
3. 移植落地清单:位宽、时钟、DMA描述符一个都不能漏
3.1 用户侧AXI-Stream宽度与跨时钟FIFO
CMAC的用户接口是512bit @ 322.265625MHz,可以配置,这个频率和位宽组合是线速下带协议开销的必然结果。我们原来的用户逻辑深度绑定在128bit总线上,第一步就是把所有包处理模块改成512bit接口,并在每个时钟域边界加上异步FIFO。
跨时钟FIFO的深度不能拍脑袋。100G线速下,假设最坏情况每微秒有12.5KB数据涌入,异步FIFO至少得容纳半微秒的突发,也就是6.25KB。我用的FIFO深度是512bit x 1024,也就是64KB,在WRITE_LEVEL超过一半时拉高almost_full,给下游留出反应时间。这个设计后来在打流测试中几乎没有出现因FIFO深度不足导致的丢包。
3.2 CMAC初始化顺序:别一上电就等下不了link
CMAC初始化有个严格顺序,尤其是RS-FEC使能的100G链路。大致流程是:
- 释放GT复位,等待tx_reset_done和rx_reset_done拉高。
- 等待CMAC内部时钟稳定,读cmac_status寄存器确认tx_clk_stable和rx_clk_stable。
- RS-FEC使能时,等待rx_aligned、rx_rsfec_lock等状态位置位。
- 最后才把AXI-Stream接口的复位释放,开始收发数据。
我一开始图省事,把CMAC恢复位和用户逻辑复位一起释放,结果出现概率性的link up慢和RX alignment超时。后来改成严格分域复位,问题消失。上板调试时最值得做的第一件事就是把CMAC状态寄存器的每一个bit都映射到可读寄存器,link不up时直接读状态,而不是抓波形。
3.3 tkeep/tuser处理:512bit接口上SOP和EOP可以同拍出现多次
512bit接口碰到64B小包时,一拍里最多有8个完整包,这意味着一拍内可能有多个SOP和多个EOP。这是一个很多人移植时忽略的点。10G时代64bit接口一般一包跨两拍,SOP和EOP不会挤在同一拍;100G必须处理“一拍多包”的情况。
我写的包解析逻辑不再用一个单包状态机,而是分两步:先用组合逻辑根据tkeep扫描出当前拍内所有SOP/EOP边界,生成一个“包边界位图”;然后再根据位图并行地为每个包分配描述符或做校验和计算。这一步是后续能打满线速的基础。
3.4 UDP首部处理与校验和分工
UDP/IPv4校验和最好在FPGA侧硬件计算。IPv4首部校验和用16bit one‘s complement sum很简单,UDP校验和如果不需要可以关掉,但要注意Host侧驱动如果开启了RX校验和验证,需要确认两者分工不会造成双重计算。我建议FPGA侧计算并填好IPv4首部校验和,UDP校验和默认填0;如果对端也是自研逻辑,两边约定好即可。实测这样处理对性能影响最小。
3.5 DMA描述符与中断:10G的参数直接带过来一定出事
这是移植过程中改动最隐蔽也最影响结果的一环:
| 参数 | 10G习惯值 | 100G建议值 | 说明 |
|---|---|---|---|
| RX描述符环深度 | 256 | 2048或4096 | 小包线速下描述符消耗极快 |
| TX描述符环深度 | 256 | 1024 | 发送侧相对宽松 |
| 中断聚合 | 关闭或很少 | 建议使能 | 避免每个包都触发一次中断 |
| 描述符批量回收 | 无 | 按batch回收 | 对PDMA效率影响很大 |
如果Host侧用DPDK,中断聚合还涉及驱动里的NAPI budget和PMD轮询参数。总之,把这几个参数从“10G时代思维”里拿出来,是移植的核心工作之一。
4. 上板测试全流程:从链路建立到iperf3真实数据
4.1 测试环境拓扑
我用的是Alveo U250板卡 + QSFP28 DAC线缆直连一台Mellanox ConnectX-5 100G网卡,Host系统是Linux + DPDK,业务测试用iperf3打流。U250上CMAC走的是GTY Bank,DAC线缆比光模块省去插拔调试的麻烦,初期联调推荐这么干。
4.2 建链检查清单
上电后第一步不是跑iperf3,而是确认物理链路状态:
- CMAC link up状态是否为1。
- RS-FEC锁定状态是否为1。
- rx/tx error counter持续是否为零。
- 光模块或DAC的TX/RX功率是否在正常范围。
这几项用内部寄存器回读,全部OK才继续。链路都没up就查UDP逻辑,那是浪费时间。
4.3 Host侧准备与打流命令
Host侧使能DPDK绑定网卡:
# 配置hugepages echo 8192 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages # 绑定vfio-pci驱动 dpdk-devbind.py --bind=vfio-pci 3b:00.0然后分别做收发包测试:
# Host -> FPGA 方向 iperf3 -c 192.168.1.10 -u -b 100G -l 1472 -t 60 # FPGA -> Host 方向需要在FPGA逻辑里设置成发包模式,或用testpmd从侧发对于纯UDP业务,我更推荐用testpmd直接发流,便于控制包长和速率。
4.4 打流实测数据:修复前
第一轮打流结果很有意思,也是后续两个故障的引子:
| UDP包长 | 吞吐 | 线速比例 | 丢包情况 |
|---|---|---|---|
| 64B | 28 Gbps | 28% | 严重丢包 |
| 256B | 78 Gbps | 78% | 轻微丢包 |
| 1024B | 94 Gbps | 94% | 轻微丢包 |
| 1472B | 95 Gbps | 95% | 接近零丢包 |
大包表现不错,小包惨不忍睹。这个结果完全暴露了100G UDP小包处理瓶颈,后面故障实录二里会细讲。
4.5 抓包验证字段
打流同时用tcpdump或者wireshark抓一些样例包,重点确认源MAC、目的MAC、IPv4首部校验和、UDP长度字段是否都是预期值。抓包时有个容易误导的坑:很多版本网卡驱动默认开启RX校验和offload,wireshark抓包会显示checksum bad,实际是校验和卸载导致软件看不到正确checksum,别急着往FPGA头上扣锅。
5. 故障实录一:Link Up了却不通,先分清“ping不通”和“UDP不通”
5.1 现象:链路全部正常,但PC ping不通FPGA
板卡上电,CMAC link up、RS-FEC锁定、单板光口状态全部正常。FPGA侧ARP表里能看到PC的MAC地址,说明ARP学习功能在工作;但PC上ping FPGA的IP却不通。当时第一反应是UDP offload核有问题,准备抓链路层数据。
5.2 排错链路:把“通”的定义拆开
我先用testpmd从Host发广播包和UDP包到FPGA,结果FPGA内部计数器显示收包正常。这就说明MAC层和物理层没问题,问题出在协议响应上。
接着用ILA抓CMAC的RX和TX AXI-Stream,发现了根因:PC ping时发出的ARP request FPGA确实收到了,ARP学习也更新了,但这个开源核默认不回复ARP;同时更明显的是,ICMP echo request这个帧被UDP核直接丢弃了,因为核只处理IPv4/UDP,对ICMP报文没有响应逻辑。
换句话说,这个核的定位就是“UDP offload”,不是“完整TCP/IP协议栈”,ping不通是特性而不是bug。
5.3 修复:补一个精简ICMP echo模块
为了让工程符合大家的测试习惯,我写了一个精简的ARP responder + ICMP echo回显模块,挂在UDP核之前。ARP处理部分:收到目标IP为本机的ARP request后,查ARP表并回复reply;ICMP部分:Ethernet/IP头逐层校验,如果是ICMP echo request,就调换源目的MAC和IP后回echo reply,校验和用one‘s complement重算。
这个模块大概200多行Verilog,逻辑不复杂。加完之后ping通了,整个联调链路顺畅很多。
5.4 教训
排查“100G UDP不通”时,第一步永远是确认“不通”到底指什么:是ping不通,还是UDP业务数据不通,还是Link都不up。FPGA UDP核通常只承诺处理UDP业务包,没有义务响应ICMP。在高密度网络场景下,甚至有些硬件故意不响应ping,以减少CPU负担。这个坑看似简单,但真的会让很多人查一整天。
6. 故障实录二:64B小包只有28G,问题出在哪一层
6.1 现象:大包线速,小包崩盘
第一轮iperf3结果里大包能打到94Gbps以上,但64B小包只有28Gbps。这个差距太悬殊,肯定不是物理链路问题,因为大包能线速。
6.2 分层排查:先隔离MAC/PHY,再查用户逻辑
我第一步在FPGA内部做了个回环测试:把CMAC的RX数据直接送回CMAC TX,绕过UDP逻辑和DMA。用testpmd从Host发64B小包,测试发现回环下能把小包推到120Gbps以上,说明MAC和PHY完全没有问题,瓶颈在用户逻辑侧或者Host驱动侧。
然后我在用户逻辑各段插上性能计数器,看AXI-Stream的tready拉低比例。64B小包打流时,RX FIFO的tready只有30%左右的占比,也就是说有70%的时间下游没有能力接收新数据。瓶颈定位到消费端,而不是链路端。
6.3 根因一:描述符分配逻辑每个时钟只能处理一个包
打开描述符分配逻辑的代码,问题一目了然:原来的代码在每个时钟周期只允许处理一个包,分配描述符、写地址、更新指针都是串行状态机。当512bit接口一进来8个64B小包时,一个包占一拍,即使状态机每一拍都能处理一个包,吞吐也才到322Mpps上限;但实际上一拍能进8个包,4拍才处理3个,背压就起来了。
改动思路是把描述符分配逻辑从单包状态机改成“扫描一拍内所有包边界,并行分配多个描述符”的流水线结构。这里的关键是先用组合逻辑把tkeep转换成“包起始/结束位图”,再按位图批量处理,而不是一个个包轮流处理。
6.4 根因二:中断和描述符回收频率跟不上小包速率
修完描述符分配逻辑后,64B小包从28Gbps提升到70Gbps左右,但离线速还差一些。进一步排查发现,Host侧DPDK驱动轮询时,描述符回收的粒度太细,每个包都单独处理,导致消费者索引前进速度跟不上。
解决方案是在描述符回收端做成批量处理:每次回收处理一批已完成的描述符批次,而不是一个包一次收。同时使能中断聚合,让小包场景下每秒中断次数大幅降低。最终64B小包打到了约127Mpps,吞吐约80Gbps,对于这个工程来说已经比较理想。
6.5 修复前后对比
| UDP包长 | 修复前吞吐 | 修复后吞吐 | 修复后pps |
|---|---|---|---|
| 64B | 28 Gbps | 80 Gbps | 127 Mpps |
| 256B | 78 Gbps | 96 Gbps | 46.9 Mpps |
| 1024B | 94 Gbps | 96 Gbps | 11.7 Mpps |
| 1472B | 95 Gbps | 96 Gbps | 8.1 Mpps |
100G小包线速是148.8Mpps,127Mpps虽然没到线速,但系统瓶颈已经从FPGA用户逻辑转移到了PCIe/DMA/Host驱动组合,剩余差距更多是通用方案和专用ASIC之间的差距。
6.6 教训
测试小包一定要看pps。只看Gbps会被大包成绩迷惑。64B小包打流时,一定要考问三个环节:FPGA用户逻辑能不能每个时钟处理多个包、描述符分配能不能跟上、中断和描述符回收频率会不会淹没Host。
7. 故障实录三:DMA描述符环的回绕,Host端悄悄吞包
7.1 现象:FPGA显示包已发出,Host却收不到
从FPGA往Host方向的测试中,FPGA内部发送计数显示所有包都成功送入CMAC并发出,但Host端testpmd统计到的包数少了一大截。开始怀疑是Host驱动丢了包,但反复检查驱动配置没有发现问题。
7.2 排错链路:顺着描述符生产与消费整条链查
先看FPGA侧描述符写回次数,再看中断触发次数。发现描述符写回次数和Host收到的包数对不上,说明问题出在描述符本身。
检查描述符环配置时发现,Host侧DPDK报告ring size是4096,FPGA内部掩码也是4095,没有配置错误。再查描述符基地址,发现用户逻辑里把64位基地址截断成了低32位。这台机器上Host内存分配的高地址超过4GB,FPGA把描述符写到了低4GB的某个地址,和Host期待的高地址物理内存完全是两个地方,Host自然收不到写回信息。
7.3 修复:64位地址、64B对齐、门铃顺序缺一不可
修复包括三件事:
- 描述符基地址保持完整64位,不要在用户逻辑里做任何截断。
- 每个描述符条目按64B地址对齐,方便DMA引擎批量读写。
- 驱动里写tail寄存器(门铃)之前加一次内存屏障,确保描述符数据先落内存,再通知硬件去读。
特别是第三点,如果先写tail再写描述符内容,硬件可能读到半成品描述符,轻则丢包,重则地址错误。这个顺序问题在10G时代偶尔出现,100G高吞吐下会频繁暴露。
7.4 教训
DMA方向的问题,不能只看“发出去没有”这个表面现象。要把描述符生产、地址计算、写回通知、中断触发这4个环节当成一条链,逐个确认。FPGA和Host之间最隐蔽的坑往往出在地址位宽和同步顺序上。
8. 联调收尾阶段的几个经验补充
跑完这几轮测试,留一个个人体会:100G UDP移植成功的关键真的不在UDP协议本身,而在把整条数据链路当成一个流量系统来调。任何一个环节——时钟域、包处理节奏、描述符深度、中断频率、地址位宽——都会在某个包长和速率下成为瓶颈。
另一个长期有用的习惯是把FPGA内部各级计数器都做成可读寄存器。上板调试全凭计数器说话,ILA只在协议异常时抓关键波形用。计数器至少留:CMAC接收帧数/字节数、UDP核处理包数、描述符分配数、写回数、中断数、FIFO满次数包括与RX缓冲次数,哪个数字异常,问题就在哪个区间。
下一步我计划把UDP核往多个方向扩展:一个是多队列和RSS支持,让Host侧多核可以并行消费小包;另一个是把帧头解析逻辑做成可配置规则,为后续接入RDMA或自定义硬件协议预留基础。如果你也在做100G FPGA UDP,希望这份记录能帮你少踩几个坑。