1. 为什么要在FPGA上折腾100G RDMA
先把结论摆在前面:在FPGA上实现100G RDMA,核心难点从来不是"写代码",而是把Xilinx的CMAC、PCIe、DMA这几个IP核的时钟域、复位域、数据位宽对齐。我见过太多人卡在"链路起来了但数据不通"或者"能收不能发"的状态,最后发现是某个IP核的复位极性搞反了。
这套方案适合谁?适合已经做过千兆以太网、对AXI4-Stream协议有基本概念、手里有Alveo或类似加速卡(或者自研板卡搭载UltraScale+系列FPGA)的工程师。如果你连AXI-Lite和AXI-Stream的区别都还没搞清楚,建议先补一下基础,否则后面每一步都是坑。
RDMA(Remote Direct Memory Access)的本质是让网卡直接读写远端内存,绕过CPU和内核协议栈。在FPGA上做这件事,意味着你要自己实现一个"网卡"——从物理层的SerDes,到MAC层,再到传输层的可靠传输逻辑,全部用RTL或者IP核搭出来。100G的线速意味着每秒要处理约1.5亿个最小包,每个时钟周期都不能浪费。
Xilinx(现在叫AMD)提供了一套相对完整的IP生态:CMAC(100G以太网MAC)、PCIe Gen3/Gen4 Integrated Block、AXI DMA、以及QDMA。但官方文档往往只告诉你"怎么配置",不告诉你"为什么这么配"以及"配错了会怎样"。这篇内容就是把我踩过的坑和验证过的配置路径完整梳理一遍。
提示:本文基于UltraScale+系列FPGA和Vivado 2022.2版本,不同器件和工具版本在IP核界面和参数命名上可能有差异,但核心逻辑相通。
2. 100G以太网MAC层的IP核选型与配置逻辑
2.1 CMAC vs. 100G Ethernet Subsystem:到底选哪个
Xilinx提供两个层级的以太网IP:一个是CMAC(100G Ethernet MAC)独立IP,另一个是100G Ethernet Subsystem,后者把CMAC和PCS/PMA打包在一起。选哪个取决于你的板卡设计。
如果你的板卡上已经有外部PHY芯片(比如通过Interlaken或CAUI-4接口连接),那用CMAC就够了,PCS/PMA由外部PHY处理。如果是FPGA直接驱动光模块(比如QSFP28直连),那就需要Subsystem,因为它包含了PCS和PMA层。
我个人的建议是:优先用Subsystem。原因很简单,CMAC单独使用时,你需要自己处理CAUI-4接口的64b/66b编解码和通道对齐,这部分调试起来非常痛苦。Subsystem把这些都封装好了,你只需要关心AXI4-Stream接口上的以太网帧。
配置Subsystem时,有几个参数必须搞清楚:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| Line Rate | 100 Gbps | 对应QSFP28的4通道25G |
| PCS/PMA Mode | CAUI-4 | 4通道,每通道25.78125 Gbps |
| AXI4-Stream Data Width | 512 bit | 对应64字节/周期的处理能力 |
| Clock Frequency | 390.625 MHz | 512bit × 390.625MHz ≈ 200Gbps(双沿) |
| Include FCS | 勾选 | 让IP核自动处理CRC校验 |
这里重点说数据位宽和时钟频率的匹配。100G线速下,如果AXI4-Stream位宽是512bit,那么每个时钟周期需要传输64字节。以太网最小帧是64字节(含FCS),也就是说每个周期至少要处理一个最小帧。时钟频率的计算方式是:100Gbps ÷ 512bit = 195.3125 MHz,但实际IP核内部会用一个更高的时钟(390.625 MHz)来跑,因为要处理双沿或者内部逻辑的时序余量。
2.2 复位序列:最容易翻车的地方
CMAC/Subsystem的复位不是拉一下就行。它有一个严格的顺序:
- 先复位PMA(物理层),等待PMA锁相环锁定
- 再复位PCS(编码层),等待通道对齐完成
- 最后复位MAC层,等待链路状态变为"up"
如果你一次性把所有复位都拉高再拉低,大概率会出现"链路显示up但实际收不到包"的情况。我的做法是写一个简单的状态机,用tx_disable和rx_disable信号来控制,配合IP核输出的stat_rx_aligned和stat_tx_aligned状态位来判断。
// 简化的复位状态机 localparam IDLE = 0, RESET_PMA = 1, WAIT_PMA = 2, RESET_PCS = 3, WAIT_PCS = 4, RESET_MAC = 5, READY = 6; always @(posedge clk) begin case(state) IDLE: if (start_reset) state <= RESET_PMA; RESET_PMA: begin pma_reset <= 1'b1; state <= WAIT_PMA; end WAIT_PMA: begin pma_reset <= 1'b0; if (pma_locked) state <= RESET_PCS; end // ... 后续状态类似 endcase end注意:
stat_rx_aligned信号在通道对齐完成前会一直抖动,不要用它直接做复位逻辑的触发条件,一定要加消抖或者用状态机过滤。
2.3 时钟域处理:390MHz不是闹着玩的
100G Subsystem输出的用户时钟是390.625 MHz,这个频率在UltraScale+上属于比较高的。如果你的用户逻辑(比如包处理、DMA接口)也跑在这个时钟下,时序收敛会非常困难。
我的做法是:在Subsystem的AXI4-Stream接口后面加一个异步FIFO,把390MHz域的数据搬到250MHz或300MHz域。虽然位宽不变(还是512bit),但时钟降下来之后,后端逻辑的时序压力会小很多。代价是引入了一点延迟,但对于RDMA场景来说,几百纳秒的延迟增加是可以接受的。
FIFO的深度建议至少64深,因为100G线速下突发流量很常见,浅FIFO容易溢出。另外,FIFO的读写时钟比要算清楚:390.625MHz写、250MHz读,读侧位宽需要扩展到1024bit才能匹配带宽(512×390.625 ≈ 1024×195.3125)。
3. PCIe与DMA:主机和FPGA之间的数据通道
3.1 PCIe Gen3 x16还是Gen4 x8
100G网络的数据要落到主机内存,PCIe带宽必须够。PCIe Gen3 x16的理论带宽是15.75 GB/s,实际可用约12-13 GB/s。100G以太网的理论带宽是12.5 GB/s,所以Gen3 x16刚好够用,但余量很小。
如果板卡支持Gen4,建议用Gen4 x8,理论带宽同样是15.75 GB/s,但x8的引脚数更少,布线更容易。不过Gen4的信号完整性要求更高,PCB板材和连接器都要升级,成本会增加。
在Vivado里配置PCIe IP核时,重点注意这几个参数:
- Maximum Link Width:根据板卡实际走线选择x8或x16
- AXI Data Width:建议512bit,和DMA匹配
- Reference Clock Frequency:100MHz或250MHz,看板卡晶振
- BAR配置:至少需要一个BAR用于寄存器访问,一个BAR用于DMA描述符
3.2 AXI DMA vs. QDMA:选型的关键考量
Xilinx提供两种DMA方案:AXI DMA和QDMA。AXI DMA是免费IP,配置简单,但只支持简单的Scatter-Gather模式,队列深度有限。QDMA是付费IP(需要License),支持多队列、多功能,更适合高性能场景。
对于RDMA应用,我强烈建议用QDMA。原因有三点:
第一,QDMA支持Descriptor Bypass模式,可以直接从主机内存读取描述符,不需要FPGA内部维护描述符缓存。第二,QDMA的队列深度可以到4096,而AXI DMA通常只有64或256。第三,QDMA支持MM(Memory Mapped)和ST(Streaming)两种模式,ST模式可以直接和CMAC的AXI4-Stream对接,省掉一层转换逻辑。
配置QDMA时,这几个参数需要特别注意:
| 参数 | 推荐值 | 原因 |
|---|---|---|
| PCIe Gen | Gen3/Gen4 | 根据板卡能力 |
| Number of Queues | 8-16 | 太少不够用,太多浪费逻辑 |
| Descriptor Size | 64 bit | 支持64位地址 |
| C2H/H2C Buffer Size | 4KB | 匹配以太网MTU |
| MSI-X Vectors | 每队列一个 | 中断亲和性 |
3.3 描述符环的设计:软件和硬件的契约
RDMA的核心是"零拷贝",也就是说数据从网卡到应用内存不经过内核缓冲区。在FPGA实现中,这意味着QDMA的描述符环要直接指向应用层的内存地址。
描述符环的结构通常是这样的:主机驱动在内存中分配一块连续区域,分成多个描述符条目,每个条目包含源地址、目的地址、长度、控制位。FPGA端的QDMA IP核通过PCIe读取这些描述符,然后执行DMA传输。
这里有一个非常容易踩的坑:描述符环的对齐。QDMA要求描述符环的基地址必须按4KB对齐,而且每个描述符的大小必须是16字节的整数倍。如果你在驱动里用malloc分配内存,大概率不会对齐,需要用posix_memalign或者mmap来保证对齐。
// 正确的描述符环分配方式 void *desc_ring; posix_memalign(&desc_ring, 4096, RING_SIZE * sizeof(struct qdma_desc)); // 然后把这个物理地址写入QDMA的配置寄存器提示:QDMA的驱动在Linux内核中有开源版本,但FPGA端的IP核配置必须和驱动的版本匹配。Vivado 2022.2对应的QDMA驱动版本是2022.1,用错版本会出现"队列使能失败"的错误。
4. RDMA传输层的RTL实现要点
4.1 可靠传输:Go-Back-N还是选择性重传
RDMA over Converged Ethernet(RoCE)的传输层需要实现可靠传输。在FPGA上,最简单的是Go-Back-N,也就是接收方发现丢包后,发送方从丢失的包开始重传所有后续包。复杂一点的是选择性重传(Selective Repeat),只重传丢失的包。
Go-Back-N的实现简单,只需要维护一个发送窗口和一个确认号。但缺点是丢包时带宽利用率下降明显。在100G线速下,即使丢包率只有0.01%,Go-Back-N的重传开销也会让有效带宽降到80G以下。
选择性重传需要维护一个接收位图(Bitmap),记录哪些包已经收到。位图的宽度等于窗口大小,通常用BRAM实现。每个包到达时,根据序列号设置对应的位;发送方根据位图决定重传哪些包。
我的建议是:如果追求极致性能,用选择性重传;如果只是验证功能,Go-Back-N足够。选择性重传的位图逻辑大概需要200-300个LUT,在UltraScale+上不算什么,但调试复杂度会高不少。
4.2 序列号回绕:32位够不够
TCP的序列号是32位,在100G线速下,32位序列号大约每3.4秒就会回绕一次(2^32字节 ÷ 12.5GB/s ≈ 0.34秒,实际考虑包间隔会更长)。对于RDMA来说,序列号回绕必须处理,否则会出现"旧包被当成新包"的错误。
处理方式有两种:一是用64位序列号,彻底避免回绕;二是用32位序列号,但在比较时考虑回绕。64位序列号的代价是每个包多4字节开销,在100G下这4字节的带宽损失可以忽略。所以我倾向于直接用64位序列号,省去回绕判断的逻辑。
4.3 拥塞控制:DCQCN的简化实现
RoCEv2使用DCQCN(Data Center Quantized Congestion Notification)作为拥塞控制算法。完整的DCQCN实现非常复杂,涉及ECN标记、CNP包生成、速率调整等多个环节。
在FPGA上,我建议先实现一个简化版:只做基于ECN的速率调整,不做精细的定时器和参数调优。具体来说,当收到带ECN标记的包时,把发送速率降低一半;当连续收到N个不带ECN标记的包时,把速率提高一点。这个逻辑用几十行RTL就能实现,效果虽然不如完整版DCQCN,但在实验室环境下足够用。
// 简化的速率调整逻辑 always @(posedge clk) begin if (ecn_received) begin current_rate <= current_rate >> 1; // 减半 rate_decrease_timer <= DECREASE_INTERVAL; end else if (rate_decrease_timer == 0) begin current_rate <= current_rate + RATE_STEP; // 缓慢增加 end end5. 上板调试:从链路up到数据通
5.1 第一步:确认物理链路状态
板子上电后,第一件事是读CMAC/Subsystem的状态寄存器。重点看这几个位:
stat_rx_aligned:通道对齐完成stat_rx_status:接收链路正常stat_tx_status:发送链路正常stat_rx_remote_fault:远端故障
如果stat_rx_aligned一直是0,说明PCS层没对齐。常见原因是光模块不兼容或者参考时钟频率不对。QSFP28模块需要156.25MHz的参考时钟,如果你给了161.1328125MHz(这是某些协议用的),PLL就锁不上。
5.2 第二步:回环测试
链路up之后,先做内部回环:把CMAC的TX AXI4-Stream直接接到RX AXI4-Stream,发一个包看能不能收到。这个测试可以排除MAC层以上的问题。
如果内部回环不通,检查AXI4-Stream的tvalid和tready握手逻辑。100G Subsystem的AXI4-Stream接口有一个特点:tready信号在复位后需要几个周期才能拉高,如果你在tready为低时就发数据,包会丢。
内部回环通了之后,做外部回环:用一根光纤把QSFP28的TX和RX连起来(或者用光模块的自环模式)。这个测试验证PCS/PMA和光模块是否正常。
5.3 第三步:PCIe枚举和DMA测试
PCIe部分,先在Linux下用lspci确认设备被枚举。如果看不到设备,检查:
- FPGA的PCIe复位信号是否正确
- 参考时钟是否稳定
- BAR空间是否配置正确
设备枚举成功后,用QDMA的测试工具(比如qdma_test)做简单的DMA读写。先测试寄存器读写,再测试小数据量DMA(比如4KB),最后测试大数据量(1MB以上)。
注意:QDMA的C2H(Card to Host)和H2C(Host to Card)通道是独立的,测试时要分别验证。我遇到过C2H正常但H2C不通的情况,最后发现是H2C的描述符环地址没有正确写入。
5.4 第四步:端到端RDMA测试
当以太网链路和PCIe DMA都通了之后,就可以做端到端测试了。最简单的场景是:两台机器各插一块FPGA卡,通过QSFP28直连,一台做发送端,一台做接收端。
发送端的流程是:应用层写数据到内存 → 驱动填充描述符 → QDMA从内存读数据 → 数据经过RDMA传输层封装 → CMAC发送到光纤。
接收端的流程相反:CMAC收到包 → RDMA传输层解封装 → QDMA写入内存 → 驱动通知应用层。
这个过程中,最容易出问题的是地址翻译。RDMA需要把虚拟地址翻译成物理地址,如果翻译错了,数据会写到错误的内存位置,导致程序崩溃或者数据损坏。建议在驱动里加一个地址校验逻辑,确保每个描述符的地址都在合法范围内。
6. 几个让我熬夜的坑和最终解决方案
6.1 坑一:CMAC的FCS字段被重复计算
CMAC IP核默认会自动计算并插入FCS(帧校验序列)。但如果你的AXI4-Stream数据里已经包含了FCS(比如从另一个MAC转发过来的包),就会出现"双重FCS"的问题,接收端会认为所有包都CRC错误。
解决方案:在CMAC配置界面里,把"Include FCS"选项根据实际情况设置。如果是自己生成包,勾选;如果是转发已有包,不勾选。我当初就是没注意这个选项,调试了两天才发现。
6.2 坑二:QDMA的MSI-X中断不触发
QDMA支持MSI-X中断,每个队列可以配置独立的中断向量。但MSI-X的配置涉及PCIe配置空间的修改,如果BAR空间映射不对,中断就不会触发。
我的解决步骤是:先用lspci -vv查看设备的MSI-X Capability结构,确认中断向量表的位置和大小。然后在驱动里用pci_alloc_irq_vectors申请中断,最后在QDMA的配置寄存器里使能对应的中断向量。
如果中断还是不触发,检查FPGA端的cfg_interrupt信号是否正确连接。这个信号在PCIe IP核的配置界面里有一个"Interrupt Pin"选项,必须设置为INTA。
6.3 坑三:100G线速下的时序收敛
390MHz的时钟域在UltraScale+上做时序收敛,需要一些技巧:
- 用Pipeline打拍,把组合逻辑切碎
- 用BRAM代替分布式RAM,减少LUT压力
- 用IDELAY/ODELAY调整IO时序
- 在Vivado里开Performance_Explore策略
我最终把用户逻辑的时钟降到了250MHz,用异步FIFO做时钟域 crossing,时序一下就干净了。虽然增加了一点延迟,但稳定性提升了很多。
6.4 坑四:光模块兼容性
不是所有QSFP28模块都能在FPGA上正常工作。有些模块的I2C初始化序列和FPGA的I2C控制器不兼容,导致模块无法进入工作状态。
我的经验是:优先用Finisar(现在叫Coherent)或InnoLight的模块,这两个品牌的兼容性最好。如果模块不工作,先用I2C读取模块的ID信息,确认模块类型和速率是否匹配。有些模块需要特定的初始化序列,比如设置均衡器参数,这些可以通过I2C写入。
7. 性能调优:从能跑到跑得快
7.1 包处理流水线
100G线速下,每个时钟周期(390MHz)要处理一个最小包。如果包处理逻辑有10级流水线,那么每个包的延迟是10个周期,约25.6纳秒。这个延迟对于RDMA来说是可以接受的,但流水线不能有气泡。
我的做法是:把包处理分成解析、查找、修改、转发四个阶段,每个阶段用独立的流水线寄存器。解析阶段提取包头信息,查找阶段查路由表或连接表,修改阶段更新序列号和校验和,转发阶段输出到CMAC。
7.2 缓冲区管理
RDMA需要缓存未确认的包,以便重传。缓冲区的大小决定了最大窗口。在100G下,如果RTT是10微秒,那么BDP(带宽延迟积)是12.5GB/s × 10μs = 125KB。也就是说,至少需要125KB的缓冲区才能填满管道。
UltraScale+的BRAM资源有限,125KB大约需要30个36Kb BRAM。如果BRAM不够,可以用DDR4作为溢出缓冲区,但DDR4的访问延迟比BRAM高很多,需要仔细设计缓存策略。
7.3 中断合并
QDMA的中断如果每个包都触发一次,CPU会被中断淹没。100G下每秒1.5亿个包,即使每个中断只花100纳秒,CPU也处理不过来。
解决方案是中断合并:设置一个阈值,比如每收到64个包或者每100微秒触发一次中断。QDMA支持这种配置,在驱动里设置coalesce参数即可。
8. 写在最后的一些个人体会
这套方案我从零开始搭了大约三个月,其中调试的时间占了三分之二。最大的体会是:FPGA上的高速网络设计,难点不在RTL代码,而在IP核的配置和调试。Xilinx的文档虽然详细,但很多关键信息散落在不同的PG(Product Guide)和AR(Answer Record)里,需要花时间整理。
另外,仿真和上板的差距非常大。仿真里跑通的逻辑,上板后可能因为时序、信号完整性、光模块兼容性等问题完全跑不起来。所以我的建议是:尽早做上板测试,不要等仿真完全通过再上板。哪怕只是点个灯,也能验证时钟和复位是否正确。
最后,如果你也在做类似的项目,建议加入Xilinx的官方论坛或者相关的技术社区。很多坑别人已经踩过了,搜一下就能找到答案。自己硬扛虽然也能解决,但时间成本太高。