拿到紫光同创PG2L100H这块板子之后,我第一个想法不是点亮LED,而是把它做成一张真正能塞进服务器、走PCIe又能出光的加速卡。原因很简单:FPGA做高速光纤通信其实已经很常见,但国产FPGA里能同时把PCIe硬核和高速收发器做齐,还愿意把开发板做成标准卡形态的,确实不多。这篇内容就完整记录我跑通“PCIe主机 -> PG2L100H -> SFP+光模块 -> 光纤 -> 对端FPGA”这条链路的全过程,包括PCIe的LTSSM状态机怎么调、枚举过程怎么排错、光纤链路怎么配,以及中间踩过的各种坑。适合手里有同款板子、或者正准备选国产FPGA做光通信/PCIe加速卡的同学参考。
1. 板卡和芯片摸底:PG2L100H到底适合干什么
1.1 从型号看芯片定位
PG2L100H这个型号,按我对紫光同创产品线的理解,可以拆开看:PG开头是Pango系列,2代产品,L代表逻辑型器件,100说明逻辑规模在中等档位,H后缀大概率对应带高速收发器的版本。这颗芯片最值钱的地方不在于逻辑资源有多少,而在于它把PCIe硬核和SerDes高速收发器集成到了一起。
实际搞过FPGA开发的人都知道,逻辑资源再多,要是没有高速串行收发器,想接SFP+光模块就是一句空话。以前国产FPGA做光通信,要么外接PHY芯片,要么用并行LVDS硬凑,带宽上不去,PCB布线还特别痛苦。PG2L100H这类器件出来之后,情况完全不一样了——单芯片就能覆盖万兆光模块的收发需求,而且自带PCIe硬核,数据进了FPGA之后可以直接通过PCIe上主机,不需要中间再挂一颗PCIe转接芯片。
1.2 高速收发器和PCIe硬核资源盘点
我手上这块板子的核心资源大致是这几个方面:
高速收发器(SerDes)通道:芯片集成了多路高速收发器,标称单通道速率覆盖1G到10G档位。这个速率范围很关键,因为它正好覆盖10G光模块的线速率10.3125Gbps,也覆盖常用的PCIe Gen2/Gen3速率。如果你用过Xilinx的GTX/GTH,这套东西的定位是类似的:核心由PMA(物理介质接入层)和PCS(物理编码子层)组成,内部包含串化器/解串器、CDR时钟恢复、均衡器、编解码和弹性缓冲。
PCIe硬核:支持Endpoint和Root Complex两种工作模式,常见配置x1/x2/x4/x8,协议版本支持到Gen2/Gen3。做PCIe采集卡、加速卡时,把它配置成Endpoint插到服务器主板上;做特定背板交换场景时,也可以配置成Root Complex去管理下游设备。
DDR控制器:和PCIe配合起来,FPGA可以把光纤进来的数据先缓存到DDR里,再通过DMA批量搬到主机内存,避免零碎小包频繁打断CPU。
普通逻辑资源:中等规模,做协议解析、DMA描述符管理、数据包处理足够用了。如果做复杂的网络卸载引擎,资源会偏紧,但做一版能用的原型验证完全没问题。
1.3 开发板的硬件形态和外设观察
开发板本身的设计更接近一张标准半高半长PCIe卡,而不是那种面包板式的评估板。这是我很满意的一点。板上的关键外设大概包括:
- SFP+光模块笼子,支持10G光模块或DAC高速线缆。
- PCIe金手指,x4/x8物理接口,能从PCIe插槽取电并和主机通信。
- DDR内存颗粒,FPGA侧通过硬核控制器访问。
- JTAG下载口、UART调试口、LED按键等常规外设。
- 时钟系统,板上带有PCIe 100MHz参考时钟源和高速收发器参考时钟晶振。
这里我特别提醒一句:拿到板子先别急着上电调光纤,先把板子插到电脑PCIe槽里,什么都不接,确认BIOS能通过PCIe枚举认出这个设备。这一步通过之后,再谈光纤链路。否则后面出了问题,很难分清是PCIe配置的问题还是光路的问题。
2. 光纤链路的第一步:光模块、参考时钟和信号完整性
2.1 光模块选型和接口定义
光模块选择上,大家常用的就是SFP+封装的10G模块。多模短距离场景选SR模块,配合850nm多模光纤,几百米内没问题;单模长距离选LR模块,配合1310nm单模光纤,可以到10公里。实际开发调试,我建议优先用多模SR模块加短跳线,成本低,坏了换起来也不心疼。
SFP+模块的接口逻辑其实不复杂,核心是两组高速差分信号:
- 发送差分对,从FPGA高速收发器的TX端连接到光模块的发送引脚。
- 接收差分对,从光模块接收引脚连接到FPGA高速收发器的RX端。
除了高速数据线,还有I2C管理接口用来读模块的EEPROM信息(型号、光功率、温度等)、LOS信号用来指示接收光功率是否达标、TX_DISABLE用来关闭发射。上板调试时,I2C读取模块光功率这个功能非常有用。如果LOS一直拉高,多半是光纤没插好或者对端没有发光,链路没建立。
2.2 参考时钟设计:高速收发器和PCIe对时钟的要求完全不同
很多第一次做高速光通信的人会栽在参考时钟上。PCIe接口和高速收发器用的参考时钟是两套东西,不能混用。
PCIe的参考时钟是100MHz差分时钟,用来训练链路并作为收发器的位宽转换基准。它的抖动指标要求非常严格,板上通常用专用PCIe时钟芯片产生。
高速收发器做光纤通信时,参考时钟频率取决于你想跑的线速率和编码方式。例如,如果跑10G Base-R这种应用,收发器内部PLL锁定在156.25MHz的参考时钟上,经过内部倍频和分频之后,产生10.3125Gbps的串行比特率。如果跑XAUI或者Aurora,可能用156.25MHz或125MHz。
这里有个容易忽略的细节:PCIe的100MHz参考时钟和光纤用的收发器参考时钟,必须在FPGA内部走不同的时钟引脚,分别接到对应的IP核时钟输入端,不能拿PCIe的100MHz去做光纤收发器的参考时钟,也不能反过来。两者的频点和抖动要求完全不同,硬凑的话会导致CDR锁定失败,错误率居高不下。
2.3 高速链路的信号完整性检查清单
如果是用厂商提供的官方开发板,PCB布线已经帮你做好了,但上板前还是建议做一轮检查:
- 光模块的TX/RX差分对是否连接到FPGA高速收发器的专用引脚,而不是普通IO。
- 高速差分信号路径上是否有AC耦合电容。SFP+模块的相关标准要求在链路中放置AC耦合电容,很多开发板上已经集成。
- 差分阻抗是否匹配,SFP+的高速信号线一般按100欧差分阻抗设计。
- 空闲未使用的收发器通道是否做了合理处理,不要悬空乱接,避免引入噪声。
有人会问:这些检查做完之后,怎么验证物理层通不通?答案是先跑误码率测试(IBERT),而不是直接跑上层协议。用工具链里的高速收发器误码测试功能,把收发器配置成回环模式,TX发出伪随机序列,RX接收并比对。如果误码率在1e-12以下,说明物理层是健康的,这时再往上跑协议就有底气了。
3. PCIe配置实战:从IP例化到LTSSM进入L0
3.1 PCIe IP的生成与例化
在紫光同创的开发环境里新建工程、选好PG2L100H芯片之后,第一件事就是生成PCIe IP。我用的配置大致是:Endpoint模式,x4通道,Gen2速率,使能MSI中断,使能两个32位BAR空间(一个用作寄存器控制,一个用作数据搬运)。如果不太清楚自己需要几个BAR,先按这个配置来,后面按需调整。
IP生成之后,需要把关键端口例化到顶层模块:
- 参考时钟输入引脚,接板上100MHz差分时钟。
- 复位信号,接PCIe的PERST复位,或者用逻辑控制一个上电复位。
- 收发器差分对,PCIe TX/RX lane分别接金手指对应的差分引脚。
- AXI接口,PCIe IP内部通常把事务层转换成AXI或类似的内部总线接口,用于和用户逻辑交互。
这里有个新手容易忽略的问题:PCIe IP的复位时序和时钟稳定性。PCIe要求在参考时钟稳定之后、再释放复位,而且复位释放后要等待一段时间,让内部PLL锁定。如果复位时序做乱了,LTSSM状态机会一直卡在Detect状态环回,枚举永远过不了。
3.2 LTSSM状态机:排查枚举失败的关键地图
PCIe链路训练状态机(LTSSM)是理解PCIe排错最核心的知识点,没有之一。每次枚举失败,第一步就是查当前LTSSM状态停在哪个阶段。
LTSSM的主要状态包括:
- Detect:检测对端是否有设备,链路两端是否都准备好。
- Polling:发送训练序列TS1/TS2,尝试建立位同步和块同步。
- Configuration:协商最终链路宽度和速率,处理lane翻转。
- L0:正常工作状态,可以传输TLP报文。
- Recovery、L1、L2等:链路恢复和低功耗状态。
对调试来说,最关键的是Polling和Configuration这两个阶段的子状态。简单理解:Polling阶段在解决“我能不能和对面说话”,Configuration阶段在解决“我们用几根线、什么速度说话”。如果卡在Polling,大概率是物理层问题——参考时钟没起振、差分线接反、光模块没插好(如果是通过光模块做PCIe链路,这种情况也常见)。如果卡在Configuration,通常是通道数和速率协商不一致,或者lane翻转到确认处理有问题。
我习惯的做法是,在FPGA内部把LTSSM状态寄存器通过硬件调试工具实时抓出来,然后和主机端lspci的结果对照看。主机BIOS开始枚举时,状态机会从Detect一路跑到L0,整个过程在毫秒级。如果没进L0,看它停在哪个状态,排错范围一下就缩小了。
3.3 枚举过程:BIOS/操作系统如何发现你的FPGA卡
PCIe枚举时,主机会做以下几件事:读取设备厂商ID和设备ID,确认设备类型;读取Class Code,判断设备属于什么类型(网络控制器、存储控制器、还是处理加速器);给每个BAR空间分配物理地址;分配中断资源。
PG2L100H的PCIe硬核在IP配置时可以指定厂商ID和设备ID。如果用的是默认配置,可能需要改成自己的值,否则lspci显示出来是一个陌生ID。改完重新生成bit文件,下载之后重新扫描PCIe总线,就能看到设备了。
Linux下常用命令:lspci -tv看设备树,lspci -vvv看设备能力集和当前链路状态。看到LinkSta里的LnkSta字段显示Speed 5GT/s、Width x4,说明链路已经跑到Gen2 x4。如果Speed显示2.5GT/s,说明协商到了Gen1,通常是因为对端或板卡只支持Gen1,或者训练时无法稳定跑Gen2。这时候可以在IP配置里强制只跑Gen1,先让链路稳定,再逐步提高。
3.4 用DMA搬数据:链路通了只是开始
PCIe链路起来之后,还只是拿到了一个能通信的管道。真正干活靠的是DMA。PG2L100H这套环境里,官方提供了类似Xilinx XDMA的DMA控制器方案,支持描述符环形队列、MSI中断、多通道DMA等。如果你之前用过Xilinx的XDMA,这套设计思路几乎是平移过来的。
核心流程是这样的:主机的驱动在内存里申请一块缓冲区,把物理地址和长度填入描述符;FPGA侧DMA控制器从描述符队列里读取任务,发起内存读或写;完成后通过MSI中断通知主机。
跑通之后,你可以在Linux下对/dev设备做读写,数据会从主机内存搬进FPGA、再从FPGA光纤口出去,反过来也行。至此,一个真正的PCIe光纤加速卡雏形就出来了。
4. 光纤通信协议栈:用现成IP还是自己写协议
4.1 优先用官方高速串行协议IP
光纤链路物理层调通之后,接下来要决定数据怎么在链路上封装。如果你的应用不需要和别的设备互通,只是为了两块FPGA之间高速传数据,最省事的是用官方提供的传输层参考设计,类似Xilinx Aurora那样的协议,支持64B/66B编码、通道绑定、流控、热插拔等。
用现成协议的优势很明显:对齐、时钟补偿、差错处理这些底层细节都已经处理好了,你只需要关心如何把用户数据包从A点搬到B点。我第一次做光通信时,就是自己写对齐和时钟补偿,结果在长时间大数据量测试时频繁出现通道失步,排查了很久才发现是对端芯片时钟频率有微小偏差导致缓冲区溢出。后续的协议栈开发我没再重复造轮子的习惯,能拿现成IP解决的问题,坚决不自己写底层。
4.2 自定义简化协议的帧格式设计
如果因为特殊需求必须自己定义协议(例如你要传输非标准数据格式,或者要做极低延迟的数据通路),建议遵循几个设计原则:
- 帧头要有固定特征字段,用于接收端的帧同步识别。
- 净荷长度要明确写在帧头里,接收端才能知道一帧数据在什么位置结束。
- 帧尾放CRC校验,至少32位CRC,防止数据出错被上层静默吞掉。
- 空闲帧(IDLE)要周期性发送,一方面维持链路同步状态,另一方面做时钟补偿。
我常用的简化帧长是64字节到1024字节可变,帧头用4字节0xAAAAAAAA加2字节长度加2字节序号,帧尾4字节CRC。接收端先用帧头完成同步,然后按长度字段提取净荷,做CRC校验,再送入后续FIFO。这个方案在调试时非常直观,逻辑分析仪一抓就知道哪一步出问题。
4.3 跨时钟域的坑:为什么不能直接把数据塞给收发器
自定义协议最容易翻车的点是跨时钟域处理。高速收发器发送端和接收端各自有独立的用户时钟,发送端时钟来自本地PLL,接收端时钟是接收数据恢复出来的时钟。两块板卡之间不存在共同时钟源,所以两边数据速率会有微小的频率偏差。
如果不做处理,长时间传输后接收FIFO必然溢出或下溢,表现为链路偶尔丢包、错误突发。处理方案有两个:一是用弹性缓冲配合IDLE字符做时钟补偿,发送端每隔一定时间插入一个可删除的IDLE空字符,接收端检测到缓冲区水位偏高时删除一个,水位偏低时补一个;二是在更上层做基于帧的重传机制,但那是万不得已的办法。
我自己的项目里是两者配合:物理层弹性缓冲解决比特级偏差,上层协议用序号和超时重传解决偶然丢帧。单纯依赖某一层,长期跑都不太稳妥。
5. 整链路联调:Linux下识别设备到满带宽传输
5.1 Ubuntu下让系统识别FPGA设备
板卡插上主机后,开机到Ubuntu系统,先用lspci确认设备存在:
lspci -tv lspci -vvv -s 01:00.0如果系统没有识别到设备,先确认PCIe物理链路是否稳定。最常见的两个原因:一是金手指没插到位,二是PCIe参考时钟没起振。这两点排查完之后再考虑FPGA内部逻辑问题。
系统识别之后,可以选择用UIO、VFIO或者官方驱动做后续开发。我调试阶段最喜欢用UIO框架:内核加载uio_pci_generic模块,把设备绑定到UIO驱动,用户态直接mmap BAR空间,读写寄存器非常方便。VFIO功能更完整但配置复杂,适合正式驱动开发时再用。
5.2 驱动层发起DMA读写
用UIO框架时,用户态程序可以这样操作:
- 打开设备文件,mmap映射BAR空间,读取寄存器。
- 申请一块物理内存对齐的内存,获取物理地址。
- 把物理地址和长度写入DMA描述符寄存器。
- 启动DMA,等待中断或轮询完成标志。
实际测试中,我会在主机侧准备一块64KB的缓冲区,填入递增序号;下发到FPGA后,FPGA接收完原样回传,主机校验数据是否一致。这个简单回环测试能同时验证光纤收发链路和PCIe DMA链路,排查问题非常好用。
5.3 带宽实测和瓶颈分析
链路全通之后,我做了实际的带宽测试。PCIe Gen2 x4的理论带宽是2GB/s,而万兆光纤的有效数据速率大约在1.25GB/s(考虑编码开销后更低)。实测下来,整条链路能稳定跑到900MB/s到1.1GB/s,接近万兆上限。
瓶颈主要在光链路这一侧,PCIe带宽是富余的。如果以后要上25G光模块,就需要升级PCIe到Gen3 x8或者更宽,否则PCIe反而会成为瓶颈。这点在做方案选型时要想清楚,别光纤速率提上去了,PCIe通道反而卡脖子。
6. 调试工具链和我的排错经验
6.1 工具链自带的在线逻辑分析仪
国产FPGA工具链和Xilinx的Vivado相比,生态和成熟度肯定有差距,但基本调试工具还是齐全的。PDS里的在线逻辑分析仪类似ChipScope/ILA,可以在FPGA内部预设探针信号,触发条件设置命中之后,把波形抓出来看。
我的调试流程一般是:先抓收发器侧的关键信号,确认对齐完成、CRC错误计数不涨;再抓用户接口侧信号,确认数据有效信号和数据是否对齐;最后抓DMA描述符状态,确认PCIe侧任务是否正常完成。一层一层往上查,绝不跳步。
6.2 几个让我记忆深刻的排错过程
第一个坑是PCIe枚举失败。第一次上板时,系统完全识别不到设备。我先用硬件调试工具看LTSSM状态,发现一直停在Detect状态。检查参考时钟,发现板上PCIe时钟芯片没有输出,原来是时钟芯片的使能引脚没有接对,默认拉低导致时钟不工作。改完时钟配置后,状态机正常跑到了L0。
第二个坑是光纤链路偶尔丢包。物理层误码率测试是过的,但上层协议一跑大数据就丢包。后来用逻辑分析仪抓接收侧数据,发现是接收FIFO溢出——链路速率匹配没做好。我在发送端加入了以IDLE字符为基础的时钟补偿机制后,问题消失。
第三个坑是DMA描述符内存地址的不对齐问题。CPU分配的内存如果没做物理连续和地址对齐,描述符里的地址跨页就会导致DMA读到错误数据。解决方法是在驱动里用内核API分配物理连续、按页对齐的内存。
6.3 一些通用经验总结
- 上电顺序很重要。PCIe设备要求先有参考时钟,再释放PERST复位,复位释放后不能立刻请求配置访问,要等内部初始化完成。
- 做光模块调试时,先用模块自环和短光纤,排除光纤本身的问题,再连远距离设备。
- DDR控制器初始化时序要严格按照手册来,初始化失败会导致DMA搬运数据时随机出错。
- 国产FPGA工具链和Xilinx有差异,遇到编译报错多看看官方文档,很多是工程配置问题而不是代码问题。
- 跨时钟域是高速系统最大的坑,所有跨接的地方都要用异步FIFO或握手处理,不要抱有侥幸心理。
在我实际做完一版PCIe光纤卡之后,最大的体会是:这个项目的难点不在于某个单一模块,而在于PCIe、光纤、DDR、DMA这些子系统之间的协同。每个子系统单独验证都正常,但串在一起时,任何一个环节的时序、复位和时钟处理不到位,整条链路就会出各种奇怪问题。调试时一定要按照物理层、链路层、事务层一层一层往上验证,不要直接跳到上层应用去找原因。先把LTSSM跑进L0,再把误码率测到1e-12以下,再跑DMA回环,最后才谈性能和业务逻辑。这个顺序走下来,所有问题都会变得可控。