这两年做 100G 网络方向的 FPGA 项目,最大的感触就是:真正难的不是 RTL 逻辑,而是把 GT、MAC、DMA、协议栈和主机驱动串成一条完整的链路。尤其是基于 ROCE V2 的 RDMA 通信系统,涉及的概念横跨高速串行接口、以太网协议、PCIe DMA 和应用层编程,任何一个环节出问题,定位起来都是按天算的。这篇文章把我最近在 Xilinx UltraScale+ 平台上搭建 100G RDMA 系统的完整过程做个复盘,重点讲 AXI Lite 配置链路的设计、GT 复位时序的坑、以及上板调试的排查思路,给同样在做高速接口或者正准备入坑 RDMA 的朋友一个参考。
先说明一个前提:这套系统不是从零开始造轮子,MAC 和 RS-FEC 用的是 Xilinx 100G Ethernet IP 核,ROCE V2 的传输层是自研 RTL 实现的,DMA 和 PCIe 部分用的是 XDMA IP 外加自研描述符管理逻辑。选择这种混合路线的原因后面会详细说,总之对于大部分团队来说,直接用现成 IP 打通数据面,把精力集中在协议定制和业务逻辑上,是性价比最高的路径。
1. 为什么用 FPGA 做 100G RDMA:高性能网络的真实瓶颈与取舍
1.1 软件协议栈在 100G 时代的吃力点
先聊一个很多人忽略的事实:100G 不仅仅是 10G 的十倍。以太网速度从 10G 升到 100G,数据面需要的处理能力是按数量级增长的,但 CPU 单核性能的增长早就放缓了。Linux 内核协议栈在 10G 时代还能勉强靠多队列和中断合并撑一撑,到了 100G 线速,一个 1500 字节的包间隔只有 67.2 纳秒,CPU 连做一次完整的中断处理都不够,更别说还要做校验、拷贝、协议解析。
RDMA 的核心思想是绕过内核,把数据直接从一个应用的内存搬到另一个应用的内存。传统 TCP 路径下,数据要经过网卡→内核 socket 缓冲区→用户态缓冲区,中间还涉及系统调用、上下文切换、内存拷贝,这些开销在 100G 时代完全是灾难。所以高性能计算领域基本都是靠 RDMA 这类技术来支撑,把数据面从 CPU 卸载到网卡或者 FPGA 上。
但问题来了:软件 RDMA 方案(比如 Soft-RoCE)虽然能跑,性能却远达不到硬件卸载的效果。它只是把协议栈搬到了用户态,仍然占用大量 CPU,在 100G 场景下实际吞吐很难超过 40G-50G,而且 CPU 占用率感人。想要真正跑满线速,还是得靠硬件卸载。
1.2 为什么选 FPGA 而不是成品网卡
市面上做 RDMA 的网卡方案已经很成熟了,Mellanox(现 NVIDIA)的 ConnectX 系列是事实标准,性能极强。但项目场景特殊时,成品网卡有几个硬伤:
- 黑盒协议栈:厂商不开放微码和内部状态机,想做一些定制化的传输语义很难。比如要做自定义的 ACK 策略、特定的重传机制,或者把 ROCE 和自有协议融合,基本没戏。
- 业务耦合不灵活:很多高性能系统要求数据在传输之前做实时处理,比如加解密、压缩、数据格式转换。这些逻辑放在 CPU 上做,带宽和延迟都受不了;放在网卡上,你改不了;放在 FPGA 上,直接内联到数据路径里,零拷贝完成处理,这才是 FPGA 的核心价值。
- 成本和大规模部署:按项目规模算,FPGA 的单价虽然不低,但如果板卡本身还有其他用途(前处理、控制面加速),综合成本反而更优。
我这次的选择是Xilinx VU9P 级别的 UltraScale+ FPGA,用 100G Ethernet MAC 硬核 IP,配合自研 ROCE V2 传输层模块。这套方案的好处是数据面完全可编程,协议细节自己可控,坏处是工程复杂度高,尤其 GT、MAC、DMA 之间的协同配置非常繁琐。这篇博客主要就是把这些繁琐点讲清楚。
1.3 ROCE V2 为什么成为 FPGA 领域的事实选择
RDMA 有三大主流实现:InfiniBand、RoCE v1、RoCE v2。InfiniBand 的协议栈不开放,FPGA 只能纯自研而且量级很大;RoCE v1 跑在二层,不支持路由;RoCE v2 把报文封装进了 UDP/IP 里,可以跨三层路由,而且协议细节相对简单,MAC 层 IP 核自带的 CRC 校验能直接复用部分逻辑,这对 FPGA 来说非常友好。
具体来说,ROCE v2 的报文格式是:以太网头 + IP 头 + UDP 头(目的端口 4791)+ BTH(Base Transport Header)+ 数据。BTH 里包含了 OpCode、目的 QP 号、PSN(包序列号)等信息。相比 InfiniBand 复杂的链路层管理,ROCE v2 在 FPGA 上的实现路径清晰很多:MAC 收包后剥掉以太网头,解析 IP/UDP 头定位到 QP 上下文,然后根据 BTH 的 OpCode 决定是 READ、WRITE 还是 SEND,再做对应的 DMA 搬运。
很多团队纠结要不要从 L2 层完全自研 ROCE,我的建议是:第一次做的话,MAC 层一定用 Xilinx IP,不要手写 PCS/PMA。100G 的 RS-FEC、时钟恢复、位同步这些模拟和高速数字混合的东西,IP 核帮你扛掉了,你只需要关心复位时序和配置寄存器,这已经省了 80% 的调试工作量。
2. 系统架构拆解:数据搬运、协议处理与主机交互的分工
2.1 主数据面的核心路径
整个系统按数据流可以分为三块:线卡侧(line side)、协议处理侧、主机侧。
线卡侧就是 QSFP28 光口进来的 4 路 25G(100GBASE-R 标准就是把 100G 拆成 4 个 25G 通道),先进 FPGA 的 GTY 高速收发器,然后进入 100G Ethernet IP 核。这个 IP 核内部完成了 PCS 层对齐、RS-FEC 解码(如果使能)、MAC 层组帧/解帧、以及 CRC 校验。IP 核对外输出的是标准的 AXI4-Stream 接口,用户逻辑只需要处理从 MAC 出来的以太网帧数据。
协议处理侧是自研的 ROCE V2 传输层模块。它做的事情包括:解析 IP/UDP 头,匹配 QP 上下文表,识别数据包类型(RDMA WRITE、READ、SEND、ACK、CNP 等),维护 PSN 和重传队列,对写请求做数据搬运,对读请求做响应数据组装。
主机侧走 PCIe Gen3 x16,用 XDMA IP 做 DMA 引擎。RDMA 引擎从线上收到的数据并不会直接放到 FPGA 的 BRAM 里,而是通过 DMA 描述符直接把数据写入主机内存的 WQ(Work Queue)对应的缓冲区里,整个过程不经过 CPU。
这三块之间的交互是通过 AXI4-Stream 和 AXI4 总线完成的。一个经常被低估的设计点:DMA 描述符管理模块要独立于数据搬运模块。我第一版图省事,把描述符处理和搬运逻辑写在了一起,结果调试 PSN 重传的时候发现描述符更新逻辑和数据重放逻辑互相牵扯,状态机复杂到几乎没法维护。重构成两块独立的逻辑后,每块的状态机都降了一个复杂度量级。
2.2 DMA 子系统的设计细节
DMA 是连接协议处理和主机内存的桥梁。我这边 XDMA 是配置成独立通道模式,两个 AXI4 Master 接口分别做读写。对于 100G 线速来说,单通道 AXI4 接口的带宽很可能成为瓶颈,建议直接上多通道的 AXI4 设计,或者用 AXI4-Stream 桥接方案。
这里面还有一个重要的参数:MPS(Max Payload Size)。PCIe 的 MPS 决定了 DMA 一次能够搬运的数据量上限,Gen3 时代常见的是 256B 和 512B。100G 线速 + 1500B 的以太网 frame,意味着一个 frame 需要拆成 6 个 256B 的 TLP 传输,如果 MPS 配成 512B 则需要 3 个 TLP。MPS 大,TLP 头开销占比小,总带宽利用率高,但缓冲区粒度变粗,容易出现边界对齐问题。实测下来,MPS 配 512B 并做好 64B 对齐,对性能提升比较明显。
然后是描述符环的设计。每个描述符指向主机内存中的一段缓冲区,DMA 完成一个描述符后,通过门铃(Doorbell)寄存器通知主机驱动。这里有个容易犯的错误:门铃写回的频率。如果每收一个小包就写一次门铃,PCIe 的写带宽消耗会非常高。正确做法是累积到一定数量(比如 32 个包)或者超时(比如 10 微秒)再批量写一次门铃,用中断聚合换取带宽。
2.3 AXI Lite 在系统里的定位
AXI Lite 在整条链路上看起来是最不起眼的,但它是唯一一个把主机软件和 FPGA 硬件逻辑连接起来的控制面通道。它要负责的事包括:
- 读取链路状态(link up/down、PLL locked、FEC 错误计数)
- 配置 MAC 和 PHY(使能 RS-FEC、设置 loopback 模式)
- 配置 ROCE 模块(QP 上下文、PSN 初始值、重传超时参数)
- 配置 DMA 描述符基地址和队列深度
- 触发软复位和统计清零
之所以选 AXI Lite 而不是 AXI4 Full,是因为这些控制操作带宽需求极低(每个寄存器 32bit,偶尔读写一次),完全没必要用支持 burst 的 AXI4 Full。而且 AXI Lite 的协议简单,不需要处理 INCR/WRAP 等 burst 类型,WSTRB 也只是整字粒度的位选,状态机写起来轻松很多,出 bug 的概率也低。
3. AXI Lite 配置链路:寄存器到硬件使能的完整闭环
3.1 配置面选型时的几个考虑
很多人不理解为什么控制面不直接挂在 PCIe 的 BAR 空间上让 CPU 直接读写,而是要通过 XDMA 的 AXI Lite 通道中转。原因有两个:
第一,RDMA 场景下,控制面和数据面最好分离。如果把所有寄存器都映射到 BAR,驱动里随便一个误操作就可能踩到数据面 DMA 的描述符或者门铃地址,调试的时候根本分不清是软件写错了还是硬件逻辑的问题。通过独立的 AXI Lite 通道把控制寄存器隔离出来,至少能把心智负担降低一大截。
第二,XDMA 本身提供了完整的 AXI Lite Register Master 接口。PCIe BAR 空间经过 XDMA 解码后,可以映射到若干个 AXI Lite 从机接口,每个从机可以对应一个子模块的寄存器组。这种层次化映射天然适合多模块系统的软件管理:主机驱动只需要知道每个模块的 BAR 偏移,就能完成所有配置,不需要关心 RTL 内部的总线拓扑。
我的设计里,AXI Lite 总线上挂了四个从机:MAC 管理、ROCE 引擎、DMA 控制、以及一个全局状态寄存器组。每个从机的地址空间是 64KB(16 位偏移),这样算下来 AXI Lite 的地址位宽只需要 16 位加上模块选择位。如果模块数量多到超过 64KB,也可以把每块做成 4KB 对齐的 page,但 Xilinx IP 的 AXI Lite 接口基本都是 32bit 寄存器对齐,实际不需要那么细的粒度。
3.2 寄存器图规划的正确姿势
寄存器图是整个系统软硬件协作的契约,规划好了能省掉后面 80% 的联调痛苦。我踩过的坑是:第一版寄存器图直接在 RTL 里写死偏移,软件那边用魔数访问,结果版本迭代几次后,RTL 改了一个偏移,软件忘了同步,调试了整整两天才发现是寄存器地址对不上。
后来我用了这个办法:在 RTL 代码里用宏定义和头文件统一管理寄存器偏移,软件端直接 include 同一份头文件(用 verilog 的 include 或者生成的 h 文件)。这样硬件软件共享同一个偏移定义,再也不会出现地址不一致的问题。对于每个寄存器,我都要明确:偏移地址、读写属性(RW/RO/W1C)、复位值、位域含义。
以 ROCE 引擎的 CTRL 寄存器为例,它的布局大概是这样的:
- bit[0]:软复位,写 1 触发引擎复位,硬件完成自动清零
- bit[1]:MAC 使能,置 1 后 MAC 开始接收/发送以太网帧
- bit[2]:ROCE 引擎使能,置 1 后 ROCE 模块才能上报事件到 DMA
- bit[3-7]:保留
- bit[8-15]:RX DMA 通道门铃请求数,写 N 表示要搬运 N 个包
- bit[31:16]:版本号,只读
这种布局的好处是,软件初始化时序一目了然:先配全局寄存器确认版本,再软复位引擎,然后使能 MAC,最后使能 ROCE 并下发 DMA 门铃。每一步都有明确的寄存器操作,不会出现"看代码才知道初始化顺序"的情况。
3.3 AXI Lite Slave 设计实践
自研逻辑的 AXI Lite 从机接口看起来简单,实际写的时候还是有细节要注意。核心的写通道信号是 AWADDR、AWVALID、AWREADY、WDATA、WVALID、WREADY、BVALID、BREADY,读通道是 ARADDR、ARVALID、ARREADY、RDATA、RVALID、RREADY。
最常见的坑是写数据和写地址的握手时序。AXI 协议的规则是:AW 通道和 W 通道独立握手,从机必须等两个通道都 valid 才能执行写操作。很多新手从机在收到 AWVALID 后立刻写寄存器,但此时 WDATA 可能还没准备好,结果数据写错了。正确的做法是:用一个状态机分别等 AWVALID&&AWREADY 和 WVALID&&WREADY,两个条件都满足后再打一拍执行寄存器写入,然后返回 BRESP。
另外一个容易踩的坑是读数据的返回延迟。AXI 协议要求从机在收到 ARVALID 后可以延迟若干周期返回 RVALID 和 RDATA,但如果你的模块里有多个周期才返回的组合逻辑路径,需要插入寄存器和流水,避免违反协议。最省事的办法是:在寄存器输出端直接打一拍,用 FSM 控制 RVALID 和 RREADY 的握手。
我自己的实现习惯是:写一个通用的、参数化的 AXI Lite 从机模板,参数里定义寄存器数量和位宽,内部用一个二维数组存储所有寄存器值,每次写操作按偏移索引落到对应的位域。这样新增一个寄存器模块只需要改参数和位域映射表,不需要改协议状态机。这个模板我复用了好几个项目,基本零 bug。
3.4 AXI Lite 自检:上板前最后一关
AXI Lite 链路在上板调试前一定要做自检,否则协议或地址映射的 bug 会一直在后面干扰你,让你分不清是配置问题还是数据面问题。我的自检方式是:
- 写一个版本号寄存器(只读,RTL 里硬编码),软件读出来做比对,确认 AXI Lite 数据通道正确。
- 写一个可读写的 scratch 寄存器,软件写 0x5A5A5A5A,再读回来,确认双向通路正常,特别注意 bit 位序和字节序。
- 在 MAC 和 ROCE 模块的复位逻辑里加一个"复位完成"位,软件轮询这个位,确认复位状态机能正常走到 idle。
这三个自检过完,才能认为 AXI Lite 配置面是可靠的。之后的数据面调试,如果出了问题,就可以放心地先去查 GT 和 MAC,而不会怀疑是软件没配上。
4. GT 与 100G 以太网的工程化配置:复位与时钟的次序问题
4.1 参考时钟的来源选择
100G 系统的根是参考时钟。VU9P 这类 UltraScale+ 的 GTY 收发器,参考时钟频率是 156.25MHz(100G 用的 25.78125G 线速率对应 156.25MHz 参考)。板上我用了 CDCM6208 这类低抖动时钟芯片产生 156.25MHz,经过 SMA 或者差分走线送给 FPGA 的参考时钟引脚,再经过 IBUFDS_GTE4 原语进入 GTY 组件。
这部分有个非常重要的检查点:换板子或者改走线后,一定要用频谱仪或者示波器确认参考时钟的幅度和抖动。我遇到过参考时钟幅度偏小导致 GT 偶发失锁的问题,表现为 link 能 up 但长时间跑流量会出现零星 CRC error,最后查了一个星期,拿频谱仪一测才发现时钟芯片的驱动强度配置不对,输出摆幅只有标准值的一半。
还有一点,100G Ethernet IP 核的共享逻辑(Shared Logic)选项一定要配置成 "Include Shared Logic in the core",这样 IP 核会自己例化 GT 的复位和时钟管理逻辑,你只需要关注顶层控制接口。如果选了 "External",GT 的复位、时钟、初始化都得自己写,多出几百行 RTL,而且很容易出时序问题,不推荐新手这么干。
4.2 复位时序:谁先谁后一步都不能错
GT 和 100G MAC 的复位大概是整个项目里最容易出问题的地方了,也是网上问得最多的。这里我把关键时序理一遍:
首先,参考时钟稳定之后,要等至少 10us 或者等 GTREFCLK 的 locked 信号拉高。然后对 GTY 做复位,这个复位会触发 PLL(RPLL)锁定,PLL locked 信号拉高后,才能撤销 TX/RX 的 reset。
其次,100G Ethernet IP 核推荐的复位流程是:gt_reset →(等待 gt_tx_resetdone 和 gt_rx_resetdone)→ tx_reset/rx_reset →(等待 tx_resetdone/rx_resetdone)。特别注意 GT 的 resetdone 信号是两个,TX 一个 RX 一个,不能只看一个就以为整个 GT 都好了。
这里有一个很隐蔽的坑:100G Ethernet IP 的 tx_reset 和 rx_reset 最好分开做,不要用一个总的 reset 同时复位两个域。因为 MAC 的 TX 域和 RX 域各自有独立的时钟(TXUSRCLK 和 RXUSRCLK),如果同时复位,两个时钟域的重建时间可能不一致,导致 MAC 内部状态混乱。Xilinx 官方文档里其实也建议这样做,但我第一版就是图省事用一个复位信号,结果 link 状态偶尔会卡在一个奇怪的中间态。
另外,POWER_DOWN 信号的时序也要留意。GTY 的 RX 通道在没接光模块的时候是 power down 状态,接上光模块后需要把 GTY 的 RX_POWER_DOWN 拉低,再等一定时间(通常几毫秒)让 CDR 锁定。如果 power down 跟复位并行操作,CDR 可能还没起来,复位就结束了,RX 永远无法 lock,链路自然 up 不了。
4.3 100G PCS 与 FEC 层的选择:测试和生产环境要分开
100G 以太网标准里,RS-FEC(Reed-Solomon Forward Error Correction,CL91)是长距离传输的标配。但在同一机柜、短距离光模块直连的测试环境里,RS-FEC 可能会带来不必要的延迟,而且如果对端设备没开 RS-FEC,两边协商不一致也会导致链路 up 不了。
我的建议是:硬件设计和寄存器配置里都要保留 FEC_enable 的控制位,测试时可以先关掉 RS-FEC,直连跑通基本功能,然后逐步打开 RS-FEC 验证纠错能力。生产环境则必须开 RS-FEC,因为 25G 串行链路上的随机误码率在长距离光模块下是客观存在的,没有 FEC,重传率会高到影响实际吞吐。
同时,MAC 层的 PAUSE 帧和 PFC(Priority Flow Control)也要想好。ROCE V2 依赖无损网络,靠 PFC 来保障队列不丢包。但 PFC 是一把双刃剑:它为高优先级流量保住了缓冲区,却可能引起 head-of-line blocking,把低优先级流量全部堵死。我这边因为对端交换机的 PFC 配置没调好,头几次联调的时候出现过"高优先级流量正常、整体吞吐上不去"的现象,后来发现是 PFC 门限太激进,导致低优先级队列疯狂被 pause。
4.4 用状态寄存器读数辅助问题定位
100G Ethernet IP 提供了很多状态信号,我在设计中把这些信号接成寄存器暴露给 AXI Lite:
gt_powergoodgt_tx_resetdone / gt_rx_resetdonetx_resetdone / rx_resetdonerx_fec_corrected_cw:FEC 纠正的码字数rx_fec_uncorrected_cw:FEC 无法纠正导致丢包的码字数rx_byte_alignment:RX 字节对齐状态
调试的时候,先读这些状态寄存器,而不是上来就看波形。很多时候问题出在复位时序或者配置没到位,波形一眼看过去全是乱的,倒不如把寄存器状态打出来,逐条比对 IP 核手册里的状态机要求。我第一次遇到 link 无法 up 时,就是发现gt_tx_resetdone一直不拉高,回溯到 GT 复位逻辑才发现参考时钟还没稳定就做了复位。
5. 上板调试的完整排查链路:从链路训练到带宽实测
5.1 第一步:把问题拆到最小闭环
上板调试最忌讳的就是一上来跑整条链路,出错之后根本不知道从哪查起。我的习惯是:先用回环把数据面缩小到最小闭环,跑通了再一步步扩大范围。
第一步,用 IBERT 或者其他 GT 自测工具验证 GTY 眼图。百 G 光口直连的场景,先让 4 路 GT 都跑起来,看 eye scan 结果和 BER 是否正常。这一步主要验证物理层,跟 MAC 和 ROCE 逻辑无关。眼图不好看,后面全白搭。
第二步,把 100G MAC 配成近端回环模式(Near-End PMA Loopback),也就是数据从 GTY 发出后在 PHY 内部直接回到 RX,不经过光模块和外部链路。这时验证的是 MAC 和 GT 之间的数据通路,包括 CRC 是否正确、RS-FEC 编解码是否正常。
第三步,用光模块回环(光口接一个 loopback 模块)跑通,验证光模块的 TX/RX 通路。这时候 RS-FEC 和 25G 信号质量都会参与进来,如果这一步通了,说明物理层链路基本没问题。
这三步全过之后,才轮到 ROCE 引擎参与联调。
很多新手跳过了 IBERT 直接上用户逻辑,结果光是 4 路 GT 的眼图问题就折腾了一周。一定先隔离物理层。
5.2 常见故障现象与定位思路
我把自己遇到过的典型故障症状整理成了排查表,分享出来供参考:
| 故障现象 | 可能原因 | 排查方向 |
|---|---|---|
| link 一直 up 不了 | 参考时钟异常、复位时序错误、光模块 RX 功率过低 | 检查参考时钟幅度、复位时序、光模块收光功率 |
| link 能 up 但抓不到包 | MAC 配置错误、RX 字节对齐失败、AXI-Stream 握手状态机问题 | 检查 MAC 的 RX 状态寄存器、用 ILA 抓 AXI-Stream 接口 |
| 链路时通时断 | 光模块接触不良、RS-FEC 未开导致误码触发链路重启、PFC 风暴 | 查看 FEC 错误计数、检查光模块告警、抓取链路重启时的 PCS 状态 |
| CRC error 持续增长 | 信号质量差、时钟抖动大、RS-FEC 未生效 | 检查 eye scan 结果、确认 RS-FEC 使能、检查参考时钟抖动 |
| 吞吐只有一半 | PCIe MPS 配置过低、描述符环深度不足、门铃写回过于频繁 | 调整 MPS 到 512B、加大描述符环、批量写门铃 |
这个表只能作为方向参考,实际定位一定要配合计数器。我在 ROCE 引擎的各个关键节点上都加了计数寄存器,比如收到多少包、丢了多少包、CRC 错多少包、重传了多少包、发送队列空了多少拍。这样出现问题,软件一次性把所有计数读出来,很快就能判断是发送方向还是接收方向的问题。
5.3 性能测试方法:从链路训练到带宽实测
基础链路调通之后,就得验证性能到底能不能到线速。我用的测试方法是:主机端通过 DMA 向 FPGA 写一大块 buffer,FPGA 的 ROCE 引擎把它封装成 RDMA WRITE 请求发到对端;对端的 FPGA 收到数据后 DMA 写入主机内存,并回一个 ACK。主机端查对端内存里的数据校验和,确认数据无误。
影响吞吐的几个关键参数:
- 包长:线速吞吐和包长直接相关。1500B 的包,纯 payload 有效带宽约 94Gbps(扣除以太网头、IP 头、UDP 头和 CRC 的开销);64B 小包,线速也只有约 9.5Gbps 的有效带宽。所以性能测试要用大包为主,小包单独压测 PPS。
- 描述符环深度:描述符太少会导致 DMA 来不及搬运,接收端 buffer 满了就开始丢包。我这边 RX 描述符环深度开到了 4096,才勉强稳住 100G 线速下的小包压力。
- MPS 和 TLP 对齐:前面说过 MPS 512B 比 256B 好,但 TLP 的地址必须 128B 对齐,否则会触发额外的 split 操作,降低吞吐。
实测下来,1500B 包长、32 深度发送队列、512B MPS、RS-FEC 开启的条件下,吞吐稳定在 93.8Gbps。这个数字已经接近协议开销后理论极限的 99.5% 了。
5.4 一次心有余悸的问题排障记录
分享一个我调试了整整三天的问题。现象是:长时间跑大流量(超过 10 分钟)后,发送方向突然卡住,停止发包,且不恢复。复位之后能恢复,但跑一段时间又卡死。
一开始我怀疑是 GT 的复位偶尔出问题,抓了 GT 的复位状态寄存器,发现一切正常。后来抓发送队列的计数,发现在卡死前的最后时刻,发送 FIFO 的 almost_full 信号拉高过一次,之后发送状态机就停在了一个中间状态,再也不动了。
查代码发现,我的发送状态机在 FIFO almost_full 的时候会暂停仲裁,等 FIFO 空间释放。但问题出在:几乎同时在等待 DMA 门铃更新,而 DMA 门铃更新依赖发出去的包被 ACK 后驱动收到门铃事件,驱动再写新的描述符。这一环扣一环的依赖关系,只要有一个包丢失,整个链路就死锁了——FIFO 满等 DMA,DMA 等 ACK,ACK 永远来不了。
解决方案是在发送状态机里加了超时保护:如果等待 DMA 门铃超过一定时间(比如 100us),认为链路异常,主动 trigger 一次 ROCE 引擎软复位,并重发未确认的包。这个改动看起来简单,但其实反映了设计分层里的一个重要教训:任何跨模块的握手,在真实链路上都必须有超时和恢复机制,不能单纯依赖"正常流程一定会走完"的假设。
6. 经验总结:几个值得留意的架构决策与个人体会
6.1 把协议解析和业务逻辑解耦
这是我整个项目最后悔没早点做的一件事。第一版 ROCE 引擎里,我在解析 BTH 的同时就把业务逻辑的判断也写在了一起,比如收到 WRITE 请求后直接去查 DMA 描述符。结果就是协议状态机和业务状态机纠缠在一起,每加一个新功能都要把整个状态机过一遍,改了 A 功能又把 B 功能改坏了。
重构成两层之后才算清清爽爽:底层是 ROCE 协议状态机,只管收发/ACK/重传/PSN 维护,输出一个标准化的"数据块访问请求";上层是业务逻辑,根据请求类型做 DMA 搬运、计算、或者写寄存器。协议层的代码不该知道上层业务的长相,这是高速网络 RTL 设计里最值得坚持的原则之一。
6.2 计数器是调试效率的最大杠杆
前面提到我加了大量计数寄存器,这里再强调一次:在 RTL 里加计数器几乎不花成本,但排查问题时节省的时间是成倍的。我现在拿到一个 RTL 模块,第一件事就是看它的计数信号是否齐全。比如 ROCE 引擎至少要有:收包总数、发包总数、CRC 错误数、重传数、超时数、队列满停顿数。有这些计数,就能回答"这个模块正常工作吗"的问题,缩小排查范围。
有个同事说过一句很精辟的话:"没有计数的 RTL 模块不算写完,只能算写完了一半。" 我举双手赞成。
6.3 板级调试前先做好 AXI Lite 自检
前面详细讲过 AXI Lite 自检流程,这里再补充一个容易忽略的细节:寄存器自检时不要只写 0x5A5A5A5A,还要写 0xA5A5A5A5 和 0xFFFFFFFF、0x00000000 这几种边界值。这样可以发现数据线的高位粘连、低位悬空、某些特定的 bit 无法翻转等问题。我遇到过一块板子,scratch 寄存器读出来高 16 位一直是 0,排查后发现是 AXI Lite 总线的 WDATA 高 16 位在 PCB 上走线有问题,接受了某种电气干扰,倒是数据位线问题,不是逻辑问题。
6.4 XADC 监测:100G 高负载下容易被忽略的散热问题
最后提一个很多人忽略的点:100G 高速收发器的功耗和发热非常可观。VU9P 跑满 4 路 25G GT、外加大量逻辑翻转,芯片局部温度可以达到 80 度以上。我建议在上板调试时,用 XADC 原语读取芯片温度和电压,并把这个值通过 AXI Lite 暴露给软件。
有一次性能测试中,我发现吞吐逐步下降,一开始以为是代码问题,后来用 XADC 一测,芯片核心温度已经到 98 度,触发了一些时序路径的降频保护。把风扇转速调高、加了散热片之后,吞吐立刻恢复正常。高速接口项目的稳定性,有时候问题不在逻辑,而在物理。
这套系统从开始规划到跑通 100G 线速,前后花了大概两个半月。回头看,真正花时间的地方不是 RTL 编码本身,而是对高速接口、协议、DMA、复位时序这些环节的理解决定了调试效率。尤其是 AXI Lite 配置链路和 GT 复位时序这两个环节,做对了能省下大量联调时间。本文记录的过程是我个人在 Xilinx UltraScale+ 平台上的实践复盘,不同板卡和 IP 版本的细节会有差异,但排查的思路和架构上的取舍是通用的。希望这篇分享能帮你避开一些我踩过的坑,或者至少让你知道,遇到类似的问题时,该从哪个方向下手去定位。