1. 为什么要在 FPGA 上折腾 RoCE v2
做高性能网络的人都有一个共同的痛:CPU 越来越快,但网络协议栈的处理开销始终是瓶颈。一个 100Gbps 的链路,如果走传统 TCP/IP 内核协议栈,光是数据拷贝和中断处理就能把好几个物理核吃满,应用层真正拿到的有效带宽可能连一半都不到。RoCE v2(RDMA over Converged Ethernet version 2)就是冲着这个痛点来的——它把 RDMA 的内存语义直接架在 UDP/IP 之上,让网卡硬件完成报文封装、可靠传输和内存直接访问,CPU 基本不参与数据搬运。
那为什么要在 FPGA 上做这件事?原因很直接:Xilinx 的 ERNIC IP 就是一颗可以烧进 FPGA 的“RDMA 网卡内核”。你不需要去买昂贵的商用 RDMA 网卡,只要手里有一块带高速收发器的 Xilinx 卡(比如 Alveo 系列或者带 CMAC 的 UltraScale+ 器件),配合 ERNIC IP,就能自己搭出一个支持 RoCE v2 的 100G 网络加速端点。这对做金融低延迟、分布式存储、HPC 互联、甚至自研智能网卡的团队来说,价值非常大——你拿到的是完全可控的硬件数据通路,而不是一个黑盒。
这篇文章面向的是有 FPGA 基础、想切入高速网络加速的开发者。我会围绕 Xilinx ERNIC IP 和 PG332 文档,把 RoCE v2 的硬件实现路径拆开讲清楚:从整体架构怎么搭、IP 怎么配、关键接口怎么接,到实际调试中会踩的坑。PG332 是 Xilinx 官方给 ERNIC 的产品指南,文档号 PG332,很多人第一次翻会觉得它写得“点到为止”,很多细节要自己补,我下面会把这些补全。
2. ERNIC IP 的整体架构与设计思路拆解
2.1 ERNIC 到底在 FPGA 里扮演什么角色
先把概念理清楚。ERNIC 全称是 Ethernet RDMA NIC,它不是一颗独立的芯片,而是一套可以综合进 FPGA 的 IP 核。它的定位是:在 FPGA 内部实现一个符合 RoCE v2 规范的 RDMA 端点,对外通过以太网 MAC 收发报文,对内通过 AXI 接口和用户逻辑或主机交互。
你可以把它理解成“把一张 RDMA 网卡塞进了 FPGA 的 fabric 里”。传统的 RDMA 网卡(比如那些商用卡)内部也是类似的逻辑:一个以太网 MAC/PCS、一个报文解析与封装引擎、一套队列管理(QP)、一套内存翻译(MR/MTT)机制。ERNIC 把这些东西做成了可配置的 IP,你负责提供 MAC、时钟、内存和上层驱动。
从 PG332 的描述看,ERNIC 的核心模块大致分几块:以太网接口层(对接 CMAC 或其它 MAC)、RoCE v2 报文处理引擎(负责 BTH、RETH 等头部封装解析)、传输层状态机(处理 QP 状态、序列号、ACK/NAK)、DMA 引擎(读写主机或 DDR 内存)、以及配置与管理接口(通过 AXI-Lite 访问寄存器)。这几块协同工作,才能完成一次完整的 RDMA 写或读操作。
2.2 为什么选 ERNIC 而不是自己从零写
有人会问:RoCE v2 协议我也懂,为什么不自己用 Verilog 撸一个?答案在于工程量。RoCE v2 看起来只是“UDP 里塞个 BTH”,但真正难的是可靠传输那一套:序列号管理、重传、拥塞控制、QP 状态迁移、内存保护、乱序处理。这些逻辑的状态机极其复杂,自己写一版能跑通的,没个一年半载下不来,而且很难保证和标准协议栈互通。
ERNIC 的价值就在于它把这些“脏活累活”都封装好了,而且经过了 Xilinx 的验证,能和主流 RDMA 生态(比如标准 verbs 接口的驱动)对接。你只需要关注:怎么把 MAC 接上、怎么分配内存、怎么配 QP。这是典型的“站在巨人肩膀上”的思路。当然代价是你要接受它的接口约束和资源占用,但对绝大多数项目来说,这个交换是划算的。
2.3 一个典型的系统框图长什么样
我实际搭过的系统大致是这样分层的:最底层是 FPGA 的 GT 收发器(比如 GTY),往外接光模块;GT 之上是 CMAC(100G 以太网 MAC),负责 PCS/PMA 和 MAC 层;CMAC 的 AXI-Stream 接口接到 ERNIC 的以太网侧;ERNIC 的内存侧通过 AXI 接到 DDR 控制器或者直接接到用户逻辑的 BRAM;控制侧通过 AXI-Lite 接到 MicroBlaze 或者主机的 PCIe BAR。
这里有个关键点:ERNIC 对内存的访问是走 AXI 的,所以你的内存子系统带宽必须够。100Gbps 线速下,双向流量意味着内存侧要能扛住 200Gbps 以上的读写,DDR4 一般够用,但如果你用 BRAM 做小缓存,就得仔细算容量和带宽了。这个后面讲参数计算时会细说。
3. PG332 文档解读与核心配置要点
3.1 PG332 里最该先看的几节
PG332 篇幅不短,但真正决定你能不能跑起来的,其实就那么几节。我的建议是:先看“Overview”把架构图吃透,再看“Core Interfaces”搞清楚每个 AXI 接口的方向和位宽,然后重点啃“Configuration”那一章,最后对照“Example Design”看官方怎么接的。
很多人一上来就翻寄存器手册,结果被几百个寄存器劝退。其实寄存器是最后调的时候才需要的,前期你只要把 IP 配好、接口接对,大部分功能就能跑。PG332 的寄存器章节更像是“字典”,用到哪个查哪个,不用通读。
3.2 关键配置参数怎么选
ERNIC 的配置里,有几个参数直接决定资源占用和功能:
| 配置项 | 含义 | 选型建议 |
|---|---|---|
| 线速 | 支持 10G/25G/40G/100G | 按你的 GT 和 CMAC 能力选,100G 资源占用最大 |
| QP 数量 | 支持的队列对数量 | 按并发连接数定,一般 256~1024 够用,越多 BRAM 占用越高 |
| MTU | 最大传输单元 | RoCE v2 常用 1024 或 4096,要和交换机一致 |
| 内存接口位宽 | AXI 数据位宽 | 512bit 是常见选择,匹配 DDR 效率 |
| 是否使能 DCQCN | 拥塞控制 | 无损网络建议开,普通环境可关 |
这里重点说 MTU。RoCE v2 的 MTU 必须端到端一致,包括交换机。如果你 FPGA 侧配 4096,交换机配 1024,那大包会被分片或者丢弃,性能直接崩。我踩过一次坑:实验室交换机默认 MTU 1500,我 IP 里配了 4096,结果小包能通、大包全丢,查了两天才发现是 MTU 不匹配。所以配之前先确认整条链路的 MTU。
3.3 时钟与复位设计
ERNIC 涉及多个时钟域:GT 的收发时钟、MAC 的 AXI-Stream 时钟、内存侧的 AXI 时钟、控制侧的 AXI-Lite 时钟。这些时钟之间的跨域处理是 IP 内部做的,但你要保证每个时钟的频率和相位关系符合 PG332 的要求。
复位方面,ERNIC 有一个全局复位和若干分模块复位。我的经验是:上电后先复位 GT 和 CMAC,等链路 up(看 CMAC 的 status 寄存器),再释放 ERNIC 的复位。如果顺序反了,ERNIC 可能在链路没起来的时候就开始发报文,导致状态机卡死。这个顺序在 PG332 里没有特别强调,但实际调试中很关键。
4. RoCE v2 报文处理与实操实现
4.1 一次 RDMA 写操作的完整流程
要理解 ERNIC 怎么工作,最好的办法是跟一遍 RDMA Write 的流程。假设主机 A 要往主机 B 的内存里写数据:
- 主机 A 的驱动把数据准备好,告诉 ERNIC 的 QP:“把这批数据发到 B 的某个虚拟地址”。
- ERNIC 从内存读出数据,封装成 RoCE v2 报文:外层是以太网头 + IP 头 + UDP 头(目的端口 4791),内层是 BTH(Base Transport Header)+ RETH(RDMA Extended Transport Header)+ 数据。
- 报文经 CMAC 发出,穿过交换机到达主机 B。
- 主机 B 的 ERNIC 收到报文,解析 BTH 拿到 QP 号,解析 RETH 拿到虚拟地址,通过内存翻译表(MTT)把虚拟地址转成物理地址,直接把数据 DMA 进内存。
- B 回一个 ACK,A 收到后确认完成。
整个过程 CPU 只在第 1 步参与,后面全是硬件干的。这就是 RDMA 的威力。ERNIC 在 FPGA 里实现的,就是第 2 步和第 4 步的硬件逻辑。
4.2 报文封装的硬件实现细节
ERNIC 内部对报文的封装是流水线式的。以太网头、IP 头、UDP 头这些字段大部分是固定的(除了 checksum 和长度),可以预先算好放在寄存器里。BTH 和 RETH 则要根据每次操作动态生成。
这里有个细节:UDP checksum。RoCE v2 要求 UDP checksum 有效,但很多实现为了性能会把它算成 0(IPv4 下允许)。ERNIC 支持硬件计算 checksum,但会消耗一些逻辑资源。如果你的网络环境对 checksum 校验严格,就必须开;如果是可控的内网,可以关掉省资源。我一般在内网测试时关掉,上生产再开。
另一个细节是 PSN(Packet Sequence Number)。每个 QP 的每个报文都有递增的 PSN,接收侧靠它检测丢包和乱序。ERNIC 内部有 PSN 生成和校验逻辑,你不需要手动管,但要保证 QP 初始化时 PSN 从 0 开始且两端一致。
4.3 内存翻译表(MTT)的配置
RDMA 的核心之一是“零拷贝”,而零拷贝的前提是网卡能直接把数据写进应用的内存。这需要一张虚拟地址到物理地址的映射表,也就是 MTT。ERNIC 通过 MTT 把 RETH 里的虚拟地址翻译成物理地址,然后发起 DMA。
MTT 的配置是通过驱动完成的。在 FPGA 场景下,如果你没有标准驱动,就需要自己写一段逻辑来填 MTT。PG332 里给了 MTT 的格式:每个 entry 包含物理页基址和权限位。我的做法是:在初始化阶段,把应用要用的内存区域按页(比如 4KB)注册进 MTT,每个页一个 entry。这样 ERNIC 收到报文后,用虚拟地址的高位索引 MTT,低位作为页内偏移,就能算出物理地址。
这里要注意 MTT 的大小。如果你注册 1GB 内存,按 4KB 页算就是 26 万个 entry,每个 entry 假设 8 字节,就是 2MB 的 MTT。这块内存要放在 ERNIC 能快速访问的地方,一般放 DDR 里,但访问延迟会影响性能。所以有些实现会用大页(2MB),把 entry 数量降下来。
5. 实操过程:从零搭一个 ERNIC 工程
5.1 环境准备与 IP 获取
先说环境。你需要 Vivado(建议 2021.2 或更新,老版本对 ERNIC 支持不全),一块带 GTY 的 Xilinx 开发板(Alveo U250/U280 或者自制的 UltraScale+ 板卡),以及一个支持 RoCE v2 的交换机(或者两台机器直连)。
ERNIC IP 不是免费随便下的,它属于 Xilinx 的以太网 IP 家族,需要 license。如果你有 Vivado 的完整授权,一般能在 IP Catalog 里搜到。搜不到的话,去 Xilinx 官网下载 PG332 对应的 IP 包,手动加到 IP repository 里。
5.2 搭建 Block Design
我习惯用 Block Design 来搭,直观且不容易出错。步骤大致是:
- 新建工程,选对器件型号。
- 添加 CMAC IP,配置成 100G,接口选 AXI-Stream。
- 添加 ERNIC IP,线速选 100G,QP 数量按需,MTU 设 1024。
- 添加 DDR4 控制器,配置成 AXI 接口,位宽 512bit。
- 用 AXI-Stream 把 CMAC 和 ERNIC 的以太网侧连起来。
- 用 AXI 把 ERNIC 的内存侧接到 DDR 控制器。
- 加一个 MicroBlaze 或者 Zynq PS,通过 AXI-Lite 接 ERNIC 的控制接口。
- 连时钟和复位,注意前面说的复位顺序。
这里有个容易错的地方:CMAC 和 ERNIC 之间的 AXI-Stream 位宽要匹配。CMAC 100G 一般是 512bit @ 322MHz 左右,ERNIC 的以太网侧也是类似位宽,但具体要查 PG332 的接口表。如果位宽不一致,要加位宽转换器,但转换器会引入延迟,尽量让两边一致。
5.3 约束文件与时序收敛
高速设计里,约束是命根子。ERNIC 涉及 100G 的 AXI-Stream,时钟频率高,时序很紧。你需要:
- 给 GT 的参考时钟加 create_clock。
- 给 CMAC 和 ERNIC 的 AXI-Stream 时钟加 create_clock,并设置正确的频率。
- 跨时钟域的信号加 set_false_path 或 set_max_delay,具体看 PG332 的建议。
- 内存侧 AXI 加 output delay 约束,匹配 DDR 控制器的要求。
我实测下来,ERNIC 在 UltraScale+ 上跑 100G,时序收敛的难点主要在 AXI-Stream 那一段。如果时序过不去,可以试着降低一点频率(比如从 322MHz 降到 300MHz),或者优化布局约束。Vivado 的 implementation strategy 选 Performance_Explore 通常有帮助。
5.4 上板调试与链路建立
综合实现生成 bitstream 后,下载到板子。第一步是看 CMAC 的链路状态:读 CMAC 的 status 寄存器,确认 link up 且速率协商正确。如果 link 没起来,先查光模块和光纤,再看 GT 的复位和时钟。
链路 up 之后,配置 ERNIC 的 QP。通过 AXI-Lite 写寄存器:设置 QP 状态为 INIT,配置目的 IP、MAC、QPN、PSN。然后迁移到 RTR(Ready to Receive),再迁移到 RTS(Ready to Send)。每一步的状态迁移都有对应的寄存器操作,PG332 里有详细说明。
状态迁移完成后,就可以试着发一个 RDMA Write 了。我一般先用一个小数据量(比如 64 字节)测试,用 ILA 抓 AXI-Stream 上的报文,看 BTH 和 RETH 是否正确。确认无误后再加大数据量测带宽。
6. 常见问题与排查技巧实录
6.1 链路起不来怎么办
这是最常见的问题。排查顺序:
- 看 GT 的复位是否释放,参考时钟是否有。
- 看光模块是否被识别,TX_DISABLE 是否拉低。
- 看 CMAC 的 link status 寄存器,如果是 0,检查 PCS 层。
- 如果两台机器直连,确认一端是 master 一端是 slave(或者都配成 auto-negotiation)。
我遇到过一次:光模块是好的,但 GT 的参考时钟频率设错了(应该是 156.25MHz,我设成了 125MHz),结果链路死活起不来。所以时钟一定要对着板卡手册确认。
6.2 报文发出去了但收不到
如果链路 up 了,报文也发了,但对端收不到,先查这几个:
- 目的 MAC 和 IP 是否配对。
- UDP 目的端口是否是 4791(RoCE v2 的标准端口)。
- 交换机是否开了 RoCE v2 支持(有些交换机默认不开)。
- MTU 是否一致。
我踩过的坑是交换机没配 PFC(Priority Flow Control)。RoCE v2 在无损网络里依赖 PFC 来保证不丢包,如果交换机没开 PFC,大流量下丢包会导致重传,性能暴跌甚至连接断开。所以生产环境一定要配 PFC 和 ECN。
6.3 性能上不去怎么调
如果功能通了但带宽只有几 Gbps,排查方向:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 带宽低且 CPU 占用高 | 走了内核协议栈 | 确认用的是 RDMA verbs 接口 |
| 带宽低但 CPU 低 | PCIe 或内存带宽瓶颈 | 测 DDR 带宽和 PCIe 带宽 |
| 带宽波动大 | 拥塞控制没开 | 开 DCQCN,配 ECN |
| 小包性能差 | 报文处理延迟高 | 优化流水线,减少跨时钟域 |
我实测过一个案例:DDR 带宽够,但 AXI 位宽只有 256bit,结果内存侧成了瓶颈,100G 只能跑到 60G。后来把 AXI 位宽改成 512bit,立刻跑满。所以内存接口位宽一定要算够。
6.4 独家避坑清单
- 复位顺序:先 GT/CMAC,后 ERNIC,反了会卡死。
- MTU 一致:端到端都要确认,包括交换机。
- PFC/ECN:无损网络必配,否则大流量丢包。
- AXI 位宽:内存侧至少 512bit,否则带宽不够。
- 时钟约束:AXI-Stream 时钟要仔细约束,时序过不去先降频。
- MTT 大小:大页能减少 entry 数量,降低访问延迟。
- UDP checksum:内网可关,生产要开。
7. 一些实操心得与扩展思路
做 ERNIC 这段时间,我最大的体会是:FPGA 网络加速的门槛不在写 RTL,而在“系统集成”。ERNIC IP 本身很成熟,但你把它和 CMAC、DDR、PCIe、驱动串起来,中间任何一个环节出问题,现象都是“不通”或“慢”,排查起来很费劲。所以我的建议是:先用官方 Example Design 跑通,再一点点改成自己的架构,不要一上来就大改。
另外,ERNIC 只是 RoCE v2 的一个实现,如果你要做更定制化的东西,比如在 RDMA 基础上加自己的协议头,或者做智能网卡的 offload,可以在 ERNIC 的用户接口上挂自己的逻辑。PG332 里提到了用户可扩展的接口,这块空间很大,值得深挖。
最后分享一个小技巧:调试 RDMA 时,Wireshark 是神器。只要交换机支持端口镜像,把流量镜像出来,用 Wireshark 解 RoCE v2 报文,BTH、RETH、PSN 一目了然。比对着 ILA 抓波形猜要快得多。我很多问题都是靠 Wireshark 定位的,强烈推荐。