做100G的UDP链路,说难也难,说容易也容易。前阵子我把一套开源的100G FPGA UDP协议栈移植到自研板卡上,从CMAC的link都点不亮开始折腾,到最后稳定跑满线速收发,前后大概花了两周。整个过程踩了不少坑:接口位宽对不上、小包边界处理错乱、UDP校验和回填顺序搞反、光模块回环插错口,每一处都够让人头疼半天。这篇就把整个移植和上板测试过程完整复盘一遍,重点讲工程里容易卡住的细节:CMAC接口怎么跟协议栈对齐、512bit数据通路怎么迁、UDP checksum的并行算法怎么做、上板之后该按什么顺序验证、出了问题又能从哪里查起。适合正在折腾100G UDP、想把开源协议栈搬到自研板上,或者从10G/25G往100G迁移的朋友参考。
1. 移植前的方案与接口规划
100G UDP的方案选型,其实比大多数人想象的要简单,但也藏着几个决定性因素。先明确一下,100G的线速意味着物理层部分不可能用FPGA纯软逻辑实现,传统10G里的软核MAC到100G根本跑不动。实际主流做法是用FPGA厂商提供的MAC硬核,Xilinx平台叫CMAC,它把PCS、PMA、RS-FEC这些物理编码子层全部集成好了,对外暴露一个AXI4-Stream接口。也就是说,UDP协议栈的上层逻辑面对的不再是MII/GMII这类传统MAC信号,而是tdata、tkeep、tvalid、tlast、tuser这样的标准AXIS总线。这反而让协议栈的移植很大程度上变成了“接口对齐工程”。
1.1 为什么偏要做100G UDP,而不是RDMA或RoCE
很多人一听到100G就下意识问,这速率为什么不用RDMA,RDMA不香吗?从功能上说RDMA确实香,但那是针对高性能计算、分布式存储这类需要极低延迟和CPU卸载的场景。而很多硬件加速项目,比如流量采集、实时协议解析、信号预处理数据回传,本质上只是需要一个高带宽、低延迟、转发逻辑可控的传输通道,UDP完全可以满足。
RDMA的问题在于它的生态太重。FPGA侧即使有开源RoCE实现,协议状态机、QoS、拥塞控制这些都很复杂;主机侧还依赖网卡驱动和特殊API,调试周期长。UDP只要能把以太网帧收进来、解析出头部、把payload按规则转走就够了,FPGA代码完全自控,主机只要用标准socket就能收发,开发速度不在一个量级。另一个现实原因是开源基础更好:网上能找到的100G UDP参考实现比RoCE多得多,改起来有参照,移植项目最怕连参照都没有。
1.2 开源UDP栈拿到手之后,第一件事不是开改,而是清点接口假设
这里我要强调一个最容易被忽略的步骤。开源代码不管是从GitHub上下下来的,还是从某个开发板配套资料里扒出来的,它一定是在某个特定硬件环境和特定CMAC配置下验证过的。移植的第一步是把这个协议栈对所有外部接口的假设全部列出来:
- AXIS数据位宽是多少,512bit还是256bit
- tkeep粒度是8字节还是4字节
- user clock频率是多少,是322.265625MHz还是402.8MHz
- 帧到AXIS接口时,前导码、CRC是什么状态,是CMAC已经去掉还是原样保留
- 错误标志tuser的信号定义
这些参数哪怕差一个,协议栈第一拍的解析就全错了。举个真实例子,我之前帮朋友调一个移植,开源码假设CMAC已经去掉了前导码,所以代码直接从以太网头开始解析;结果我们工程的CMAC配置里保留了前导码并开启了TX/RX CRC透传,出来的数据流最前面多了8字节,整个EtherType字段判断全部后移,帧全被当成异常帧丢掉。查到最后就是这8字节的差异,改CMAC配置比改协议栈代码省事得多。
另外,很多开源的FPGA UDP工程会把mac地址、IP地址、端口号这些参数做成了Verilog里的localparam。移植时记得全工程搜索这些固定参数,不要只改协议栈顶层的一个例化接口。有一次我漏改了一个固定端口,导致PC端一发UDP包,FPGA就回一个ICMP destination unreachable,抓包抓了半天才定位到是端口匹配表里还留着原板卡的旧值。
1.3 位宽与时钟规划:512bit搭配322MHz的舒适区
拿到开源代码后,我第一件事是把整个数据通路的位置和频率理清楚。100G线速下,要支撑100Gbps的MAC速率,如果数据位宽是512bit,user clock就是100Gbps / 512bit约等于195.3Mpps乘以1500字节之类的包长?实际上更准确的说法是,CMAC的用户侧接口通常可以选择512bit @ 322.265625MHz,这也是Xilinx最常用的组合。512bit乘以322.265625MHz再乘以8/10这类编码开销折算后,差不多正好对应100G物理线速。
为什么不选256bit然后把频率拉到644MHz?因为644MHz的时序收敛难度大得多,尤其在协议栈里涉及帧边界判断、过滤逻辑、FIFO位宽转换这些地方,组合逻辑稍微长一点就成负slack重灾区。322MHz对于现代UltraScale+器件来说是很舒服的工作频率,留出来的时序余量可以给后续的业务逻辑用。如果开源协议栈本来是基于256bit接口写的,你非要统一到512bit,那就是给自己挖坑,还不如沿用它的原始位宽配置,把CMAC的AXIS位宽配成一致即可。
时钟架构上,一般要分三个域:CMAC user clock、line clock(物理层恢复时钟)、用户业务时钟。大部分开源协议栈会把解析、封装、过滤都放在user clock域,这样省去跨时钟域FIFO的麻烦。如果应用侧业务时钟远低于user clock,比如只有50MHz,那么协议栈和业务逻辑之间必须插一个异步FIFO,而且这个FIFO的读写位宽很可能不一样,处理起来又是一层复杂度。能不做跨时钟域就尽量不做,这是我做这类项目的第一原则。
2. 工程落地与CMAC配置
方案确定了,接下来就是实际工程搭建。这一步不建议一上来就把完整的UDP业务逻辑接上去,而是先搭一个最小工程,把CMAC跑通,用ILA看到AXIS上有数据再往下走。我这次移植过程中的一个教训就是,上来就把全部代码例化上去,结果综合之后到处都是错误,排查了大半天也不知道是代码问题还是环境问题,最后老老实实拆成三个小阶段:先点CMAC,再接协议栈,最后接业务侧FIFO。
2.1 CMAC核配置:100G单通道还是4个25G通道
在Vivado里例化CMAC,最核心的选择是拓扑配置。100G光模块绝大多数是QSFP28封装,一根光纤跑100G,那CMAC就配成单通道100G,外部物理接口就是一组GTY四通道捆绑。还有一种常见配置是4通道25G,这种情况下QSFP28的四个通道分别跑25G,对外可以接4根25G光纤或者用分支光模块转成多路低速业务。两种配置在IP界面上完全不同:单通道100G得到一个统一的AXI-S接口,4通道25G则是四个接口,协议栈需要自己处理四路汇聚。
对这个项目,我直接选了单通道100GE,因为FPGA对端的测试设备是一台100G网卡,不需要考虑多路分支。要注意的是,CMAC的配置里有一项“RS-FEC”,100G模式下建议开启。RS-FEC可以提供很强的纠错能力,能容忍一定信号质量下产生的随机误码。如果没有FEC,有些光模块在较长距离传输时可能会出现偶发CRC错误,排查起来特别费劲。
2.2 引脚约束与时钟管理:参考时钟和QSFP28通道顺序
CMAC的物理接口高度依赖参考时钟和GTY引脚分配。参考时钟一般是156.25MHz差分时钟,必须进GTY的专用时钟输入,不能在普通IO上随便拉一个时钟去驱动高速收发器。我见过一个案例,板卡设计时把参考时钟接到了普通时钟引脚,结果CMAC虽然能初始化,但实际线路速率完全不对,误码率感人。
引脚约束上,QSFP28的收发差分对和原理图必须严格对应。踩过一个大坑:开发板的目标芯片集成在某个模块上,而开源工程的管脚约束是按另一块板卡写的,两边的GTY通道序号对不上。上板后链路始终起不来,后来用ILA抓CMAC的rx_tvalid信号,发现根本没数据,最后查原理图才发现把TX差分对接到了RX通道上,物理交换之后一切正常。如果你用的板卡和开源工程不是同一块,切记重新核查所有GTY位置约束,不要指望原有约束直接能跑。
参考时钟还涉及句法约束,比如参考时钟管脚要加上set_property的输入时钟约束、某些板卡还需要在XSDB里配置时钟芯片的寄存器。这点容易被忽略,因为很多开发板的时钟芯片上电默认频率就是156.25MHz,刚好够用。但自研板卡上如果时钟芯片默认不是这个频率,就得在初始化时通过I2C或SPI配置,这个步骤一般由FPGA上电逻辑或者外部MCU来完成。
2.3 最小系统先点亮,再做时序摸底
完整代码接上去之后做的第一件事是时序摸底。这里的经验是:如果综合实现之后出现大面积负slack,先不要急着硬调,用report_timing_summary看是哪条逻辑路径最长。常见元凶是帧解析状态机里的组合比较器,比如把一个64位的MAC地址比较写成一个超长的assign逻辑,在512bit数据通路的322MHz频率下,这条路径很容易成为关键路径。
我这次就遇到过一条负2ns的路径,定位之后发现是一个非常复杂的if-else嵌套,把判断拆成三级流水线之后勉强收敛了。需要注意的是,拆流水线会改变数据的延迟对齐关系,尤其是tuser错误标志、tlast这些信号必须跟着打同样级数的拍,否则帧边界和错误标志就会错位,测出来全是假错误。
还有个实用技巧:上板调试之前,先把CMAC配置成内部回环模式,比如选择PMA loopback或者line loopback,这样测试逻辑不需要依赖光模块。内部回环下用ILA直接抓AXIS接口,能快速确认协议栈的解析部分是否正确。等数字逻辑验证完毕,再切换到正常模式,接上光模块和光纤去验证物理链路。
3. 协议栈内部移植与代码改造
CMAC点通之后,真正花时间的还是协议栈的逻辑改造。前面说过,开源代码都是在某一套接口假设下写好的,移植的过程就是把它的AXIS接口、时钟、参数全部校准到你的工程上来。这一章我会把RX方向、TX方向、checksum计算、ARP/ICMP几个核心模块拆开讲,这些都直接影响上板能不能通、通得稳不稳。
3.1 RX方向:512bit下多个包边界同拍到达的处理
RX方向的核心功能是:接收CMAC的AXI-S数据,按以太网帧解析,做头部过滤,把payload交给应用侧。传统10G协议栈里,数据位宽64bit,一拍最多处理一个包起点或终点,逻辑好写。到了100G的512bit接口,情况就变了:如果你发一堆64字节小包,一个用户时钟周期内可能正好落下好几个完整的小包。我所在的流里,一拍之内可能既有上一包的tlast,又有下一包的tvalid和tstart。
这就意味着,协议栈里所有“本拍只处理一个帧边界”的假设都得推翻。正确做法是设计一个输入缓冲池,把当前周期里已经解析到一半的帧状态保存下来,同时要能预判下一帧的数据是否已经开始进入。开源代码里通常已经体现了这套逻辑,但移植时很容易因为理解不透改出bug。我的调试经验是先用64字节小包连续灌输入,观察filter模块和计数器的丢包统计,这个规模下问题暴露最明显。
过滤逻辑上,100G UDP场景不太需要复杂的CAM/流表,大多是固定规则匹配,比如目标MAC是否等于本机MAC、目标IP是否等于本机IP、UDP目标端口是否在允许列表里。每拍512bit数据过一遍这些比较器,组合逻辑压力不小,建议对匹配结果也做流水打拍处理,不要在同一个周期里既做匹配又做数据跳变。
3.2 TX方向:先存后发与UDP checksum回填
TX方向的设计比RX更绕,核心原因是UDP checksum依赖整个UDP报文的载荷。发送payload时,checksum字段必须等整个包运算完成才能知道结果,但UDP头在payload最前面。因此硬件上不能边收边发,必须等到整包读完算完checksum才能输出。开源代码里大多用一个双口RAM加一个“发送缓存”模块,把应用侧写入的payload先存下来,存的过程中同时做checksum累加,最后读出时在UDP头位置回填算好的校验值。
我移植时遇到的最大坑是“可发送”信号的逻辑。发送模块要等到一整个包的payload全部写入RAM后才允许开始读,这个done信号的产生时机如果提前一拍,后面读出的字节就可能读到未初始化的RAM内容,导致payload尾部出现随机垃圾数据。这个bug特别坑,因为大包测试不容易发现,小包和中等包长很容易触发。最后我的排查方式是PC端发500字节的固定pattern,FPGA回环后PC端用脚本比对,才抓到尾部多出的几个字节。
还有一个关于CRC的细节:CMAC默认会在TX方向自动插入以太网FCS(CRC32)。移植时不要自己再在协议栈里生成CRC,否则对端收到的帧尾部会有重复的CRC段,直接把帧判定为错误帧丢弃。反过来,如果你把CMAC配置成透传模式,那就必须自己生成CRC,两条路二选一,千万别双重插入。
3.3 UDP checksum与IP checksum的并行实现
checksum计算是100G UDP绕不开的技术点。IP校验和是16位反码和的机制,也就是把整个头部按16bit分组累加,溢出回卷再加,最后取反。100G下不能像软件那样逐步累加,必须在一个时钟周期内把进来的512bit数据一次性算掉。
我在实现上采用了两级流水设计。第一级,把512bit数据按16bit边界拆成32个16bit字,用一个加法树把所有部分和全部相加,得到这个周期的64bit和。第二级,把结果做16bit回卷,和上一周期保留的进位状态合并,等整包结束时再取反输出。这个设计的好处是加法树深度可控,两级流水后时序压力不大。如果为了省资源只做一级,加树的组合逻辑深度会大得多,322MHz下很可能跑不过时序。
这里有一个特别容易错的点:checksum覆盖的范围不只是payload,还包括UDP伪头部。伪头部里有源IP、目的IP、协议号、UDP长度,这些信息在解析IP头时才能得到。很多简化版的100G UDP参考实现为了图省事,直接把UDP checksum字段固定写成0x0000,IPv4协议里这是合法的,接收方可忽略校验。但严谨起见,我还是建议把checksum做完整,因为有些测试工具会直接标记checksum invalid,给排查带来不必要的干扰。
IP头checksum也同样处理,但因为IPv4标准头只有20字节,只需对少数几个16bit字做累加,逻辑规模很小,移植时注意别把它跟UDP checksum搞混,UDP的是另一个独立流水线。
3.4 ARP与ICMP:没有它们,UDP包根本到不了FPGA
很多人在移植UDP协议栈时忽略ARP,觉得这是个旁路功能。但实际调试中ARP极其关键。PC要往FPGA发UDP数据,操作系统要先查ARP表,如果不知道目标IP对应的MAC地址,数据包根本不会从网卡发出来。上板测试第一步基本就是配好IP之后,在PC端ping FPGA的IP。ping不通,后面UDP收发根本无从谈起。
ARP应答模块要处理的场景比较直接:收到目的IP为本机IP的ARP请求广播,就回复一个ARP reply,在reply中填入自己的MAC地址和收到的源MAC地址。这里要注意PC端在IP刚配置好时可能连发多个ARP请求,模块要支持连续较快地应答,不能出现应答一个之后需要长时间复位重新初始化的情况。我在自研工程里给ARP模块加了一个简单的状态机缓存,能在收到请求的下一拍就输出reply,实际测试下来PC端一般第一次ping就通了。
ICMP实现只需要支持ping的echo request和echo reply。很多协议栈里这算“锦上添花”的功能,但对调试链路作用很大。Ping通意味着MAC层、IP层基本没问题,再测UDP会更有把握。同时,我会在协议栈里加一组管理寄存器,记录收到的总帧数、UDP帧数、丢弃帧数及原因、发送包数、ARP请求数、ICMP请求数等,上板时通过AXI-Lite寄存器和串口打印出来。测试中一旦发现丢包或者不通,直接对着计数器和ILA波形分析,定位速度会快很多。
4. 上板测试全流程
上板测试是整个项目里最容易让人焦虑也是最容易出成就感的阶段。我强烈建议按下面的顺序逐级推进:先纯内部回环,再接光模块验证物理链路,然后PC端ARP/ping,再做UDP收发功能测试,最后才是满带宽压测。千万别上来就跑到满速率测试,一旦不通,你都不知道问题出在哪一层。
4.1 先做内部回环,再上光模块回环
上电之后先别接业务逻辑,把CMAC配回内部回环模式,比如PMA loopback,让发送数据在芯片内部直接回到接收端,然后从协议栈的RX统计看是否有正常的帧进来。这个模式下测试的是纯数字逻辑部分,不依赖光模块质量、光纤连接和参考时钟是否真的锁上,所以排查起来干扰最少。我一般会用一个顶层测试逻辑直接往TX侧灌标准的UDP报文,然后在RX侧用ILA抓数据,检查帧头、长度、payload是否完整。
确认内部回环通之后,再切换到正常模式,把光模块插上,用一根光纤跳线把QSFP28模块的TX接到同一个模块的RX。注意回环方式:有些模块支持内置电回环,插上直接用。如果只是普通光模块,那就要在外部用光纤把收发口连起来,或者直接用loopback模块。这一步如果link能起来,说明物理层没问题;如果link起不来,查光模块的I2C上电信息、参考时钟、reset时序,基本逃不出这三样。还要注意,部分板卡的QSFP28电源和reset是通过GPIO控制的,如果FPGA没有拉对电平,模块根本没上电,怎么检查都是白搭。
4.2 与PC建链:网卡驱动、静态ARP这些细节
内部回环和光回环都过了,才轮到真正的点对点通信。测试对端我用的是一台带100G网卡的服务器,用光纤连接到FPGA板卡的QSFP28口。PC网卡配好IP,比如192.168.10.1/24,FPGA侧协议栈固定IP设置为192.168.10.2/24,MAC地址是固定的,不用烧写EEPROM,直接写在参数里即可。
先ping。如果能ping通,说明ARP应答、ICMP应答这两个模块工作正常,MAC帧的收发链路是通的。如果ping不通,优先在PC端查ARP表。Windows和Linux下ARP表比较难刷,有时候网卡驱动会过滤掉来源MAC不在白名单里的ARP reply,导致始终学不到FPGA的MAC。Linux下可以用arp -d清空缓存重试,Windows下可以用arp -d配合管理员权限,再不行就手动arp -s 192.168.10.2 xx-xx-xx-xx-xx-xx强制写入静态表项。
这个阶段还有一个容易踩的坑:PC网卡如果开启了硬件校验和卸载(checksum offload),wireshark抓包看到的UDP checksum可能全是0x0000,看起来像FPGA端全错了,实际上只是网卡驱动做了offload,没把实际校验值填回去。测试的时候建议把网卡的tx/rx checksum offload全部关掉,或者至少知道这个机制,别被假象误导。
4.3 UDP功能测试:发几个包,看逻辑是否真正通了
ping通之后,用最简单的UDP收发命令验证协议栈。PC端可以用Python脚本,也可以用网络调试助手。FPGA端配置好接收端口和回环行为,最简单的设置是:PC向FPGA的指定端口发送payload,FPGA协议栈收到后原样再把payload回发给PC的源端口。这样一发一回,能验证整个RX解析、用户FIFO、TX封装链路是否完整。
我习惯在PC端发一串递增的字节序列,比如0x00到0xFF循环,FPGA侧回环后再比对数据一致性。这一步可以基本确认checksum计算正确、MAC/IP/UDP头部没有格式错误、收发状态机没有丢字节或多字节。如果回包内容完全一致,链路基本可用。
如果PC发UDP包后没有任何回包,先看FPGA侧收包计数器有没有增加。计数器没增加,大概率是ARP还停留在PC端的静态配置或者过滤模块把包丢了;计数器有增加但没回包,问题往往出在TX方向,这时候要重点检查发送状态机和发送FIFO的读使能逻辑。
4.4 满带宽压测:不只是看吞吐,更要看小包包率
功能通了之后,才是真正的压力测试。我这边用的打流工具是iperf3,服务器端执行iperf3 -s,FPGA侧如果只做回环,那测出来的是双向带宽。如果FPGA只接收并统计,那PC端执行:
iperf3 -u -c 192.168.10.2 -b 80G -P 8 -l 1400B -t 300
就差不多能灌到近80Gbps的UDP流量。实测下来,只要协议栈本身没有过度丢包,50-90Gbps的吞吐都能跑出来。但如果只看iperf的报告不够精确,因为iperf走的是它自己的高层协议,中间有计数逻辑,很难定位是FPGA丢包还是驱动丢包。我推荐在FPGA侧保留一个字节计数器,测试结束后对比PC发出的总字节数和FPGA收到的字节数,误差在几十KB以内属于正常,差很多就要查丢包点。
小包压测比大包更能暴露出协议栈的设计缺陷。64字节UDP数据包,对应一个1518字节以内的以太网小帧,线速下每秒接近1.5Mpps,而协议栈每拍最多处理几个包,如果解析状态机处理不了多帧同一拍到达的情况,丢包率会非常难看。我建议至少压128字节、256字节、1024字节、1500字节这几种包长。小包能稳住,大包一般问题不大。
如果测试环境只有40G网卡,也不是不行。FPGA可以把100G QSFP28拆成4个25G通道,或者降低CMAC的线速率到40G/50G来配合。但要注意,线速率一变,协议栈的AXIS接口位宽和user clock都要重新配置,最好在工程里预留参数,不要每次改都要改一堆硬代码。
4.5 长时间稳定性测试:烤机几小时,看错误计数增长
压测不能只跑几十秒,长时间稳定性更能发现潜在问题。我一般会让PC端持续向FPGA打流两到四个小时,期间每半小时记录一次FPGA侧的接收计数、CMAC的CRC错误计数、RX link状态和FEC corrected码字统计。关心的现象是:计数器是否持续线性增长、CMAC是否出现link drop、CRC错误计数器是否在悄悄增长。
如果CRC错误数定期跳增,多半是信号质量问题,要检查光模块型号、光纤损耗、QSFP28连接器是否松动。如果是FEC纠错码字很多但CRC几乎为零,说明信号质量堪忧但还在FEC能力范围内;如果CRC错误数和FEC纠错数一起涨,那基本可以断定是物理层不稳或者参考时钟跑偏,这类问题需要回看硬件设计而不是改协议栈软件代码。
5. 常见问题与避坑手册
挪到这一步,我把这次移植过程中遇到的最典型的几个问题整理成了一个速查表。每一个都是我或同事真实踩过的,照着排查能省很多时间。
5.1 常见故障速查表
| 现象 | 可能原因 | 排查与解决思路 |
|---|---|---|
| CMAC RX link始终down | 光模块未识别、参考时钟未工作、reset未完成、GPIO控制不对 | 先读光模块I2C,确认模块有响应;用ILA抓gt_dmonitor状态;核对晶振频率 |
| 小包正常,大包丢包或错包 | RX侧FIFO深度不足、TX缓存RAM读写冲突、发送缓存模块的done信号提前 | 扩大FIFO深度;确认TX侧“整包写完”信号时序;用固定pattern做逐字节比对 |
| ping不通,ARP表学不到 | ARP应答模块异常、MAC地址冲突、PC端静态ARP或白名单 | 在FPGA侧加ARP接收计数器;PC端手动arp -s写静态表项 |
| 能收不能发,或发了对端收不到 | TX状态机卡住、CRC双重插入、UDP长度字段错误 | ILA看m_axis_tx_tvalid是否拉高;关闭CMAC的CRC兼容模式;核对UDP length字段 |
| 对端抓包提示checksum错误 | checksum回卷逻辑缺位、交换了部分和顺序、伪头部未计算 | 检查16bit回卷代码;对照标准IPv4 UDP pseudo header字段 |
| 时序不收敛,负slack集中在解析逻辑 | 组合比较器过长、加法树过深、流水级数不一致 | report_timing_summary定位;加流水打拍;统一tvalid/tlast/tuser延迟 |
| 网络看似稳定但偶尔断流 | RS-FEC失步、光模块信号劣化、RX FIFO偶发溢出 | 开FEC;检查光功率;增加RX FIFO深度并加溢出统计 |
5.2 排查手段的三个原则
调试这类系统,我总结出三个原则。
第一,计数器先行。任何协议栈里都必须有足够的统计寄存器。收了多少帧、丢了多少帧、因为什么原因丢的,这些计数器在调试时是定位问题的第一手证据。没有计数器,你面对的可能只是一堆ILA波形,完全没有方向。
第二,ILA触发条件要具体。ILA并不是抓越多信号越好,触发条件很关键。比如怀疑丢包,就用tlast + 状态机的IDLE状态作为触发条件,抓状态机跳变附近的数据;怀疑首包错位,就用tvalid上升沿触发。触发条件设计得好,调试效率能翻好几倍。
第三,逐层回环。不要一上来就打外部PC。从内部回环测数字逻辑,到光模块外部回环测物理层,再到PC端真实对打,每一层都验证通过再往上叠。如果哪一层出问题,整个链条能被切得很短,排查范围立刻就缩到很小。
5.3 关于时序和代码风格的一点额外建议
100G UDP这种工程,数据通路全是512bit宽,时序收敛不能全靠工具硬推。代码风格上要刻意避免在关键路径上放组合逻辑长链。比如MAC地址比较、UDP端口匹配这类判断,尽量做成寄存器输出结果,用一个周期出匹配标志,数据在下一拍再使用这个结果。这样看起来多打了一拍,但对时序的改善非常明显。
另外,tkeep和tvalid的延迟一致性是最容易忽略又最容易出错的。在拆流水线时,一定要让tdata、tkeep、tvalid、tlast、tuser整套信号打同样的拍数。如果tvalid晚了一拍,tdata早了一拍,解析状态机就会在一拍之内看到“数据还没稳定但valid已经拉高”的乱象,表现出来就是偶发丢包、帧长错乱,很恶心。
5.4 移植遇到IP核版本不一致怎么办
最后说一个跟代码逻辑无关但很现实的问题。开源工程往往基于特定版本的Vivado和特定版本的CMAC IP核,而你本地装的环境很可能不一样。CMAC因为是个硬核IP,在IP界面配置参数、寄存器地址映射这些地方跨版本变化不小。我遇到过一个问题:开源工程里的CMAC寄存器初始化代码是用旧版本生成器写的,跟新版本IP的部分地址对不上,导致上电初始化少配了一个寄存器,链路就是起不来。
如果IP核版本对不齐,建议不要直接沿用scenario配置,而是重新例化一个新的CMAC核,沿用旧工程里已经验证过的配置选项,把新核的接口和旧代码重新连接。过程中特别留意两个信号:一个是gt_txp/rxn引脚约束,另一个是drp接口和s_axi控制接口的地址位宽。IT处理完这两块,基本能解决问题。如果实在发现行为有差异,用模板生成的example design先自测一遍IP核,确认链路本身没毛病,再接到协议栈上。
写在调试完成之后
整个项目做完,我最大的体会是100G UDP移植的难点不在UDP协议本身,而在于高速数据通路的时序理解、AXIS接口的对齐意识、以及多帧边界的处理。UDP协议栈的核心逻辑从10G到100G没有本质变化,变的只是每一拍能放下多少数据、边界落在哪里、关键路径能不能收敛。刚上手100G的朋友,千万不要被“100G”这个数字吓到,先把CMAC点亮,再谈协议栈移植,一步步来,其实整个链路并不复杂。等这版稳定之后,我打算再把更深的流控和TCP相关逻辑也加进去,到时再把新的问题和心得整理出来。