ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

从10G到100G:FPGA UDP offload移植实战与调试记录

从10G到100G:FPGA UDP offload移植实战与调试记录 之前一直在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.25MHz64/128bit 312.5MHz512bit 322.265625MHz线速小包(64B)速率14.88 Mpps37.2 Mpps148.8 Mpps常见光模块形态SFPSFP28QSFP28 / QSFP-DD RS-FEC典型PHY编码64B/66B64B/66B64B/66B RS-FEC(544,514)如果用户逻辑每拍处理一个以太网包10G时代157MHz时钟处理14.88Mpps还比较轻松25G时代312MHz处理37.2Mpps也勉强能跑。到100G322MHz时钟对应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 NetworkingXilinx官方的网络参考设计包含CMAC UDP offload XDMA/QDMA的完整链路FPGA侧代码和Host侧驱动都有示例社区活跃度高。Verilog-EthernetAlex 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-MappedHost侧驱动没有对应的DPDK PMD甚至license不允许商用。这些都要在动手移植前确认清楚。2.3 三个候选的横向对比维度XUP Vitis NetworkingVerilog-Ethernet自研mini UDP原生100G支持CMAC RS-FEC完整流程需要自己组合需要完全自建DMA子系统XDMA/QDMA可配置无需外接可裁可简Host侧软件生态DPDK/testpmd示例无需自研驱动集成难度中等example多高高文档活跃度高高但100G资料少完全看自己三个方案里我最后选了第一个核心原因是它把“能上板跑通”的概率拉到了最高剩下要花时间的只是把业务逻辑改顺手。3. 移植落地清单位宽、时钟、DMA描述符一个都不能漏3.1 用户侧AXI-Stream宽度与跨时钟FIFOCMAC的用户接口是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初始化顺序别一上电就等下不了linkCMAC初始化有个严格顺序尤其是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描述符环深度2562048或4096小包线速下描述符消耗极快TX描述符环深度2561024发送侧相对宽松中断聚合关闭或很少建议使能避免每个包都触发一次中断描述符批量回收无按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 BankDAC线缆比光模块省去插拔调试的麻烦初期联调推荐这么干。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 --bindvfio-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包长吞吐线速比例丢包情况64B28 Gbps28%严重丢包256B78 Gbps78%轻微丢包1024B94 Gbps94%轻微丢包1472B95 Gbps95%接近零丢包大包表现不错小包惨不忍睹。这个结果完全暴露了100G UDP小包处理瓶颈后面故障实录二里会细讲。4.5 抓包验证字段打流同时用tcpdump或者wireshark抓一些样例包重点确认源MAC、目的MAC、IPv4首部校验和、UDP长度字段是否都是预期值。抓包时有个容易误导的坑很多版本网卡驱动默认开启RX校验和offloadwireshark抓包会显示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表并回复replyICMP部分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包长修复前吞吐修复后吞吐修复后pps64B28 Gbps80 Gbps127 Mpps256B78 Gbps96 Gbps46.9 Mpps1024B94 Gbps96 Gbps11.7 Mpps1472B95 Gbps96 Gbps8.1 Mpps100G小包线速是148.8Mpps127Mpps虽然没到线速但系统瓶颈已经从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是4096FPGA内部掩码也是4095没有配置错误。再查描述符基地址发现用户逻辑里把64位基地址截断成了低32位。这台机器上Host内存分配的高地址超过4GBFPGA把描述符写到了低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希望这份记录能帮你少踩几个坑。
RELATED READING

延伸阅读

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