1. 项目缘起与整体设计思路
1.1 为什么选择SFP光口而不是RJ45电口
做过工业现场数据采集或者多板卡互联的朋友应该都有体会,当传输距离超过30米、电磁环境又比较恶劣的时候,RJ45电口就开始力不从心了。我之前做一个多通道同步采集的项目,八块板卡分布在两个机柜里,最远的两块板子之间走线大概40米,用电口跑千兆的时候丢包率肉眼可见地往上飙,示波器一挂全是共模噪声。后来换成SFP光口方案,同样的拓扑、同样的数据量,误码率直接降到可以忽略不计的水平。
SFP光口以太网传输的核心价值在于三点:电气隔离、长距离低损耗、抗电磁干扰。光纤本身是介质,两端天然隔离,不存在地环路问题;多模光纤在千兆速率下可以稳定跑550米,单模更是能到10公里以上;而且光信号不受电机、变频器、继电器这些工业现场常见干扰源的影响。对于FPGA开发者来说,SFP接口的另一个好处是它本质上就是一个高速串行收发器通道,和FPGA内部的GTP/GTX/GTH收发器直接对接,不需要额外的PHY芯片做并串转换,链路简洁、延迟确定。
这个项目要做的,就是在一块带有SFP笼子的FPGA板卡上,实现完整的1000BASE-X光口以太网传输链路,包括物理层编码、MAC层收发、以及上层的数据打包与校验。整个设计在Vivado环境下完成,目标器件可以是Xilinx 7系列及以上的FPGA。
1.2 1000BASE-X协议栈的层次拆解
很多刚接触光口以太网的朋友容易把1000BASE-X和1000BASE-T搞混。简单说,1000BASE-T是跑在双绞线上的,需要复杂的回声消除和均衡;1000BASE-X是跑在光纤上的,物理层干净得多。它的协议栈从下往上大致是这样的:
- PMD层(物理介质相关):负责光电转换,SFP模块干的就是这个活。FPGA这边输出的是高速差分电信号,SFP模块把它转成光信号发出去,接收方向反过来。
- PCS层(物理编码子层):这是FPGA内部逻辑要处理的核心部分。1000BASE-X使用8B/10B编码,把8位数据映射成10位码字,保证直流平衡和足够的跳变沿供时钟恢复使用。同时PCS层还负责 comma字符检测、码组同步、通道绑定等。
- PMA层(物理介质附加):串并转换、时钟恢复、预加重和均衡。这部分通常由FPGA内部的硬核收发器完成。
- MAC层:以太网帧的组装与解析,包括前导码、SFD、目的地址、源地址、长度/类型、数据和FCS校验。
在Vivado里,Xilinx提供了两种主要路径来实现这个链路:一种是用1G/2.5G Ethernet PCS/PMA or SGMII IP核,它把PCS和PMA都封装好了,你只需要接上SFP的差分对和时钟,配置成1000BASE-X模式即可;另一种是直接用GTP/GTX收发器 + 自己写8B/10B编解码和MAC逻辑,灵活性更高但工作量也大得多。对于绝大多数项目,我建议走IP核路线,稳定可靠,时序收敛也容易。
1.3 整体数据流与模块划分
这个设计的顶层数据流是这样的:上位机通过某种接口(比如UART或者PCIe)把待发送的数据交给FPGA,FPGA的MAC层把数据封装成以太网帧,加上前导码和FCS,然后送给PCS/PMA IP核做8B/10B编码和串化,最后通过SFP光模块发出去。接收方向完全对称:SFP收到光信号,转成差分电信号送给FPGA收发器,IP核完成解串和10B/8B解码,恢复出以太网帧,MAC层做FCS校验和地址过滤,有效数据存入FIFO供后续处理。
模块划分上,我习惯分成四个部分:光口物理层模块(IP核+时钟约束)、MAC收发模块(帧组装/解析+FCS)、数据缓冲模块(异步FIFO做时钟域 crossing)、用户接口模块(对接实际业务逻辑)。这样划分的好处是每个模块职责单一,仿真验证的时候可以单独拎出来测,出了问题也容易定位。
2. 硬件设计与引脚约束的关键细节
2.1 SFP接口引脚定义与板级连接
SFP笼子有20个引脚,但真正和FPGA打交道的核心信号就那么几组。先看发送方向:TD+和TD-是一对高速差分信号,从FPGA收发器的TX端出来,经过AC耦合电容(通常0.1uF)接到SFP的发送引脚。接收方向:RD+和RD-从SFP的接收引脚出来,同样经过AC耦合电容接到FPGA收发器的RX端。注意这里的AC耦合电容位置有讲究,一般放在靠近发送端的地方,接收端是否需要电容要看具体收发器要求,7系列GTP的RX端内部有耦合,外部通常不需要再加。
除了高速差分对,还有几个低速控制信号必须接:TX_DISABLE用来关闭发送,TX_FAULT指示发送故障,RX_LOS指示接收光信号丢失,MOD_ABS指示模块是否插入。这几个信号我一般直接接到FPGA的普通IO上,内部做上拉,通过逻辑读取状态。另外I2C接口(SCL/SDA)用来读取SFP模块内部的EEPROM信息,包括厂商、波长、传输距离等,调试阶段很有用,但产品里不一定需要。
注意:SFP的TD/RD差分对在PCB上必须做100欧姆差分阻抗控制,走线尽量短且等长,过孔数量控制在最少。我见过因为差分对走线不对称导致眼图闭合的案例,重新布线后问题消失。
2.2 收发器参考时钟与约束写法
7系列FPGA的GTP收发器需要外部提供参考时钟,频率取决于你选的线速率和内部PLL配置。对于1000BASE-X,线速率是1.25Gbps(8B/10B编码后),GTP的参考时钟通常选125MHz。这个时钟必须从专用的时钟引脚输入,不能随便找个普通IO。板上一般会放一个125MHz的差分晶振专门给收发器用。
在Vivado里,参考时钟的约束要写在XDC文件里,基本格式是:
create_clock -period 8.000 -name gt_refclk_p [get_ports gt_refclk_p] set_property LOC GTPE2_CHANNEL_X0Y1 [get_cells gt_wrapper_i/gtp_i] set_property LOC IBUFDS_GTE2_X0Y1 [get_cells gt_wrapper_i/ibufds_gt]如果你用的是IP核,Vivado会自动生成一部分约束,但参考时钟的周期约束和引脚位置约束还是得自己确认。我踩过的坑是:IP核生成的示例设计里参考时钟约束是200MHz,但我的板子实际是125MHz,综合不报错,实现也不报错,但上板就是不通。后来用ILA抓收发器的状态寄存器才发现PLL根本没锁。所以上板前一定核对参考时钟频率和约束是否一致。
2.3 时钟域分析与跨时钟处理
这个设计里至少有三个时钟域:收发器恢复时钟(从接收数据里恢复出来的125MHz)、用户时钟(IP核输出的125MHz或62.5MHz)、系统时钟(板上晶振提供的100MHz或200MHz)。数据从MAC层到用户逻辑,必然要跨时钟域。
我的做法是在MAC层和用户接口之间放一个异步FIFO,用IP核生成的独立时钟FIFO,写端用MAC时钟,读端用系统时钟。FIFO深度选512或1024,足够缓冲突发数据。这里有个细节:FIFO的写使能要结合MAC层的有效信号和FIFO满信号做反压,否则数据丢了都不知道。读端同理,空信号要参与用户逻辑的流控。
实操心得:跨时钟域的信号除了数据总线,还有valid、ready、last这些控制信号。我的习惯是控制信号也走FIFO,或者用双触发器同步加握手,绝对不要直接拿一个时钟域的组合逻辑输出去驱动另一个时钟域的触发器,亚稳态会让你调到怀疑人生。
3. Vivado工程搭建与IP核配置实战
3.1 工程创建与器件选型
打开Vivado,新建工程,选RTL Project,器件根据你手头的板子来。我用的是xc7a200tfbg484-2,属于Artix-7系列,带GTP收发器。选器件的时候注意看两点:有没有GTP/GTX收发器,收发器的参考时钟引脚是否引到了板子上的晶振。有些低端器件比如Spartan-6的某些型号只有GTP,速率上限低一些,但跑1.25Gbps没问题。
工程建好后,先把板级的XDC约束文件加进去,包括时钟、复位、SFP控制信号、差分对引脚位置。这些约束最好从板卡厂商的示例工程里抄,自己写容易漏。特别是差分对的LOC约束,写错了综合能过但实现会报错。
3.2 1G Ethernet PCS/PMA IP核配置要点
在IP Catalog里搜“1G Ethernet PCS/PMA”,双击打开配置界面。几个关键选项:
- Standard:选1000BASE-X,不要选SGMII,除非你接的是PHY芯片而不是光模块。
- Physical Interface:选GTP或GTX,取决于你的器件。
- Line Rate:1000BASE-X固定1.25Gbps,不用改。
- Reference Clock:选125MHz,和板上晶振一致。
- Include Shared Logic in core:如果这是你工程里唯一用到的收发器,选这个,IP核会把QPLL/CPLL和复位逻辑都包进来,省事。如果多个通道共享,就选另一个选项,把共享逻辑放外面。
配置完成后,IP核会生成一个示例设计(Example Design),强烈建议先生成示例设计跑一遍,确认硬件链路没问题,再往自己的工程里集成。示例设计里包含了完整的MAC层(用AXI Ethernet IP或者简单的FIFO接口),你可以直接用它来测试光口回环。
3.3 收发器复位与初始化流程
GTP收发器的复位不是简单拉一下就行,它有一套严格的时序要求。IP核会输出几个状态信号:tx_resetdone、rx_resetdone、pll_lock。正确的初始化顺序是:
- 等待参考时钟稳定(至少100us)。
- 释放PLL复位,等待pll_lock拉高。
- 释放TX复位,等待tx_resetdone拉高。
- 释放RX复位,等待rx_resetdone拉高。
- 此时链路才真正建立,可以开始收发数据。
我一般写一个简单的状态机来管理这个过程,每个状态加一个超时计数器,如果某个信号在规定时间内没拉高就报错。实测下来,从复位释放到链路建立大概需要几十毫秒,取决于参考时钟的稳定时间。
常见问题:如果rx_resetdone一直不拉高,先检查SFP模块有没有插好、光模块的TX_DISABLE有没有被误拉高、对端有没有在发送数据。1000BASE-X的接收方向需要收到对端的comma字符才能完成码组同步,如果对端没发数据,rx_resetdone是不会拉高的。
4. MAC层收发逻辑与FCS校验实现
4.1 以太网帧格式与发送状态机
以太网帧的格式是固定的:7字节前导码(0x55)+ 1字节SFD(0xD5)+ 6字节目的MAC + 6字节源MAC + 2字节长度/类型 + 46到1500字节数据 + 4字节FCS。发送的时候,前导码和SFD由PCS层自动加,MAC层只需要从目的MAC开始组装。
我的发送状态机是这样的:IDLE状态下等待用户数据有效,收到后先发目的MAC和源MAC(这两个可以从寄存器配置),然后发长度/类型,接着把用户数据流式发出去,同时把数据送给FCS计算模块。数据发完后,把FCS计算结果取反(以太网FCS是CRC32的补码)发出去,最后回到IDLE。
这里有个细节:如果用户数据不足46字节,需要补零到46字节,否则接收端会当成runt帧丢弃。我一般会在发送状态机里加一个计数器,数据发完后如果计数小于46,就继续发零直到满足最小帧长。
4.2 CRC32 FCS计算模块的写法
FCS用的是CRC32,多项式是0x04C11DB7,初始值全1,结果取反,字节序是LSB first。网上有很多现成的Verilog代码,但要注意字节序和位序的对应关系。我习惯用查表法或者逐位异或法,逐位法资源占用少但速度慢,查表法速度快但需要ROM。
对于千兆速率,125MHz时钟下每个时钟周期处理8位数据,逐位法肯定来不及。我的做法是用8位并行CRC,每个时钟周期算8位,具体实现是把CRC寄存器的高8位和输入数据异或,然后查表得到新的CRC值。这个表可以预先算好,用case语句或者ROM实现。
// 8位并行CRC32核心逻辑(简化示意) always @(posedge clk) begin if (crc_en) begin crc_reg <= next_crc; end end assign next_crc[31:24] = crc_reg[23:16] ^ crc_table[data_in ^ crc_reg[31:24]]; assign next_crc[23:16] = crc_reg[15:8] ^ crc_table[data_in ^ crc_reg[31:24]]; // ... 其余字节类似实测下来,8位并行CRC在125MHz下时序余量很足,Artix-7上跑200MHz都没问题。
4.3 接收方向:帧解析与地址过滤
接收方向比发送复杂一些,因为要处理各种异常情况。我的接收状态机流程是:等待SFD(0xD5),收到后开始接收目的MAC,和本机MAC寄存器比较,如果不匹配且不是广播地址就丢弃整帧;匹配的话继续接收源MAC、长度/类型,然后根据长度字段接收数据,同时算FCS。数据收完后,把算出来的FCS和帧尾的4字节比较,一致才把数据写入FIFO,否则丢弃并置错误标志。
这里有个坑:长度/类型字段的值如果大于1500,表示这是EtherType而不是长度,比如0x0800表示IPv4,0x0806表示ARP。我的做法是判断这个字段,如果大于1536就当成EtherType处理,数据长度由帧尾的FCS位置决定,而不是由这个字段决定。这个逻辑在纯自定义协议里可以简化,但如果你要和标准以太网设备互通,就必须处理。
避坑技巧:接收状态机一定要有超时机制。如果链路中途断开或者对端发了畸形帧,状态机可能卡在某个状态出不来。我一般给每个状态加一个计数器,超过一定周期没进展就强制回到IDLE,同时置一个错误标志供上层查询。
5. 上板调试与常见问题排查实录
5.1 ILA抓波形:链路建立过程可视化
Vivado的ILA(Integrated Logic Analyzer)是调试光口链路的利器。我一般会在以下几个信号上挂ILA:pll_lock、tx_resetdone、rx_resetdone、rx_los、tx_fault、以及MAC层的rx_valid和rx_data。触发条件设成rx_resetdone的上升沿,这样能抓到链路建立的完整过程。
实测下来,正常的链路建立波形是这样的:pll_lock先拉高,大约几十个时钟周期后tx_resetdone拉高,再过一段时间rx_resetdone拉高。如果rx_resetdone一直不拉高,看rx_los是不是高,如果是高说明没收到光信号,检查光纤有没有插好、对端有没有发数据。如果rx_los是低但rx_resetdone不拉高,可能是comma字符检测有问题,检查参考时钟频率和IP核配置。
5.2 常见报错与解决方法速查表
| 报错/现象 | 可能原因 | 排查方法 | 解决方法 |
|---|---|---|---|
| 实现阶段DRC RTSTAT-2 | 收发器位置约束冲突 | 检查XDC中GTP LOC是否重复 | 确保每个GTP通道唯一LOC |
| 比特流生成失败 | 引脚约束缺失或冲突 | 看Implementation的Critical Warning | 补全XDC或修正冲突引脚 |
| 上板后链路不通 | 参考时钟频率不对 | ILA抓pll_lock | 核对晶振频率和约束 |
| 接收数据错乱 | 8B/10B解码错误 | 看rx_resetdone和rx_los | 检查光纤和SFP模块 |
| FCS校验一直失败 | CRC字节序或初值错误 | 用已知数据测试CRC模块 | 核对多项式和位序 |
| 跨时钟域数据丢失 | FIFO满/空处理不当 | 看FIFO的满空标志 | 加反压或加深FIFO |
5.3 光模块兼容性与眼图测试
不是所有SFP模块都兼容所有FPGA板卡。我遇到过某品牌的SFP模块在A板上能用,在B板上就不行,后来发现是B板的AC耦合电容值不对。选SFP模块的时候,优先选和板卡厂商推荐一致的型号,或者至少确认模块的速率、波长、传输距离和板卡匹配。
眼图测试需要用到示波器加光探头,一般开发者没有这个条件。替代方案是用FPGA内部的收发器眼图扫描功能,7系列GTP支持内部眼图扫描,通过DRP接口读取,可以大致评估信号质量。如果眼图张开度很小,检查PCB走线、连接器焊接、SFP模块金手指是否干净。
实操心得:调试光口的时候,我习惯先做近端回环(用一根光纤把同一个模块的TX和RX连起来),确认FPGA内部逻辑和SFP模块本身没问题,再做远端回环(两块板子对接),这样能把问题范围缩小一半。近端回环通了但远端不通,问题大概率在对端或者光纤上。
6. 性能优化与进阶扩展方向
6.1 时序收敛技巧与资源优化
千兆速率下,125MHz时钟对7系列FPGA来说不算高,时序收敛一般不难。但如果你在同一个工程里还跑了其他高速逻辑,比如DDR控制器或者图像处理流水线,就可能出现时序违例。我的经验是:把光口相关的逻辑放在独立的pblock里,给它分配专用的时钟区域和IO资源,减少和其他逻辑的竞争。
资源优化方面,MAC层的FIFO用Block RAM实现,不要用分布式RAM,否则LUT占用会很高。CRC模块用查表法的话,ROM也会占Block RAM,如果资源紧张可以改成逐位法但提高时钟频率,用面积换速度。
6.2 从千兆到万兆的升级路径
如果项目需求升级到万兆,1000BASE-X的方案就不能直接用了。万兆以太网(10GBASE-R)的线速率是10.3125Gbps,需要GTH/GTY收发器,8B/10B编码也换成了64B/66B。Xilinx提供了10G Ethernet PCS/PMA IP核,配置流程和千兆类似,但时钟要求更高,PCB走线也更严格。我的建议是:如果当前项目只是千兆需求,先把千兆做稳,万兆的升级路径留好接口,比如把MAC层和物理层之间的接口做成标准化的AXI-Stream,将来换物理层的时候MAC层不用动。
6.3 多板卡同步与时间戳应用
在多板卡采集系统里,光口以太网不仅仅是数据传输通道,还可以用来做时间同步。我的做法是在以太网帧里嵌入一个64位的时间戳,发送端在帧离开MAC的瞬间锁存本地计数器,接收端收到帧后用自己的计数器减去时间戳,得到链路延迟。如果多块板卡都这样做,就可以实现亚微秒级的同步精度。
这个方案的关键是时间戳的锁存点要尽量靠近物理层,越靠近PHY,延迟越确定。我一般把锁存点放在PCS层的发送使能信号上,这样软件层面的抖动就被隔离掉了。实测下来,两块板卡之间的同步误差可以做到200纳秒以内,对于大多数工业采集场景足够了。
最后分享一个我在实际项目中总结的小技巧:光口调试的时候,先不要急着上协议分析仪,用FPGA内部的ILA把收发器状态和MAC层信号抓出来,90%的问题都能定位。协议分析仪虽然强大,但价格贵、操作复杂,而且很多时候你只需要知道“链路通没通”“数据对不对”,ILA完全够用。等ILA确认物理层和MAC层都没问题了,再上分析仪看上层协议,效率会高很多。