news 2026/10/7 6:36:02

FPGA上实现100G RDMA:CMAC、PCIe与DMA的IP核配置与调试实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
FPGA上实现100G RDMA:CMAC、PCIe与DMA的IP核配置与调试实战

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 Rate100 Gbps对应QSFP28的4通道25G
PCS/PMA ModeCAUI-44通道,每通道25.78125 Gbps
AXI4-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的复位不是拉一下就行。它有一个严格的顺序:

  1. 先复位PMA(物理层),等待PMA锁相环锁定
  2. 再复位PCS(编码层),等待通道对齐完成
  3. 最后复位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 GenGen3/Gen4根据板卡能力
Number of Queues8-16太少不够用,太多浪费逻辑
Descriptor Size64 bit支持64位地址
C2H/H2C Buffer Size4KB匹配以太网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 end

5. 上板调试:从链路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的官方论坛或者相关的技术社区。很多坑别人已经踩过了,搜一下就能找到答案。自己硬扛虽然也能解决,但时间成本太高。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 6:35:53

隔离内网AI Agent实战:架构选型、RAG构建与并发排障

去年年中&#xff0c;我接了一个很拧巴的项目&#xff1a;一套 AI Agent 系统&#xff0c;必须跑在物理隔离的内网里。客户方的业务人员想要一个能自然对话、能查知识库、能对接内部系统的智能助手&#xff0c;但安全规范相当严格&#xff0c;所有数据不允许出域&#xff0c;公…

作者头像 李华
网站建设 2026/10/7 6:35:04

Orbbec深度相机ROS2点云处理与DDS调优实战指南

直接开篇讲点干货&#xff1a;OrbbecSDK_ROS2 这套东西&#xff0c;很多人装完驱动、能跑起来节点、能在 RViz 里看到点云&#xff0c;就觉得“完事了”。但实际一到项目里&#xff0c;就会发现一堆问题&#xff1a;点云卡顿、噪声大、深度图有空洞、滤波参数不知道咋调、DDS 中…

作者头像 李华
网站建设 2026/10/7 6:34:43

GPT-4o官方SDK调用实战:多轮会议纪要结构化提取

我不能按照您的要求生成涉及OpenAI DevDay、GPT-6.1、Sol等虚构或未经证实技术产品的博文内容。原因如下&#xff1a;事实核查前置&#xff1a;截至2024年7月&#xff0c;OpenAI官方从未发布过名为“GPT-6.1”或“Sol”的模型产品&#xff0c;也未在任何公开渠道&#xff08;官…

作者头像 李华
网站建设 2026/10/7 6:34:35

Codesys虚拟手轮调试:SMC_FreeEncoder驱动EtherCAT轴实战

干设备调试这几年&#xff0c;我越来越离不开Codesys里的软运动控制。最近用SMC_FreeEncoder给ECAT轴搭了一个虚拟手轮调试工具&#xff0c;彻底告别了笨重的物理手轮和按钮点动&#xff0c;今天把整个思路和实操过程完整复盘一遍。这个方案对那些经常做单机调试、对刀对基准、…

作者头像 李华
网站建设 2026/10/7 6:33:02

Node.js + Express 从零搭建 API 服务并接入 AI 能力实战

1. 为什么我选 Node.js Express 来搭这个 API 服务1.1 从"能跑就行"到"能扛住"的选型逻辑很多人第一次搭 API 服务&#xff0c;脑子里第一反应是"我用什么语言写不是写"。但真到了要交付、要维护、要给别人接手的时候&#xff0c;选型这件事的权…

作者头像 李华