1. 项目概述
搞FPGA的人应该都有这种感觉:千兆网、万兆网跑得多了,突然看到有人把100G的UDP协议栈整套开源出来,第一反应是“真有人愿意干这种事”。一百兆的带宽对硬件设计来说完全是另一个世界——数据位宽、时钟频率、片内存储容量、对外接口协议全部要重新审视。而这篇文章要聊的就是这么一件事:把一个开源的100G UDP协议栈,从参考工程迁移到自己的目标板卡上,完成综合、实现、上板,并在真实链路下跑通收发测试的全过程。
可能有人会问:100G的UDP,为什么不用CPU、不用网卡,非要跑到FPGA上?核心需求就是两个词——极低延迟、极高通量。拿数据中心里的实时数据采集系统举例,数据从传感器或者光纤链路进来以后,希望在微秒级完成封装、路由到远端,或者反过来,从网络侧以线速把UDP包拆开、提取有效数据喂给后级DSP或者存储阵列。传统做法是CPU加专用网卡,但Linux协议栈的延迟抖动在几十微秒到几百微秒之间,分配缓冲区、中断处理、软中断调度都是有代价的。FPGA上做纯硬件协议栈,UDP包的解析和生成直接在逻辑门里完成,延迟基本可以做到亚微秒级别,吞吐量也能稳定等于物理层线速。
再说开源这件事。FPGA领域真正敢把100G级别UDP协议栈开源出来的项目并不多,因为这类代码的调试成本极高,尤其是在多通道高速收发器、MAC层状态机、包缓冲调度这些环节,没有实际板卡验证过的代码,放出来很容易把新人带进沟里。所以这次移植,我拿到源码以后的第一件事不是急着点亮LED,而是把代码结构、接口约束、时序要求一个个理清楚,再把参考工程的时钟方案和复位逻辑完整复现到自己板卡上。
这篇博客主要面向三类读者:一是准备在自家板卡上引用开源100G整机协议栈的FPGA工程师,二是做高速数据采集、网络测试仪表、金融极速交易、半导体测试机这类高性能低延迟应用的同学,三是打算从万兆往百G技术栈过渡、想整体了解硬件UDP协议栈怎么搭的人。文章会按照项目拆解、移植要点、上板流程、问题排查四个大块展开,最后补充一些我在实际操作中积累的小经验。
2. 整体设计思路与代码结构梳理
2.1 为什么在FPGA上做UDP要选全硬件协议栈
先聊一个基础问题:UDP协议栈在FPGA里到底该做什么事,做到哪一层?常见的实现方式有三种。第一种是纯软核方案,在FPGA里定制MicroBlaze或者RISC-V软核,跑精简版的LwIP,外部扩展一个百兆或千兆的MAC芯片,这种方案本质上是把嵌入式软件移植到FPGA内部,灵活但速度瓶颈明显,到万兆已经很吃力,100G根本不用想。第二种是部分卸载方案,FPGA逻辑实现MAC和DMA,协议解析仍然靠CPU,这种适合数据面和控制面分离的设备,但数据通路还是有中断和驱动介入。第三种就是这次移植的全硬件方案,从MAC层到IP层、UDP层的完整数据通路全部由逻辑实现,外部接口是标准AXI-Stream,丢给用户的已经是解析完的包头信息和有效载荷,反过来发送路径上用户把有效数据往接口一放,协议栈自动帮你包上UDP头、IP头、以太网头,再做校验和、FCS、按帧发出。
选用全硬件栈的核心原因在于性能和确定性。100G的物理层速率换算过来大概是10.3125Gbps每通道,四条通道并行;如果数据位宽是512 bit,用户逻辑时钟要跑到322MHz左右。这种情况下,任何软件介入都会成为瓶颈,而全硬件流水线按帧处理,一拍一拍往前走,没有调度抖动,时序可控,丢包率可测。
不过引入的问题也随之而来。硬件协议栈没有操作系统那种灵活的内存管理,所有缓冲区都是固定深度、固定地址对齐的,参数一旦定下来,跑起来就不能像软件那样动态分配。这就要求我在移植前把项目自身的数据流向彻底搞清楚:每个UDP包的最大长度是多少、每秒最大包速率是多少、突发流量有多大、回环路径需不需要同时支持收发。这些参数直接决定片上RAM资源的占用率和时钟频率指标。
2.2 开源工程的核心模块划分与数据流
拿到工程以后,我先画了一张大框图,把整个系统按数据面和控制面拆开。这里说一下典型开源100G UDP协议的模块划分方式,不同项目可能有差异,但大方向一致:
- 10G/25G/100G以太网MAC层:负责和PHY侧的高速收发器对接,完成以太网帧的接收、发送、FCS校验、Preamble处理、帧间隙管理。100G这一级在Xilinx平台对应CMAC硬核,或者用软核逻辑实现工作在322MHz的MAC。开源工程里如果针对Xilinx,大多直接调用CMAC原语;针对Intel平台则调用E-Tile硬核IP。
- 100G PHY侧适配层:包括与GTH/GTY/GTM等高速收发器对接的接口,处理位宽转换、时钟域转换、对齐标记插入。这个部分高度依赖具体芯片,移植时基本要重写。
- UDP/IP协议处理层:这是整个协议栈的逻辑核心。接收方向完成MAC头剥离、IP头解析、UDP头解析、校验和校验、按四元组查表判断是否交给用户逻辑;发送方向执行用户数据的包头添加、校验和计算、长度字段填充、分片处理。
- 包缓冲与调度模块:由于UDP是无连接的,数据包接收和发送通常是完全独立的两个数据流,需要通过packet FIFO做时钟域隔离和突发缓冲。这里的设计要点是缓存深度要和端到端链路延迟、DDR读写带宽匹配,否则突发流量一来就会丢包。
- AXI-Stream接口封装:协议栈对外的收发接口统一做成标准AXI4-Stream,方便对接用户自己的DMA、基因解析、数据搬运逻辑。
工程里最容易被低估的一块是“协议栈与用户逻辑之间的握手机制”。硬件做UDP解析,解析出的包头信息要能一次性干净地交给下游,例如udp_payload_valid、udp_dst_port、udp_length、ip_dst_addr这些信号必须在一个时钟周期内同时有效且稳定保持,直到整包数据结束。如果握手机制写得含含糊糊,上板以后会出现偶发丢包,而且极难定位。
2.3 移植前的需求确认与工程裁剪策略
移植一个100G级别工程,不能抱着“代码拿来直接编”的心态,第一步一定是需求裁剪。开源工程通常为了通用性会带上很多额外功能,比如ICMP回显、ARP缓存老化、多端口过滤表、巨型帧支持,甚至RTP扩展头。这些功能对资源占用和时序收敛都有影响,能砍就砍,砍完以后代码可读性和可维护性都上一个台阶。
我这次的实际场景是点对点数据传输:FPGA和测试端之间固定IP地址、固定UDP端口,数据内容为连续采样的ADC样本,包长固定1500字节。针对这个需求,我把ARP功能保留(对端协商需要),ICMP直接禁用,多端口过滤表改为单个端口直通,巨型帧关闭。实践证明,裁剪掉一个完整的ICMP处理状态机以后,整个工程的LUT占用少了大概7%,时序裕量也好了一些,尤其是跨时钟域处理那块,原本因为多余逻辑引入的布线拥塞明显缓解。
还有一个容易被忽略的点——时钟方案。开源工程如果原先是跑在VCU118或者别的开发板上,参考时钟频率、收发器参考时钟输入引脚、MAC用户时钟生成方式都跟特定板卡绑定。换板卡以后,必须仔细核对PHY芯片的种类和配置方式。100G常用的PHY有Finisar、Luxtera等厂商的光模块,直接通过光模块接口连到FPGA的高速收发器,中间不需要额外的PHY芯片,这种情况下只要保证GTH/GTY的参考时钟是156.25MHz就能工作。但有些板卡上带了Retimer芯片,那就要额外考虑Retimer的初始化配置,否则高速链路根本训练不起来。
3. 移植实操:从参考工程到目标板卡
3.1 拿到源码后的第一件事:跑仿真与顶层信号梳理
移植不能直接上板。我的习惯是先把源码在Vivado或者Quartus里建立仿真工程,用工程自带的testbench跑一个最基础的UDP收发仿真,确认代码在功能层面没有问题。虽然开源工程普遍自带self-check testbench,但不同工具版本对SystemVerilog语法的支持有差异,特别是带接口类(interface)的写法、带随机化的约束、断言写法,在Vivado里和Verilator里的兼容性可能完全不同。这一步的作用有两个:一是验证工具链环境,二是测一下TCL里控制仿真的脚本是否可用,这样后续改代码时能快速回归。
仿真跑通以后,打开工程的顶层RTL,把端口列表完整过一遍。100G UDP栈的顶层一般包含以下几类接口:
- 系统侧:用户时钟输入、复位输入、状态输出
- 数据侧:S_AXIS_0(发送上游)、M_AXIS_0(接收下游)
- 控制侧:ARP请求响应输出、UDP端口配置输入、统计计数输出
- 物理侧:gt_rxp_in/gt_txp_out高速收发器差分接口、gt_refclk_p/gt_refclk_n参考时钟、gt_rxresetdone、gt_txresetdone等状态信号
重点检查的是物理侧的接口。不同板卡的GT参考时钟引脚编号不同,高速收发器的所在Quad位置也可能不同,这直接影响XDC约束文件和GT例化参数。建议把原工程的XDC完全清空重写,不要照搬。厂商参考工程里的XDC经常带了很多过约束,比如把不需要的引脚都锁了,set_property一堆莫名其妙的对象,这些在换板卡以后到处都是坑。
3.2 时钟与复位方案的重新设计
100G工程的时钟与复位设计是整个移植成败的分水岭。先说时钟。100G以太网的用户逻辑时钟一般叫user_clk,它由MAC的TX/RX时钟导出,典型值是322.265625MHz(512位宽下100G线速对应)。但MAC内部还有两个独立的时钟域:rx_clk和tx_clk。在同步以太网模式下,这两个时钟频率相同、相位基本对齐;但为了适应异步模式,设计上必须允许它们来自不同的GT恢复时钟。开源工程里通常用独立的MMCM/PLL把GT时钟转成user_clk,关键是这三个时钟域之间的跨时钟域路径必须全部用异步FIFO隔离。
我在移植时踩过一个很典型的问题:原工程在参考板上是用MMCM生成user_clk,但因为目标板卡的GT参考时钟引脚恰好没有连接到MMCM可用的时钟输入通道,导致综合能过、实现时布线失败。解决方法是换用BUFR或者BUFG_GT原语直连GT的txoutclk,省掉MMCM这一步。但要注意,直连方案虽然减少了时钟延迟,却牺牲了频率修改的灵活性,所以必须根据自己板卡的时钟拓扑仔细选。
复位设计比时钟还容易出问题。100G的GT复位流程包含:PLL锁定、GT RX复位、GT TX复位、MAC复位、用户逻辑复位、数据通路FIFO复位,这些复位信号必须按照严格的顺序和时序关系释放。开源工程里一般会提供一个带超时状态机的复位模块,但它是参考具体PHY的规格写的。换板卡后建议确认几个关键参数:GT TX/RX reset的时间计数对不对、user_clk稳定以后到MAC复位释放的等待周期够不够、复位状态下数据通路是不是处于安全状态。如果复位顺序不对,现象就是链路能UP但收不到数据包,或者收到的包CRC全错。
提示:移植时不要把参考工程的复位逻辑原封不拉进目标工程,务必把GT resetdone信号、PLL locked信号、tx/rx user clock稳定信号全部引到ILA里观测一遍,确认时序符合预期后再释放MAC复位。
3.3 收发器配置与PHY层适配
这一节是整个移植最依赖具体芯片的环节。以Xilinx平台为例,100G以太网通常使用GTY或GTM收发器,配置在4×25.78125Gbps(CAUI-4模式)或者1×100G(需要PAM4调制,常见于GTM)。开源工程多数默认用CAUI-4四通道方案,因为这种模式对PCB布线要求相对宽松,兼容性也更好。
移植时要重点检查以下几个GT参数:
line_rate是否匹配板卡实际光模块能力,默认25.78125Grefclk_frequency是否为156.25MHz,部分板卡用161.1328125MHz,必须严格对应ql0_rx_alignment相关的32B对齐逻辑是否打开,没有对齐在CAUI-4模式根本跑不起来- 每个lane的极性控制,
txpolarity、rxpolarity是否和板卡丝印一致,接反了链路会一直训练失败
顶层例化GT时,最常见的问题是参考时钟引脚约束。GT的参考时钟引脚是固定的,例如Xilinx UltraScale+系列里,一个GT Quad对应两个参考时钟输入(REFCLK0、REFCLK1),板级原理图上必须把光模块的参考时钟连到正确的引脚上。换板卡后先查原理图,搞清楚光模块插槽对应的GT Quad位置和参考时钟通道,这个信息直接写进XDC,一点都不能错。
PHY层的另一个坑在于“回环测试”。很多开源工程在协议栈内部集成了近端回环和远端回环逻辑,方便在没有对端设备时做自测。上板之前,务必先做一次GT的近端PMA回环(loopback = 0b001),确认GT收发通路、参考时钟、复位、CDR都没有问题,再切换到正常模式跑UDP业务。如果近端回环能通而正常模式不通,问题基本出在对端PHY协商或线缆链路;如果近端回环都不通,问题在自己的GT配置或板上高速信号完整性。
3.4 内存与FIFO资源重规划
开源100G UDP工程对片内存储的消耗主要集中在三块:
- MAC接收方向的行缓冲和FIFO
- UDP处理层的包头缓存与多包缓存
- 用户接口侧的packet FIFO
这三块容量大小与原工程自己的数据粒度强相关。官方参考设计一般把packet FIFO做得很深,动辄几十KB,目的是应对最极端的背压场景。但目标板卡如果是小规模FPGA,比如Kintex UltraScale而不是Virtex UltraScale+,资源可能不够,这时候就要考虑把接收FIFO改浅,或者引入外部DDR做缓存。
把FIFO改浅有个前提:必须分析清楚背压的来源。假如用户逻辑处理一个包只需要不到1微秒,而网络侧一个包的最小到达间隔是几十微秒,那其实几百字节的FIFO就足够。但如果用户逻辑偶尔要停下来处理另一个中断,或者需要通过DDR搬运数据,那FIFO深度就需要按“最大阻塞时间 × 线速”来算。公式大概是:FIFO最小深度 = 最大阻塞周期(秒) × 12.5GB/s,然后向上取整到2的幂次,再留出至少1.5倍的裕量。
拿我自己的设计举例,用户逻辑的最大阻塞时间是18微秒,100G线速下对应的数据量是225KB,再加上流水线本身留的标记信息,我最后选了512KB的packet FIFO,用两个BRAM阵列交替缓存。如果用DDR做缓冲,设计复杂度会上一个台阶,因为要引入DDR控制器、地址管理、缓存行对齐逻辑,除非实在塞不下,否则尽量用片上RAM。
4. 上板测试:从回环到真实链路
4.1 上板前的检查清单与固件准备
上板不是把bit文件下载进去就完事。我在上板前会花半小时过一遍检查清单,这部分经验基本是用无数块砖换来的:
- 电源是否满足要求:100G工程瞬时功耗可能到30W以上,核心电压和GT的模拟电压必须按板上电源设计方案核对,不能只看综合报告的功耗估计。
- 参考时钟有没有实际输出:用示波器或者板上LED检查,保证GT参考时钟稳定,没有大幅抖动。
- JTAG链路是否正常:Vivado Hardware Manager能正确扫描到设备。
- 光模块的型号与速率设置:100G光模块通常是QSFP28,插入后先确认模块EEPROM能被读取,再确认模块速率匹配25.78Gbps每通道。
- bit流配置选项:把
bin文件烧到QSPI Flash,避免每次上电都依赖JTAG。
固件方面,我建议同时生成两类固件:一类是带ILA的调试版本,一类是完整业务版本。调试版本把GT reset状态、MAC的rx/tx状态、UDP解析状态、接收FIFO水位全部挂到ILA上,触发条件设成“链路失锁”或“UDP包连续满FIFO”,方便出问题时快速抓现场。
4.2 回环测试与链路训练验证
上电后第一步,先做无业务的链路自检。在协议栈内部通常有个寄存器可以配置回环模式,我建议按这个顺序测:
- 近端PMA回环,验证GT收发通路:把FPGA发送的PRBS伪随机码在GT的PMA层直接环回到接收端。
- 近端PCS回环,验证MAC+GT整体链路:数据从MAC进、经过GT发出去后立刻收回,验证MAC的TX/RX对齐、FCS检查、帧间隙。
- 外部光纤回环(远端回环):用一根光跳线把FPGA的TX直接连回RX,验证光模块、高速信号完整性。
回环测试重点关注rx_link_status和gt_rxresetdone信号,这两个必须稳定拉高。如果光模块回环一直训练不成功,先用一根短光纤做物理回环,排除对端设备问题;仍然不行就用高速示波器看眼图,重点检查是否有严重的ISI或者电压幅值不足。
光纤回环能通以后,可以在FPGA内部配置一个简单的自收发模块:周期性地往UDP发送端口写一个固定的测试包,同时把接收下来的包内容写入片上RAM,用ILA对比收发内容是否一致。这个自收自发虽然简单,但能快速隔离物理层、数据链路层、协议层的故障范围,比直接用万兆网卡找对端联调要直观得多。
4.3 与上位机或测试设备的联调
自收发测完,就该接入真实网络了。联调的第一步不是跑满100G,而是先用小流量验证协议栈的互通性。我常用的工具组合是Wireshark加上一个命令行发包工具(比如packeth或者网卡自带的发包器)。
操作流程大致是这样:
- 把FPGA的10G/100G光口接到测试网卡。如果只有万兆网卡,那先把FPGA端配置成10G模式(如果支持),否则借助100G交换机的扇出能力做速率适配。
- 给FPGA设置一个固定IP,例如192.168.1.10,端口固定是4000。
- 从上位机往FPGA发一个UDP包,payload长度设成64字节。Wireshark里看能否抓到FPGA发出的ARP请求,要是能看到ARP还在正常跑,说明MAC和PHY层至少是通的。
- 用expect或者脚本连续发送1000个包,FPGA接收逻辑每收到一个包就把包计数加1,上位机用读取FPGA内部寄存器的通道确认接收数目等于发送数目。
- 反方向也做一次,确认FPGA发送的包上位机能收到,ID号连续、payload正确。
这一步最容易暴露的问题是两个:一个是IP和MAC地址的在FPGA内部是写死的,需要改成和当前网络匹配的值;另一个是ARP缓存老化时间设置过短,导致实际数据流量稍一停顿,协议栈又重新发起ARP请求,而请求处理期间到达的UDP包全部被丢弃。
4.4 100G线速压力测试与性能指标评估
小流量通了不代表能跑线速。压力测试要覆盖三个维度:带宽、包速率、背压。
带宽测试使用iperf3是比较常规的做法,命令起来就是iperf3 -u -b 50G -l 1400 -t 60 -c 192.168.1.10,但要注意100G的UDP带宽测试中,CPU本身可能成为瓶颈。因为单个CPU核心跑到几十G的UDP收发已经很吃力了,我建议用多队列网卡配合RSS进行多线程打流,或者用专门的硬件发包仪。硬件发包仪虽然贵,但生成的流量特征更接近真实业务,包间隔均匀、源端口可随机、包长可配置,对FPGA协议栈的背压测试非常有用。
包速率方面,UDP的最小包通常按64字节以太网帧计算,100G速率下就是每秒148,809,523个包。硬件UDP协议栈必须按这个包速率做内部处理,尤其接收方向,每个包都要完成MAC头剥离、IP解析、UDP校验和校验、FIFO写入,流水线深度不够就回压到MAC,MAC跟着回压到GT的flow control,最后在物理链路上表现为丢弃帧。测试时用Wireshark统计每秒钟的包计数,和FPGA寄存器里的收包计数做对比,误差超过0.01%就可以怀疑丢包。
背压测试是最难但最有价值的。方法是让上位机以100G线速连续给FPGA灌UDP流,同时FPGA内部的用户逻辑故意停止从FIFO读数,持续50微秒后再恢复。对应的观察结果是:协议栈不能丢包、不能乱序、恢复后FIFO水位能迅速回到安全水平。这个测试能验证packet FIFO的深度设计和背压链路的状态机是否健壮。我经历过一次FIFO水位标称512KB,结果背压测试连续丢1000个包的情况,后来定位是恢复路径上“剩余空间更新逻辑”晚了一个周期,导致FIFO写使能已经拉高但空间计算值还没更新,白白丢了一整批数据。
5. 常见问题与排查技巧实录
100G FPGA UDP协议栈上板调试,问题五花八门,但归纳起来主要集中在这几个方向。
5.1 链路能UP但收不到UDP包
这个现象我见过最多。链路状态正常,rx_link_status拉高,MAC也报告没有FCS错误,但用户逻辑的收包计数器纹丝不动。排查顺序建议如下:
- 先确认MAC层有没有把帧交到协议栈:在MAC输出侧挂ILA,观察
mac_rx_tvalid和mac_rx_tlast是否按帧出现。 - 再确认UDP目的端口是否匹配:很多开源工程把目的端口过滤做在寄存器里,默认值是0,上位机发包的端口要是没写对,协议栈会把所有包都丢掉。
- 再确认IP地址过滤逻辑:PHY接通以后,可能收到的是别人的组播包、广播包,IP头解析后地址不匹配,协议栈直接丢。Wireshark抓下包看看对端发出来的IP、MAC、端口具体是什么。
- 检查checksum的开关:有些工程默认对接收UDP校验和做严格校验,如果上位机发的包校验和本身就不对(某些软件发包器不自动算校验和),协议栈会直接丢弃。
凡是这种“链路正常但不通业务”的问题,80%出在过滤配置和校验和这两个地方,先查这两块通常能节约半天时间。
5.2 回环正常但连交换机不通
自回环能通说明FPGA端没问题,问题大概率出在和对端设备的握手或者包格式上。最常见的原因是FPGA发出的包在以太网帧层面带了VLAN Tag,或者帧间隙不符合交换机要求,而回环测试时对端是自己发出的包,所以不会报错。交换机的日志一般会提示Rx CRC error或者runt frame,如果是CRC error,去检查MAC发送方向的FCS生成逻辑有没有打开,是不是把原包的FCS字节重复算了一遍。
另一个原因是Pause帧和flow control。有些开源工程默认开启了802.3x流控,FPGA会周期性发出Pause帧。交换机侧如果同时也开了流控,两边Pause帧交互出问题,业务流会被莫名其妙地暂停。直接把FPGA侧流控关掉,保留交换机侧的流控,让交换机主动管理拥塞,通常能解决。
5.3 跑大包没问题,跑小包疯狂丢包
这是典型的包速率瓶颈问题。UDP包越小,每秒要处理的包头和帧间隙就越多,协议栈状态机在每个包上的固定开销占比越高,一旦超过处理极限就开始丢包。排查时先用iperf3 -l 64这种小包模式复现,再逐级加长包长确认临界点。如果64字节包丢包但256字节包不丢,基本可以断定是协议栈包处理吞吐不够。
针对这个问题的优化方向有三个:一是把UDP解析流水线进一步打深,减少每个包的跳变状态数;二是对处理路径上不必要的逻辑做裁剪,特别是有分支的状态机,尽量改成每个周期都有确定结果的线性流水;三是把包缓冲的FIFO改成文档对齐的“分片缓冲”,而不是整包缓冲,这样在小包场景下FIFO利用率更高,等小包的间隙也利用起来了。
5.4 偶发丢包且无法稳定复现
偶发丢包最磨人。特点是测试十分钟可能只丢一个包,重跑又不丢,这种问题多半是跨时钟域处理或者异步FIFO的边界竞争。排查思路是:
- 先把所有跨时钟域路径重新过一遍,确认每个信号都用双触发器同步,多bit信号确保通过格雷码或者握手FIFO传递。
- 在ILA里挂上FIFO的写指针和读指针的变化,看丢包瞬间是不是FIFO刚好处于满的临界状态。
- 检查复位释放的余量,有些FPGA在上电早期信号不稳定,复位释放条件太临界,导致内部状态机偶尔误触发一个额外周期。
我一次真实排障花了三天,最后发现是MAC端的rx_axis_aresetn信号和用户逻辑端的复位信号其实来自同一个复位模块,但两边的复位释放逻辑各写了一遍,一个是在locked拉高的第4个周期释放,另一个是第8个周期释放,造成数据通路还没稳定复位就被拉高,下游逻辑残存了一个周期的错误数据,随机触发丢包。把复位模块统一改成一个,所有下游逻辑共用完全相同的复位释放时间,问题彻底消失。
5.5 工具使用与调试小技巧
最后分享几个实用的调试经验。
第一,ILA触发深度配置要舍得。100G工程的采样频率是322MHz,ILA采样1K深度只有3微秒左右的窗口,根本抓不到偶发问题。建议把ILA深度调到64K以上,触发条件用计数器判断,比如“发送包数达到10000且接收包数不等于10000”的时候触发,抓到的波形基本就是问题现场的完整过程。
第二,状态寄存器要设计到位。协议栈内部各模块的关键状态都要引出来写到寄存器里,比如rx_udp_valid_count、rx_fcs_err_count、rx_drop_count、arp_timeout_count、rx_fifo_watermark这些,跑完测试后上位机读一遍,可能比ILA看得更全。
第三,版本管理要及时打tag。每调通一个环节就存一版工程,命名规则建议带日期和功能标识,比如udp100g_20250111_rx_loopback_pass、udp100g_20250112_switch_link_up。100G工程综合一次十几分钟,实现一次几小时,版本管理混乱会导致反复回退、浪费时间。
第四,日志信息的自定义。FPGA里的UDP协议栈自己可以维护一个小型调试日志系统,用掉一部分BRAM做成环形缓冲,把“收到ARP请求”“发出ARP应答”“UDP包被丢弃原因”这些事件写进去,通过JTAG或UART读出。这个思路模仿了软件里的log系统,对排查那些偶发问题特别有用。
6. 后续演进与个人体会
这个移植项目做到后面已经不只是“把代码跑起来”这么简单了。对FPGA工程师来说,100G的UDP协议栈本身就是一面很好的镜子,它能检验你对高速收发器、以太网协议、跨时钟域设计、资源规划的综合掌握程度。跑通只是第一步,后续可以考虑的演进方向包括:对接PCIe的DMA引擎,把UDP数据直接搬到上位机内存;增加多端口匹配和负载均衡逻辑;引入RDMA或者RoCEv2的支持;把功耗和面积针对特定板卡再做一轮优化。这些方向每一个都足够写好几篇博客,但基础都是先把当前这套协议栈跑稳、跑透。
我个人在实际操作中最想强调的还是文档和版本管理。开源代码不代表可以拿来就抄,尤其是100G这种级别的大型逻辑工程,每块电路都有自己的设计假设,移植过程本质上是在“翻译”参考工程的设计意图到你自己的硬件上下文中。多画图、多边调试边写笔记,最后形成的移植文档,反而可能比你编译出来的bit文件还要值钱。