做FPGA接PHY芯片的工程师,十有八九会跟RGMII这个接口纠缠一阵子。引脚少、速率高、能直接跑千兆,RGMII几乎是SoC和FPGA侧最通用的以太网物理层接口。我前段时间调一块Zynq+外部千兆PHY的板子,现象是link能起来,但iperf一跑就掉线,ILA抓出来的接收数据整段错位。折腾了两天,最后问题落在一个点:RGMII接口时序里那2ns延时没配明白。真不是玄学,是MAC和PHY两侧对“数据什么时候有效”的理解不一致。这篇文章我把RGMII从PHY到MAC这条链路里的2ns延时为什么必须加、加在哪、怎么配、怎么验证,一次说透,并附上我在实际板子上测到的数据。
文章适合三类人看:正在写RGMII时序约束的FPGA工程师,做硬件调试但被PHY寄存器搞到头疼的嵌入式工程师,以及准备评估PHY芯片时序性能的通信方向同学。不需要懂很深的理论,但最好用过Vivado、接触过一点MDIO配置,不然里面某些操作步骤会有点跳。
1. 项目概述与核心需求解析
1.1 RGMII到底是什么——4根数据线+双沿采样的精简接口
RGMII(Reduced Gigabit Media Independent Interface)是为了替代GMII被提出来的。GMII在千兆模式下有8根数据线、125MHz时钟,单沿采样,一个周期送8bit,总带宽1Gbps,接口引脚不算少。RGMII把数据线砍到4根,时钟还是125MHz,但改成了双沿采样,上升沿和下降沿各送4bit,合起来一个时钟周期还是8bit,带宽没变,引脚却少了一半。
具体到信号上,RGMII收发各有一组:发送侧是TXC、TXD[3:0]、TX_CTL,由MAC发给PHY;接收侧是RXC、RXD[3:0]、RX_CTL,由PHY发给MAC。TXC和RXC在千兆模式下都是125MHz,但数据是在上下两个沿都有效,所以每个bit的实际有效窗口只有4ns。TX_CTL和RX_CTL也不是单纯的控制线,它们在时钟上升沿对应TX_EN/RX_DV,下降沿对应TX_ERR/RX_ER,等于一根线当两根用。
这套设计最大的问题,恰恰出在“上下沿都有数据”这件事上。GMII时代时钟频率低、数据窗口宽,接收端随便找个沿采样基本都不会踩到数据跳变边缘。到了RGMII,数据窗口被压到4ns,时钟沿如果和数据变化边沿正好重叠,接收端采样的就是“正在变化中的电平”,建立时间和保持时间全都岌岌可危。所以RGMII必须让数据有效窗口的中心点对准采样时钟沿,这就是2ns延时的由来。
1.2 为什么偏偏是2ns:从Tskew到中心对齐的完整推导
很多朋友一开始不理解,为什么RGMII规范里反复强调“约2ns延时”。其实算起来非常直接。千兆模式下RXC是125MHz,周期8ns;但数据是DDR,上升沿一个bit、下降沿一个bit,所以每个bit周期实际是4ns。要让接收端采样点落在数据窗口正中间,就得把数据或者时钟偏移半个bit周期,也就是2ns。
注意,是半个bit周期,不是半个时钟周期。有些人把这个概念搞混,按4ns去配,配置出来的结果等于数据变化点又回到了采样沿附近,反而更糟。这2ns可以加在数据通路上,也可以加在时钟通路上,效果上都是让采样沿从“数据边界”挪到“数据中心”。
那为什么PHY/MAC不直接把数据做delay,非要把这个难题抛给用户?原因在于芯片厂商没法确定对端是什么方案。有的PHY内置了延时,有的MAC侧FPGA已经做了IO延时,如果两边同时加,加起来就可能凑成4ns甚至更大,直接废掉。这也是很多板子“常温能通、一热就挂”的根本原因。所以RGMII的2ns不是一个固定值,而是一个需要按实际场景选择的折中值,核心是保证接收端建立时间和保持时间都有足够余量。
1.3 影响范围:从PCB设计到软件驱动的全链路问题
RGMII时序问题的影响面比很多人想象得大。表面上它是“时序约束”问题,实际上牵扯到PCB走线长度、PHY寄存器配置、FPGA原语选型、MDIO驱动、以及高低温下的稳定性验证。任何一环疏忽,表现出来的症状都是“链路不稳定”或者“偶发CRC错误”,定位起来非常难受。
我这次项目里的板子,走线本身并不长,PCB评审时也做过等长,但等长只解决了物理长度一致的问题,没有解决RGMII协议层面的“边沿对齐”问题。换句话说,走线做得好只代表偏斜小,不代表协议对。协议要求的是“数据有效窗口中心对采样沿”,这必须在逻辑层额外配置,靠走线长度去凑2ns不现实——FR4上1 inch大约对应165ps延时,想凑出2ns要12 inch以上的长度差,正常板卡根本塞不下。
2. RGMII的2ns延时加在哪:PHY寄存器、FPGA约束与PCB走线
2.1 方案A:PHY芯片寄存器调整TX/RX延时
最省事的方式,是用MDIO总线把PHY芯片内部的RGMII延时打开。市面上常见的千兆PHY,像Realtek RTL8211系列、Marvell 88E1512、裕太微YT8531等,基本都在寄存器里提供了RGMII TX Delay和RX Delay的控制位。这些位的作用是在PHY内部为TXD/TX_CTL或RXD/RX_CTL增加约2ns延时,让数据变化点相对采样沿偏移一个bit半周期。
以RTL8211F为例(不同型号寄存器地址差异很大,拿到别的PHY一定要查手册),RGMII TX Delay控制位在0x14寄存器的bit11,RX Delay控制位在0x15寄存器的bit11,置1后内部延时约2ns。配置方式一般走MDIO接口,在Linux下可以用mdio-tools,在FPGA里可以自己写一个MDIO Master小模块。伪代码逻辑非常简单:
// 读当前寄存器值 uint16_t val = mdio_read(phy_addr, 0x14); // 使能RGMII TX Delay,保留其他bit val |= (1 << 11); mdio_write(phy_addr, 0x14, val); val = mdio_read(phy_addr, 0x15); val |= (1 << 11); // 使能RX Delay mdio_write(phy_addr, 0x15, val);用PHY内置延时的好处是MAC侧不需要做任何逻辑改动,约束文件也只是象征性写一下。坏处是不同厂商、不同批次的PHY,延时的精度和温漂特性差别很大。我实测过某款国产PHY在25℃时内部延时有2.1ns,但到85℃掉到1.6ns,余量明显变差。所以如果做高低温都要稳的产品,不能完全依赖PHY内部延时,最好还是让FPGA侧能主动控制采样点。
2.2 方案B:FPGA内用ODDR/IDDR原语配合时序约束
对FPGA用户来说,另一个常见方案是完全不管PHY的延时寄存器,在FPGA内部用IDDR/ODDR原语把DDR数据转换成两个半字节的SDR数据,然后用XDC约束把2ns偏移算进去。这个方案最灵活,也是我做项目时优先选用的方式。
先看接收方向。PHY发过来的RXD[3:0]在RXC的上升沿和下降沿都有效,FPGA不能直接把RXD当普通并行数据采,必须用IDDR把双沿数据拆开。Xilinx 7系列/Zynq的IDDR可以配置成SAME_EDGE_PIPELINED模式,上升沿采到的低4bit和下降沿采到的高4bit在同一个时钟周期并行输出。ODDR反向操作,把两个半字节在TXC的双沿发送出去。
约束上,XDC里要做两件事。第一,把RXC和TXC声明成时钟并和外部PHY的时序关系对应起来;第二,用set_input_delay和set_output_delay告诉工具数据相对于时钟是哪种偏斜关系。下面是接收方向的典型写法(实际数值需要按PCB走线长度和PHY手册重新算,不能直接抄):
# 外部PHY提供RXC,作为FPGA的接收时钟 create_generated_clock -name clk_rxc -source [get_ports eth_rxc] -divide_by 1 # 假设RXD相对RXC的延时窗口在0.5ns到2.0ns之间 set_input_delay -clock clk_rxc -max 2.0 [get_ports {eth_rxd[*] eth_rx_ctl}] set_input_delay -clock clk_rxc -min 0.5 [get_ports {eth_rxd[*] eth_rx_ctl}]发送方向类似,只不过换成set_output_delay,对象是TXC时钟输出的TXD和TX_CTL。需要注意的是,如果PHY侧已经开了2ns延时,FPGA这边的约束就要相应调整,不能让两边叠加出4ns偏移。这也是调试中容易踩的坑。
2.3 方案C:PCB蛇形走线延时
理论上RGMII的2ns偏斜也可以靠PCB走线做,常见做法是让时钟线比数据线长一段,或者反过来。FR4板材上信号传播速度大概6 inch/ns,反推2ns就是12 inch长度差。这个长度在真实板卡上几乎不可能接受,除非PHY和MAC离得非常远并且走线天然有巨大长度差,否则不建议作为主方案。
更合理的做法是让PCB走线尽量等长,把偏斜控制在picoseconds级别,剩下的事交给PHY寄存器或者FPGA约束去处理。PCB等长只是保证“不引入额外问题”,不能指望用走线去解决协议层面“边沿对齐采样”的设计缺陷。
3. 实操记录:从默认配置到2ns精准落地的三个步骤
3.1 硬件环境与初始故障现象
这次调试用的硬件是Zynq-7020加一颗国产千兆PHY,MDIO地址0x00,PHY工作在RGMII 1000Base-T模式,FPGA内部跑一个自研的以太网MAC,数据通路用AXI-Stream接DMA。刚开始上电后千兆link能建立,PHY状态寄存器读到link up、速率1000M、全双工。但一跑iperf就露馅,TCP吞吐只有两三百兆,而且持续几分钟后网口直接挂掉,ping不通,必须重新link。
用ILA抓接收侧RXD和RX_CTL,发现数据不是“偶尔错一bit”,而是整个4bit组循环错位。比如MAC期望接收到的是0xA5 0x5A这种交错数据,实际采出来的序列整体看起来像“后半字节拿到了前半字节的位置”。这说明IDDR拆分高低半字节的相位关系不对,RXC采样沿落在了数据跳变沿上,属于非常典型的RGMII延时不匹配。
3.2 第一步:先用PHY寄存器做一个“粗暴”的2ns偏置
定位到时序问题后,最先尝试的是最快速的方案:把PHY内部RX Delay打开,让PHY输出的RXD相对RXC整体偏移2ns。用MDIO工具直接操作,先把寄存器读回来,确认bit原始值,再置位写回。配置完成后重新抓ILA,发现RXD和RX_CTL的相位关系明显改善,原来错位的半字节回到了正确位置。
这一步的价值在于快速验证“问题确实出在延时上”。如果打开PHY内部延时后现象立刻变好,就不需要在FPGA侧瞎猜约束值。但这里有个陷阱:PHY的RX Delay打开后,FPGA侧约束万一还是按“无延时”写,时序分析结果就会和实际电路不一致,所以下一步必须回头修正XDC里的input delay。
3.3 第二步:FPGA侧同步调整XDC约束
PHY延时打开后,我在XDC里把接收方向的数据和时钟关系调整成“中心对齐”的样子。仍以上面的约束为例,RXD相对RXC的input delay窗口从默认的0~0.5ns改成0.5~2.0ns,发送方向也做了对称调整。然后跑综合布线,重点看时序报告里rxd_in和rx_ctl_in这两条路径的setup slack和hold slack。
第一次调整后,setup slack还有0.2ns,hold slack只剩0.05ns,几乎贴着线。这种情况说明延时的“总量”差不多对了,但窗口宽度还偏紧。我继续把input delay的min值从0.5ns微调到0.3ns,max保持不变,hold slack最后升到0.18ns。这一点点调整,就是用长跑稳定性换来的参数余量。
3.4 第三步:用ILA加长时间压力测试验证
约束调整完成后,先跑一轮短测试:在Zynq里用ILA持续抓取RXD、RX_CTL还有MAC输出的CRC错帧计数器。抓了5万帧,CRC错误计数保持0。接着把ILA关掉,用iperf做30分钟TCP流,速度稳定在930Mbps左右,再ping 1000个1000字节的大包,0丢包。这个时候才可以判断这次配置基本对了。
这里有个经验:ILA虽然能直观看到线上波形,但它本身会占用大量BRAM和布线资源,可能改变布局布线结果。验证阶段用ILA看现象没问题,但最后稳定性测试时最好把ILA删掉,或者只保留一个极小的计数器,避免“ILA存在时正常、综合成最终版本后反而出问题”这种诡异情况。
4. 实测数据与踩坑实录:高温、低温、双重延时叠加
4.1 三种工况下的Setup/Hold余量对比
下面这组数据来自同一块板子、同一版逻辑,只改变延时配置方式,在三种温度环境下各跑2小时压力测试得到。测试用的是Vivado时序报告里的slack数据,以及MAC层CRC错误计数器。
| 工况 | 延时配置方案 | Setup Slack | Hold Slack | 压力测试结果 |
|---|---|---|---|---|
| 25℃/1.0V | 仅PHY内部RX Delay | 0.34ns | 0.26ns | 2小时0错包 |
| 85℃/0.95V | 仅PHY内部RX Delay | 0.08ns | 0.21ns | 偶发CRC错误,约1小时1次 |
| 85℃/0.95V | PHY内部延时+FPGA修正约束 | 0.19ns | 0.33ns | 2小时0错包 |
| -40℃/1.05V | PHY和FPGA两侧同时加2ns | -0.12ns | 0.10ns | 频繁错包,link偶发断开 |
第一行对照实验证明25℃下PHY内部延时够用。第二行是芯片温度升高、内部延时量变小、走线阻抗也随温度变化带来的组合结果,hold slack虽然还行,但setup slack已经压到0.08ns,稍有噪声就会触发亚稳态。第三行说明FPGA侧介入修正后,把余量重新拉开。第四行则是典型的“两边同时加延时”翻车现场:PHY的2ns加上FPGA的2ns,总共偏移了约4ns,又回到了数据跳变沿采样,而且低温下器件延时变大,hold路径直接违规。
4.2 踩坑实录:延时方向给反导致的半字节错位
调试过程中我踩过最典型的一个坑,是把延时方向给反了。当时想着“数据晚了,就把采样沿往后推”,结果XDC里set_input_delay的max/min写反了,导致工具认为数据窗口在采样沿左边,而实际数据在采样沿右边。整条链路高速跑起来后,MAC收到的接收数据从0x12345678变成了0x34127856,高半字节和低半字节完全调换。
这种“半字节错位”现象最迷惑人,因为它看起来像字节序问题,很多工程师会去改DMA描述符或者改MAC的字节序配置,改来改去都不生效。我后来靠ILA把RXD、RX_CTL、RXC三个信号拉到同一个窗口里对比,才意识到错位不是软件顺序错了,而是IDDR高低半字节的相位选反了。
排查这类问题有个顺手的小技巧:如果ILA抓到的RX_CTL高半字节和低半字节正好和RXD错开一个bit位,优先查约束里的input delay方向,不要先动逻辑代码。RGMII的数据相位只有“提前2ns”和“滞后2ns”两种可能,把max/min交换一下重新布线,比翻代码快得多。
4.3 小技巧:如何用一根网线和Wireshark快速判断延时方向错误
没有ILA条件的时候,也可以用软件方法辅助判断。把FPGA侧MAC收到的数据,通过AXI接口送到处理器里,然后以raw socket发到PC端Wireshark抓包。如果RGMII延时方向不对,Wireshark里看到的以太网帧目的MAC地址会整体错位,比如本应是00:11:22:33:44:55,抓出来变成11:00:33:22:55:44或者00:22:11:44:33:66这种半字节交叉错乱。
这个现象相当有辨识度。如果是单bit错位,通常是信号质量问题或者电平问题;如果是两个半字节规律性互换,基本就是RGMII时序相位不对。我后来甚至把这种方法当成了现场快速诊断工具,不用开Vivado,一根网线加Wireshark就能判断是不是相位问题,再决定要不要回实验室改约束。
5. 快速自查清单:RGMII时序配置查这几项就够了
项目收尾阶段,我把这次调试过程中踩过的坑整理成了一张自查表,后续再做RGMII相关板卡时直接逐项核对,能省掉大量排查时间。这里分享出来,覆盖了从寄存器到约束到实物验证的关键检查项。
| 检查项 | 操作方法 | 合格标准 |
|---|---|---|
| PHY延时配置 | MDIO读寄存器确认TX/RX Delay位 | 与方案一致,只在一侧开启2ns延时 |
| 时钟约束 | 检查XDC里RXC/TXC的create_generated_clock | 时钟已定义,且与物理引脚对应 |
| input/output delay | report_timing_summary查rxd_in/txd_out路径 | setup/hold slack均大于0.15ns |
| IDDR/ODDR相位 | ILA抓RXD和RX_CTL的上下半字节 | 数据与CTL对应关系与协议一致 |
| 高低温稳定性 | 高低温箱各跑2小时iperf+ping | 0错包、0丢包、link不闪断 |
| 两侧延时叠加 | 确认PHY内置延时和FPGA约束不同时启用 | 总偏移接近2ns,远离0ns和4ns |
最后再分享一点个人体会。RGMII的2ns延时,本质上是协议为了省引脚而把数据窗口压缩后留下的“历史债”。它不是一个需要精密计算到皮秒的参数,而是一个需要保证“采样点落在数据窗口中间”的工程约束。与其死记2ns这个数,不如理解它背后那套“半bit周期中心对齐”的逻辑,这样无论换PHY型号还是换FPGA平台,都能快速定位问题。实测下来,只要抓住“只在一侧加延时,留足温度余量,用ILA验证相位,用长跑确认稳定”这四条原则,RGMII基本不会再折腾人。