干过网络调试的工程师应该都体会过这种绝望:板子回来,PHY协商正常,Link灯亮着,千兆速率也协商上了,结果一ping大包就疯狂丢包,抓包全是CRC错误,甚至直接不通。查电源、查焊接、查变压器,折腾一圈下来,最后发现罪魁祸首是RGMII接口的时钟延迟配置不对——一个在原理图、PCB、驱动三条链路里都被默认“忽略”的参数。
RGMII(Reduced Gigabit Media Independent Interface)是MAC和PHY之间最常用的千兆以太网接口之一,靠一对时钟和四根数据线完成全部收发。它把传统MII需要几十根信号的接口压缩到了一个小接口,代价就是时序极其敏感。这篇文章我准备把RGMII的时钟延迟问题一次性讲透:为什么要有延迟、延迟从哪来、怎么配置、常见故障怎么定位,帮你把这块坑填平。
1. RGMII接口的基本盘:引脚、DDR采样与那8纳秒的博弈
1.1 什么是RGMII,为什么千兆以太网离不开它
RGMII是MII接口的简化替代方案,专门为了用更少的引脚跑更高的速率。它总共只保留两组差分信号(TX/RX)、两路时钟、两路控制信号,另外还有管理接口MDIO/MDC。以千兆模式为例,发送时钟TX_CLK是125MHz,接收时钟RX_CLK也由PHY提供125MHz。
传统MII接口在百兆下需要14根信号线,到了千兆如果继续用MII方案,引脚数量会非常恐怖。RGMII通过DDR双沿采样,让4根数据线在时钟的上升沿和下降沿各传一次,等效传输8位数据,收发各4根对Mac和PHY芯片来说都极为友好。正是这种“引脚减半、速率翻倍”的设计,让RGMII成了绝大多数SoC内置MAC与外部PHY之间的首选接口。低成本交换芯片、FPGA方案、消费级路由器主控,几乎都能看到RGMII的身影。
但代价也随之而来:因为是DDR双沿采样,数据要在125MHz(8ns周期)内完成4位双沿传输,留给信号建立时间和保持时间的窗口被压缩到了纳秒级。任何一点走线偏差、芯片内部触发器延迟、PCB过孔引入的反射,都可能让采样窗口失效。
1.2 双沿采样的本质:一半信息在上升沿,一半在下降沿
RGMII的数据线有TXD[3:0]和RXD[3:0],数据在时钟的上升沿和下降沿同时采样。以TMAC发送侧为例:TXD[3:0]在TX_CLK上升沿发送的是TXD[3:0]的低4位,在下降沿发送的是TXD[7:4],两者拼在一起才是完整的一个字节。
TX_CTL这条控制信号也同理:上升沿发送TX_EN,下降沿发送TX_ER。接收侧完全对应:RX_CLK上升沿采样RXD[3:0]得到低4位,下降沿采样得到高4位,RX_CTL的两沿分别给RX_DV和RX_ER。
这种设计带来一个非常重要的特性:接收端必须在同一时钟周期内,既在边沿A采样一部分数据,又在边沿B采样另一部分数据。如果时钟和数据之间的相对相位有偏差,上升沿采到的未必是低字节,下降沿采到的也未必是高字节。更麻烦的是,PHY提供的RX_CLK和PHY输出的RXD数据之间,天然存在一个输出的延迟窗口,这个窗口必须在MAC可接受的范围之内。
1.3 8ns时钟周期下,延迟预算如何分配
千兆模式下,TX_CLK和RX_CLK的周期都是8ns。RGMII规范要求接收端在时钟的上下边沿都能可靠采样,标准推荐的时钟与数据延迟窗口大约在1.5ns到2ns之间。
为什么是1.5ns到2ns?我们拆一下:发送端(比如PHY)在时钟沿输出数据,经过内部触发器的clock-to-out延迟,再经过PCB走线传输,到达接收端(MAC)时数据相对时钟已经有偏移。PHY芯片内部的各路数据信号clock-to-out很难做到完全一致,通常存在0.5ns到1ns左右的skew。如果没有额外延迟,接收端在时钟沿采样的瞬间,数据线可能还在变化过程中,建立时间不够,采样结果就会出错。
RGMII规范给出的思路是:让数据相对于时钟移相半个周期之内,但不是整个半周期,而是1.5ns到2ns这个足够避开数据转换沿的窗口。这样接收端采样时,数据线已经稳定下来,能够同时满足建立时间和保持时间的要求。这个1.5ns~2ns的延迟参数,既包含PCB走线带来的自然延迟,也要靠端口的内部延迟配置来补齐。
2. 延迟从哪来:PHY/MAC芯片内部、PCB走线与规范要求
2.1 时钟和数据之间为什么要人为“错开”
我们先明确一个容易绕晕的点。MAC和PHY之间传递数据时,谁采样谁?发送侧是完全相反的配对关系:MAC发数据时,是把数据送给PHY;PHY必须用MAC提供的TX_CLK来采样。接收侧是PHY向MAC发数据,MAC用PHY提供的RX_CLK来采样。
问题就出在“MAC发数据给PHY,但时钟也是MAC发出去的”。TX_CLK和数据从同一个MAC芯片的同一个PLL域输出,表面上时钟和数据天然对齐,但PHY采样时需要时钟沿位于数据有效窗口的中间。如果MAC输出的时钟和数据完全同相位,PHY的输入触发器采样时,数据可能刚好在边沿(其实数据在边沿附近还处于变化后的稳定初段),建立时间不足。
所以标准做法是让发送时钟比数据“晚”一点,或者让数据比时钟“早”一点,使得采样沿落在数据位的正中间附近。RGMII v2.0规范正是通过给数据加一个可编程延迟,来让时钟沿落在数据眼图中央。这也是MDI/MDIO管理口之外,RGMII调通与否最重要的一个物理层参数。
2.2 三种注入延迟的手段:内部配置、外部走线、FPGA约束
围绕“如何在数据与时钟之间制造1.5ns~2ns偏移”,实际工程中常见三种做法:
第一种是PHY内部延迟。大多数支持RGMII的PHY芯片都内置可编程延时单元,通过寄存器或配置引脚开启。比如瑞昱的RTL8211/F系列、Micrel的KSZ9031、Marvell的88E1512/88E1518等,都提供TXDelay和RXDelay的独立配置。这种方案最简单纯粹:硬件不改,软件改寄存器即可。
第二种是PCB外部走线延迟。RGMII标准允许通过加长P5C走线来获得物理延迟,原理就是让数据线比时钟线长出一截,利用信号在PCB上的传播速度(常见FR4材料表面走线约6in/ns,即1ns对应约6英寸,实际约150mm/ns,更精确一般是6.5in/ns左右)延后数据到达时间。对1.5ns延迟,大约需要走线长25cm左右,这在大部分板子上都很难实现,所以走线延迟只适合做微调,不适合主力配置。
第三种是FPGA或MAC侧的IODELAY/DDIO约束。如果MAC侧是FPGA实现,可以在RX方向用IDELAY原语给输入数据或时钟加延迟,在TX方向用ODELAY或PLL相位调整来满足输出时序约束。这是FPGA方案里最常用、也最灵活的办法。
三种手段可以组合,但有一个原则:同一条链路上只需要一次“主延迟”即可,叠加太多会导致总延迟超过一个周期的容限,反而把眼图推坏。
2.3 为什么RGMII-ID不能两边同时开
这是工作中最常踩的坎。很多PHY默认开启RGMII-ID模式(即TX和RX都做内部延迟),而MAC侧的SoC在某些BSP里也默认配置为rgmii-id,两边同时加延迟,结果链路在低速下勉强能通,一上1000M就丢包。
原因是单侧延迟就以1.5ns~2ns为目标,双侧叠加后总延迟可能达到3ns~4ns。虽然还在一个时钟周期内,但已经偏向眼图边缘,建立时间和保持时间的余量都被吃光。极端情况下甚至会超过半个周期,导致采样沿对准了在上一个位的尾部、下一个位的头部,数据直接错位。
排查方法其实简单:先确认phy-mode到底配成了什么,再看PHY寄存器里TX/RX延迟的实际值。如果两边都开了,就需要关掉其中一侧。我的习惯是尽量用MAC侧配置rgmii-id(因为SoC内部寄存器好改),PHY侧用缺省或明确关掉延时。
3. 硬件与软件侧的实战配置
3.1 设备树里phy-mode几个选项的含义
Linux下主要通过设备树的phy-mode属性控制RGMII工作模式。常见的值有:
| phy-mode值 | 含义 |
|---|---|
rgmii | 标准RGMII,不启用任何内部延迟,依赖PCB走线和外部逻辑 |
rgmii-id | TX和RX方向都启用内部延迟(Internal Delay) |
rgmii-txid | 仅在TX方向启用内部延迟 |
rgmii-rxid | 仅在RX方向启用内部延迟 |
以RK3399/RK3568这类带GMAC的SoC为例,设备树里常写成:
&gmac { phy-mode = "rgmii-id"; snps,reset-gpio = <&gpio3 RK_PB7 GPIO_ACTIVE_LOW>; snps,reset-active-low; snps,reset-delays-us = <0 10000 50000>; status = "okay"; };设置rgmii-id的时候,SoC的MAC内部会在TX/RX两条路径上插入IDELAY,具体延迟量由SoC的GMAC控制器寄存器控制。不同厂家命名不一样,Rockchip系列通常是mac->tx_delay和mac->rx_delay,在驱动解析设备树时读取。
另一个容易忽略的点是:phy-mode改了之后,必须确认PHY芯片自己有没有自动开启延迟。有些PHY的默认配置是通过硬件引脚(如LED引脚复用、配置电阻)选择的,不是纯靠寄存器。下次上电如果PHY又恢复默认,链路可能瞬间从“能跑”变成“狂丢包”。
3.2 常见PHY厂家的延迟寄存器参考
下面给几款我用过的PHY芯片延迟配置参考。要提醒的是,芯片版本不同、封装不同,寄存器地址和位域可能略有差异,务必以对应datasheet为准。
瑞昱RTL8211F/RTL8211FD:延迟控制主要在0xB0(DLCR)寄存器中。开启TX延迟通常写bit15,开启RX延迟通常写bit8。具体示例:
phy_write(phydev, 0xb0, 0x8100); // 开启TX延迟 phy_write(phydev, 0xb0, 0x8108); // 同时开启TX和RX延迟Micrel/KSZ9031:在Micrel驱动里有专门的ksz9031_of_configure_opts函数解析设备树里的txd-to-rxd、rxdv-skew-ps等参数。实际寄存器访问涉及地址扩展,需要先写0x2寄存器选择MMD页面。常见配置是TX/RX internal delay各加0.6ns到2.0ns不等,需要在PHY寄存器0x0C(RX)和0x11(TX)的[11:10]位设置延迟等级。
Marvell 88E1512/88E1518:延迟控制通过PHY页面2的寄存器配置,典型模式是phy-mode = "rgmii-id"时,软件会在mv88e1512_config_init里关闭或调整内部延迟。这块PHY对走线长度特别敏感,我试过同一套代码在88E1512上可以,换了88E1518就必须重新调延迟等级。
实际调试中,我不会先在设备树里把phy-mode写死。第一步是用ethtool -d eth0直接把PHY寄存器dump出来,确认当前延迟配置状态,再决定是改软还是改硬。
3.3 FPGA做MAC时的IODELAY与时序约束思路
如果MAC侧在FPGA里(Xilinx 7系列或者UltraScale、Intel Cyclone等),RGMII的延迟配置就更依赖硬件描述语言和时序约束。内核方向RX_CLK由PHY提供,RXD数据也要跨时钟域处理。常用的做法是:
在Xilinx 7系列上,通过IDELAY原语给RXD[3:0]和RX_CTL增加可调延迟:
IDELAYE2 #( .IDELAY_TYPE("FIXED"), .DELAY_SRC("IDATAIN"), .IDELAY_VALUE(16) ) idelay_rxd0 ( .IDATAIN(rxd[0]), .DATAOUT(rxd_delayed[0]), .C(rx_clk), .CE(1'b0), .INC(1'b0), .LOAD(1'b0), .REGRST(1'b0), .CINVCTRL(1'b0), .CNTVALUEIN(5'b0), .CNTVALUEOUT(), .LDCPEN(1'b0), .PIPEINC(1'b0), .PIPERST(1'b0), .PIPETAP(5'b0), .RST(1'b0) );IDELAY_VALUE的换算要结合IDELAYCTRL参考时钟,通常是每tap约78ps(参考时钟200MHz),16个tap约1.25ns。具体以Xilinx文档为准。
TX方向一般是ODDR原语输出DDR数据,再配合ODELAY或PLL相位调整。时序约束里,要给出InputDelay和OutputDelay同样重要。如果是完全自己写RTL,RGMII的约束往往让人头大,但原理和PHY芯片配置异曲同工:保证数据在采样时钟沿上有足够的setup/hold margin。
3.4 用寄存器读写快速验证配置是否生效
调试RGMII时,我最常用的工具是devmem和ethtool。先看链路状态:
ethtool eth0 ethtool -S eth0 | grep -E "crc|drop|error"发现CRC错误后,读PHY寄存器确认延迟状态。很多SoC把MDIO控制器开放到了内存映射,可以直接用devmem操作。例如某款平台MDIO地址空间在0xF0004000附近,我可以直接读写PHY寄存器0x1F、0x0C等。这种方法虽然粗暴,但在没有完整BSP、驱动还没跑起来时特别管用。
如果是通过MDIO管理接口操作PHY,还可以写一个小工具直接在用户态遍历MDIO:
# 假想的总线访问脚本,实际平台需替换地址 devmem 0xF0004004 32 0x0c # 选择PHY地址和寄存器 echo "PHY reg 0x0C = $(devmem 0xF0004008 32)"很多时候PHY芯片延迟配置和MAC侧延迟配置并不是“互斥”关系,而是存在一个叠加窗口。验证方法就是在对端打流、看误码率的同时,微调某一侧的延迟值,找到最优区间,然后固定。
4. 常见故障现象与排查实录
4.1 千兆协商成功但大包全丢
这是RGMII延迟问题的典型症状。链路协商成功说明PHY的Auto-Negotiation和MDI/MDIX都正常,但大包需要连续多拍采样,只要某一拍采错,CRC就失败,到了IP层就是丢包。
排查路径:先用ethtool -S eth0看rx_crc_errors,如果这个值在上涨,问题大概率在RX方向。然后看phy-mode到底是rgmii还是rgmii-id。曾经遇到过一块板子,SoC侧配置成rgmii-id,PHY侧也通过LED配置引脚把TX Delay打开了,两边一叠加,CRC错误哗哗涨,把PHY侧延迟关掉后立即恢复。
处理建议:先只开MAC侧延迟,关掉PHY侧延迟,打流验证。如果改善不明显,再尝试MAC侧开、PHY侧也开同时调低延迟等级。
4.2 CRC错误计数器不断增长但不完全断流
另一种情况是百兆正常、千兆丢包。RGMII在百兆模式下时钟频率降到25MHz,数据周期40ns,原来的时序余量问题被甜点化,延迟配置差个1ns还能扛住。但升到千兆,8ns周期下,1ns相对误差就达到12.5%,很容易出问题。
调试时可以尝试在设备树里临时改成rgmii-txid或rgmii-rxid,配合ethtool -S观察CRC计数变化。如果改成rgmii完全不通,而rgmii-id丢包,说明延迟量可能在边界上,可以通过PHY芯片的delay grading寄存器微调,或者检查PCB走线是否满足等长要求。
走线上的问题,确认过几次后发现:RXD组内长度差不要超过±50mil,RXC与RXD的差分对(或者说时钟与数据组)不要出现超过几百mil的长度差,否则内部延迟再准也救不回。
4.3 温度升高后网络断流
这个坑比较隐蔽。芯片内部延迟单元本身对温度和电压敏感,一组出厂时刚好的延迟值,在工业级温度范围下可能漂移0.5ns甚至更多。嵌入式设备在高温箱测试时网络断流、低温恢复后正常,很多就是延迟余量不足导致。
解决办法不是加大延迟补偿到最大,而是尽量让延迟落在整个允许窗口的中间值附近,保留足够的上下裕量。另外,PCB走线长短对温度稳定性也有影响,走线太短时延迟几乎全由芯片内部提供,温度漂移就会占满整个容限。适当地增加一点走线长度,让“物理延迟”分担一部分温度漂移压力,实测下来更稳。
4.4 RGMII调试经验速查表
| 故障现象 | 优先怀疑方向 | 快速验证手段 |
|---|---|---|
| 千兆协商成功但大包全丢 | RX侧采样时序、重复延迟 | 改phy-mode为rgmii-txid或rgmii-rxid,观察CRC计数器 |
| CRC错误持续上涨 | PHY/MAC两侧延迟叠加 | ethtool -S看rx_crc_errors,关掉一侧延迟 |
| 百兆正常千兆丢包 | 延迟余量不足 | 微调PHY延迟等级,检查RXD组内等长 |
| 温度升高后断流 | 延迟漂移余量不足 | 降低延迟量到窗口中间值,增加PCB走线长度分担延迟 |
| 完全不通,Link灯都不亮 | 硬件:PHY复位、时钟、MDIO地址 | 先量PHY时钟和复位,再查MDIO能否读到PHY ID |
| 小包正常、大包超时 | 发包速率高时FIFO溢出或延迟边界 | 打双向流量测试,用ping -s 1472测包缓冲能力 |
调试时我常驻几个命令,基本能覆盖90%问题:
ethtool eth0 # 看速率和双工模式 ethtool -S eth0 # 看CRC/丢包统计 ethtool -d eth0 # dump PHY寄存器 mii-tool eth0 # 老牌工具,看链路状态 ping -s 1472 192.168.1.1 # 测大包通畅性5. 我的一点实操心得
做RGMII调了这几年,最大的体会是:RGMII不是一个“接上就能通”的接口,它更像是MAC和PHY之间一场关于时间的谈判。硬件上,原理图拉线时就把等长和参考平面做好,能省掉后面大量软件时间;软件上,设备树里phy-mode不要照抄参考设计,一定要结合自己板子的实际走线长度来定。
建议遇到RGMII问题时,先不要急着改代码,而是按“确认链路状态→确认延迟叠加→微调延迟等级→打流验证”的顺序来。手里有频谱仪或示波器的话,直接量RXD与RX_CLK的相位关系,看到底差多少ns,比瞎试寄存器管用得多。
我自己常用的一个小技巧,是把PHY芯片的延迟配置写到PHY驱动的config_init回调里,而不是依赖设备树或者config strap。这样做的好处是板子换PHY型号时,软件行为可预期,不至于因为一颗PHY芯片的不同批次改代码改到崩溃。数据手册里标注的内部延迟范围通常给的是典型值,实际批次会有偏差,所以在量产前一定要抽测几颗PHY芯片的高低温表现,确认延迟配置能覆盖整个温度区间。