
1. 为什么要在FPGA上折腾100G RDMA先把结论摆在前面在FPGA上实现100G RDMA核心难点从来不是写代码而是把Xilinx的CMAC、PCIe、DMA这几个IP核的时钟域、复位域、数据位宽对齐。我见过太多人卡在链路起来了但数据不通或者能收不能发的状态最后发现是某个IP核的复位极性搞反了。这套方案适合谁适合已经做过千兆以太网、对AXI4-Stream协议有基本概念、手里有Alveo或类似加速卡或者自研板卡搭载UltraScale系列FPGA的工程师。如果你连AXI-Lite和AXI-Stream的区别都还没搞清楚建议先补一下基础否则后面每一步都是坑。RDMARemote Direct Memory Access的本质是让网卡直接读写远端内存绕过CPU和内核协议栈。在FPGA上做这件事意味着你要自己实现一个网卡——从物理层的SerDes到MAC层再到传输层的可靠传输逻辑全部用RTL或者IP核搭出来。100G的线速意味着每秒要处理约1.5亿个最小包每个时钟周期都不能浪费。Xilinx现在叫AMD提供了一套相对完整的IP生态CMAC100G以太网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一个是CMAC100G 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 Rate100 Gbps对应QSFP28的4通道25GPCS/PMA ModeCAUI-44通道每通道25.78125 GbpsAXI4-Stream Data Width512 bit对应64字节/周期的处理能力Clock Frequency390.625 MHz512bit × 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 1b1; state WAIT_PMA; end WAIT_PMA: begin pma_reset 1b0; 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 x8100G网络的数据要落到主机内存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或x16AXI Data Width建议512bit和DMA匹配Reference Clock Frequency100MHz或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支持MMMemory Mapped和STStreaming两种模式ST模式可以直接和CMAC的AXI4-Stream对接省掉一层转换逻辑。配置QDMA时这几个参数需要特别注意参数推荐值原因PCIe GenGen3/Gen4根据板卡能力Number of Queues8-16太少不够用太多浪费逻辑Descriptor Size64 bit支持64位地址C2H/H2C Buffer Size4KB匹配以太网MTUMSI-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 EthernetRoCE的传输层需要实现可靠传输。在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使用DCQCNData 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的C2HCard to Host和H2CHost 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的文档虽然详细但很多关键信息散落在不同的PGProduct Guide和ARAnswer Record里需要花时间整理。另外仿真和上板的差距非常大。仿真里跑通的逻辑上板后可能因为时序、信号完整性、光模块兼容性等问题完全跑不起来。所以我的建议是尽早做上板测试不要等仿真完全通过再上板。哪怕只是点个灯也能验证时钟和复位是否正确。最后如果你也在做类似的项目建议加入Xilinx的官方论坛或者相关的技术社区。很多坑别人已经踩过了搜一下就能找到答案。自己硬扛虽然也能解决但时间成本太高。