1. 先弄清“网口不稳定”到底指什么
88E1512 在 SGMII 模式下工作不稳定,几乎是每个做嵌入式网络硬件的人都会碰到的问题。这块 PHY 本身不算复杂,但 SGMII 这条链路上有一堆容易被忽略的细节:参考时钟的方向、带内管理有没有使能、自协商状态机停在哪个中间态、MAC 侧和 PHY 侧是不是各自认为链路已经好了。任何一个环节出了问题,表现都是同一句话——“网口不稳定”。而“不稳定”这三个字太模糊,直接开始改寄存器基本等于盲目尝试。
我见过太多人一上来就 dump 一堆寄存器,然后对着手册逐个 bit 比对,耗掉两三天也定位不到根因。正确的顺序是先给现象分类,不同症状背后的根因集合差异很大,分类之后你至少能把排查范围压缩到三分之一。这一节讲的就是怎么分,以及手边必须准备哪些工具。
先说清楚这个内容解决什么问题:它针对 MAC 侧走 SGMII、媒体侧接铜缆或光模块的千兆以太网设计,覆盖从硬件上电、PHY 初始化、链路自协商到实际吞吐验证的完整链路。适合正在调试类似方案的人、被 link 抖动折磨过的驱动工程师、以及需要在板子回板后快速判断问题归属的硬件工程师。零基础也能看,但至少得知道 PHY 和 MAC 是两个东西。
1.1 五种典型症状,对应五套不同的排查路径
我把实际遇到过的现象归成五类,每一类的第一怀疑对象都不一样,按这个表往下走能省很多时间。
| 症状 | 现场表现 | 第一怀疑对象 |
|---|---|---|
| 链路抖动 | dmesg 反复出现 Link Up / Link Down,间隔几秒到几分钟 | 参考时钟、自协商配置不一致 |
| 链路稳定但丢包 | ping 丢包率 1%~30%,大包几乎不通 | 速率/双工不匹配、SerDes 信号质量 |
| 吞吐上不去 | 能通,但 iperf 只跑到 300~600 Mbps | SGMII 速率字段错误、CRC 错误累积 |
| 协商降速 | 强制千兆,实际协商成百兆 | 自协商能力寄存器、对端能力不匹配 |
| 冷启动不通热重启正常 | 断电重启后无 link,软重启后恢复 | 复位时序、上电时序、时钟未稳 |
第一类最容易被误判成软件问题。link 抖动很多时候确实跟软件有关,比如 MAC 侧的 in-band 自协商没打开,PHY 反复上报状态但 MAC 不认,两边的状态机就在那里来回摆。但也可能是 25MHz 晶振的负载电容选错,起振裕量不够,温度一变化频率就飘出 ±50ppm 的窗口。这两类原因在外行看来完全是同一个现象,只有把寄存器状态抓下来看才能区分。
第二类和第三类经常一起出现。链路能起来说明 SerDes 基本锁上了,但数据通路有问题。这时候要第一时间去看 MAC 的统计计数器,rx_crc_errors、rx_align_errors、rx_over_errors 这几个字段会告诉你问题严重程度。如果 CRC 错误随流量线性增长,那基本上是物理层信号完整性问题;如果零 CRC 错误但吞吐就是上不去,那更可能是协商参数不对,比如速率字段写的是千兆但实际 SerDes 只跑在百兆的重复模式上。
1.2 开工前手边必须有这几样东西
硬件层面,示波器是刚需,而且带宽要够。SGMII 是 1.25Gbps 的差分信号,测眼图至少需要 2GHz 以上带宽的探头,普通 500MHz 的示波器只能看个大概,判断不了抖动。如果实在没有高速示波器,退而求其次,用频率计测 25MHz 参考时钟的精度,用万用表测各路的供电电压和上电顺序,也能查到不少问题。我手上常备的是一个 25MHz 的有源晶振模块,用来做对照测试——怀疑板载晶振有问题时,直接飞线换一个已知良好的时钟源,一分钟就能确认。
软件层面,需要能直接读写 PHY 寄存器的通道。Linux 下有几种做法:内核自带的 mdio 工具、自己写一个字符设备驱动、或者用 ethtool 的--register-dump。最灵活的还是自己写一套基于 MDIO 总线的最小读写函数,能精确控制每次读写的时序,排查问题时不受上层框架干扰。另外要准备 iperf3、ping 的变长包测试脚本、以及一个能持续记录 dmesg 的后台进程,把 link 状态变化的时间戳记下来,事后跟其他日志对齐。
还有一个容易被忽略的东西:对端设备的配置权限。如果对端是商用交换机,你至少要能看到它的端口状态和错误计数。有些问题根本不是本地板子的问题,是对端端口被配置成了半双工,或者开了某种节能特性,本地怎么调都没用。这种时候如果看不到对端的计数器,就会陷入无限自我怀疑。
提示:别在没抓到任何寄存器快照的情况下就开始改代码。改完你也不知道是改对了还是碰巧好了,下次出问题还得从头来一遍。
2. SGMII 这条链路到底在传什么
很多人调不通 SGMII 的根源,是对这个协议的理解停留在“它是一根差分线,跑 1.25G”这个层面。实际上 SGMII 在物理层之上做了三件很关键的事:把千兆数据流塞进 1.25Gbps 的通道、用带内字段传递链路状态、以及用一套速率适配机制兼容百兆和十兆。这三件事每一件出问题都会表现为“网口不稳定”,而且症状极其相似。
这一节不打算照着手册念,而是从“为什么这么设计”的角度讲。理解了设计意图,你在看寄存器的时候就知道每个 bit 是在解决什么问题,排查时能直接往对应的方向去想,而不是逐个 bit 死记。
2.1 1.25Gbps 装 1000Mbps 数据的带宽计算
先算一笔最基础的账。SGMII 的线路速率是 1.25Gbps,采用 8b/10b 编码,也就是每 8 个 bit 的有效数据要占用 10 个 bit 的线路带宽,编码开销固定是 20%。1.25Gbps 乘以 0.8,得到 1000Mbps 的有效载荷速率。这个数字刚好等于千兆以太网的线速,一个 bit 都不多。
这笔账的意义在于:SGMII 的带宽是刚好够用的,没有任何余量。这条链路上不存在“留点余量更稳”这种事,任何导致有效带宽下降的因素都会直接吃掉吞吐。比如 8b/10b 编码在遇到长连 0 或长连 1 时会插入特殊的控制字符来做直流平衡,如果你的数据流里恰好有大量的重复模式,插入的字符会更多,实际可用带宽就会略低于理论值。这也是为什么有些场景下 iperf 只能测到 940~950Mbps 而不是满 1000Mbps,属于正常损耗,不用纠结。
真正需要警惕的是明显低于 900Mbps 的情况。如果测出来只有 600Mbps 左右,比较常见的原因是 SerDes 的均衡参数没调好,接收端眼图张开度不够,导致误码率上升,链路层不断重传。另一个可能的原因是 SGMII 链路上跑在了百兆模式但速率字段没同步,数据被重复发送占掉了带宽。这两种情况用示波器看眼图、用寄存器回读速率字段,都能很快区分。
2.2 带内管理:为什么自协商必须两端都开
SGMII 最反直觉的设计是“带内管理”。千兆以太网在正常工作时,每一帧数据前面都有 7 个字节的 0x55 前导码和一个 0xD5 的帧起始符。SGMII 把这个前导码的位置“征用”了,改成发送一个 16 bit 的控制字,里面编码了当前链路的速率、双工、link 状态和应答位。这个控制字不占用额外带宽,是嵌在数据流里的,所以叫带内。
问题在于,这个控制字只有在链路稳定之后才能传。链路还没建立的时候,两端靠什么知道对方支持什么?答案就是自协商。PHY 侧通过 SGMII 的配置字告诉 MAC “我现在是千兆全双工,link 已建立”,MAC 侧收到了才把对应的 MAC 控制器配置成同样的参数。如果一边开了自协商、另一边没开,或者两边对配置字格式的理解不一致,就会出现这种经典场面:PHY 侧显示铜缆 link 已经 up,但 MAC 侧始终认为链路是 down,或者链路起来了但速率是错的。
我遇到最多的一种情况是 MAC 侧把 SGMII 配置成了“固定千兆、禁用 in-band”,而 PHY 侧仍在跑自协商。这时候 PHY 会按照自协商结果去驱动 SerDes,但对端 MAC 根本不看那个控制字,直接按固定千兆收数据。如果 PHY 协商出来的正好是千兆全双工,那碰巧能通;如果协商成百兆,数据就被按错误的方式解析,表现为大量 CRC 错误或者干脆不通。这类问题在实验室里可能一直不复现,因为对端交换机一直协商成千兆,换一台老交换机或者换根网线就炸了。
注意:判断 in-band 有没有生效,别只看 MAC 驱动报的 link 状态,那个状态可能是驱动自己根据别的信号猜的。最可靠的方法是回读 PHY 里那个专门记录“当前实际速率和双工”的状态寄存器,跟 MAC 侧的实际配置做比对,两边一致才算真的一致。
2.3 参考时钟:整条链路的时间基准
SGMII 需要一个参考时钟来驱动内部的 PLL,再由 PLL 倍频出 SerDes 需要的 1.25GHz 时钟。这个参考时钟的选择有两种常见方案:一种是 PHY 自己外挂一个 25MHz 晶振,内部倍频;另一种是由 MAC 侧提供 125MHz 的时钟给 PHY 用。两种方案各有各的坑。
外挂晶振的方案,坑在晶振本身和负载电容。25MHz 无源晶振的精度通常标 ±30ppm 到 ±50ppm,听起来很宽松,但 SGMII 对时钟抖动的要求其实挺严。晶振的负载电容如果选错,起振频率会偏,极端情况下根本不起振或者起振后不稳定。我见过一块板子,负载电容按典型值选了 12pF,但实际晶振要求的负载是 8pF,结果常温下能工作,一到冬天就频繁掉 link。换成 8pF 之后问题消失。这种问题在实验室室温环境下几乎查不出来。
由 MAC 提供时钟的方案,坑在时序。MAC 侧的 125MHz 输出通常来自内部 PLL,这个 PLL 在 MAC 完成初始化之前是不稳定的,甚至可能是关闭的。如果 PHY 的复位在这之前就释放了,PHY 会锁到一个不存在或者不稳定的时钟上,内部状态机直接卡死。必须保证时钟稳定输出之后再释放 PHY 复位,这个顺序在很多参考设计里都没说清楚,得自己看两边的上电时序图确认。
还有第三种情况,就是时钟方向搞反了。SGMII 的时钟方向由一个专门的配置引脚决定,这个引脚在上电复位时被锁存。如果硬件上把这个脚接错了,PHY 会期望从 MAC 那边收时钟,而 MAC 那边又期望 PHY 提供时钟,两边都在等对方,结果就是谁也不工作。这种硬件级的错误软件完全救不回来,只能改板。
3. 88E1512 的工作模式与寄存器操作
这颗 PHY 支持多种工作模式,模式选择在硬件上是靠一组配置引脚在上电复位瞬间锁存的,软件上可以读取寄存器确认当前处于哪种模式,部分型号还允许通过扩展寄存器覆盖。模式选错是最容易被忽略的问题之一,因为现象看起来跟别的问题一模一样,但你无论怎么调参数都不会有效果。
这一节把模式选择、关键寄存器的用途、以及一个正确的初始化顺序讲清楚。寄存器地址和位定义我按常见约定给出,但不同后缀的型号(比如带不同封装或者不同特性的版本)映射可能有差异,具体一定要对照手上那份手册,别拿别人的配置表直接抄。
3.1 几种工作模式,先确认自己在哪一个
从 MAC 侧接口和媒体侧接口两个维度组合,常见的有这么几种:MAC 侧 RGMII、媒体侧铜缆;MAC 侧 SGMII、媒体侧铜缆;MAC 侧 RGMII、媒体侧接光模块;以及 MAC 侧 SGMII、媒体侧也接 SGMII 对端。这四种模式的寄存器配置、时钟方向、自协商行为都不一样。
我们这次排查的是第二种和第四种的混合场景,也就是 MAC 走 SGMII,媒体侧可能接铜缆也可能接光模块。这种情况下有个特别容易混淆的点:PHY 内部其实有两条独立的链路,一条是 SGMII 侧的 SerDes 链路,另一条是媒体侧的铜缆或者光纤链路。这两条链路各自有各自的 link 状态,各自跑各自的自协商。
很多人调试时只看媒体侧的 link 状态寄存器,看到它 up 了就以为整条链路通了,结果 MAC 侧没通。反过来也有人只看 MAC 侧的 link 状态,媒体侧根本没接对端也没发现。正确的做法是两个都看,而且要建立因果关系——媒体侧先 up,PHY 才会通过带内字段告诉 MAC 侧 up。如果媒体侧 up 了但 MAC 侧一直 down,问题就在 SGMII 这一段;如果媒体侧一直 down,那 SGMII 配得再对也没用。
确认当前模式最省事的办法是看硬件配置引脚的接法,或者直接读一个包含模式信息的扩展寄存器。我一般会写一个小小的自检函数,上电后把这几个寄存器的原始值打出来,存到日志里。这块板子以后出现任何网络问题,第一件事就是翻这份上电快照,跟正常板子比对,能省掉大量猜测。
3.2 关键寄存器速查:哪些必须看,哪些可以跳
下面这张表是我调试时会主动去读的寄存器集合,不是全部,但覆盖了 90% 的排查场景。地址是 IEEE 标准寄存器编号(0x00~0x1F),其中 0x00~0x0F 是标准定义的,0x10 以上是厂商扩展页。
| 地址 | 名称 | 关键位与用途 |
|---|---|---|
| 0x00 | 控制寄存器 | bit15 软复位,bit12 使能自协商,bit9 重启自协商 |
| 0x01 | 状态寄存器 | bit2 链路状态(自锁存),bit5 自协商完成 |
| 0x04 | 自协商通告 | bit8/7 百兆能力,bit6/5 双工,bit15 下一页 |
| 0x09 | 千兆控制 | bit9 通告千兆全双工能力 |
| 0x11 | 模式控制 | 光纤/铜缆模式、时钟方向相关 |
| 0x12 | 扩展配置 | 接口模式、SerDes 相关参数 |
| 0x14 | PHY 特定控制 | SGMII 速率与双工的强制/自协商选择 |
| 0x15 | 页地址 | 扩展页切换的前置操作 |
| 0x17 | PHY 特定状态 | 当前实际链路速率、双工、是否同步 |
| 0x1C | 扩展控制 | 各种补充配置,含 EEE 相关 |
看这张表的时候注意两点。第一,0x01 的 bit2 是自锁存位,读一次就清零,如果你用脚本连续读,第二次读到的是 0,会误判成链路断开。要么读的时候先读两次取第二次,要么干脆改读 0x17 里那个非锁存的实时状态位。第二,0x15 是页地址寄存器,写扩展寄存器之前必须先往这里写入页号,写完之后记得切回来,否则后续所有标准寄存器的操作都会跑到别的页上去,读出来的全是垃圾数据。
还有一个坑是软复位的等待时间。往 0x00 的 bit15 写 1 之后,PHY 内部要重新加载所有配置,这个时间手册上通常写的是“至少 100ms”,但实际测试下来,有些批次要等到 300ms 才真正就绪。如果你复位后立刻去读寄存器,读回来的可能是 0xFFFF 或者 0x0000,然后你的初始化代码基于这个错误的值继续往下配,整条链路就废了。稳妥的做法是复位后轮询 0x00 的 bit15,直到它自己变回 0,再额外延时 50ms 才开始后续操作。
3.3 配置顺序:顺序错了后面全白搭
正确的顺序是:硬件复位释放 → 等待 PHY 内部就绪 → 确认模式 → 配置自协商能力 → 重启自协商 → 轮询链路状态 → 读取实际协商结果 → 按结果配置 MAC 侧。
这里面最容易被跳过的是“确认模式”。很多初始化代码是照着某个参考设计抄的,那套配置对应的是 RGMII 模式,直接搬到 SGMII 的板子上,寄存器写进去不报错,但 PHY 内部走的完全是另一条路径,链路永远起不来。加一行代码把模式相关的寄存器读出来打个日志,成本几乎为零,收益巨大。
另一个容易出错的地方是自协商能力的配置。千兆以太网的自协商涉及三个寄存器:0x04 管十兆和百兆的能力,0x09 管千兆的能力,0x00 的 bit12/bit9 管使能和重启。如果你只在 0x09 里通告了千兆,没在 0x04 里配置百兆能力,对端如果是个只支持百兆的老设备,协商过程会走完但结果是不通。更隐蔽的情况是 0x04 的默认值带着一些你不需要的能力,比如半双工,协商时对端优先选了半双工,然后你就得到一个能 ping 通但吞吐极低的链路。
提示:如果你明确知道对端只支持千兆全双工,可以考虑关掉自协商走强制模式。但强制模式要求两端都强制,只要有一端在自协商,就会出现一端强制一端自协商的经典不匹配问题,link 起来但大量丢包。这个组合坑了无数人。
4. 从硬件到软件的完整调稳流程
前面讲的是原理和寄存器,这一节是能直接照着做的流程。我的习惯是从最底层往上走,先确认供电和时钟这些物理量没问题,再动软件配置。反过来的顺序会让你在软件层反复折腾,最后发现是硬件问题,白费一天。
整个流程大概需要半天到一天,取决于你手头的工具齐全程度。如果板子上有测试点把时钟和差分信号引出来了,会快很多。如果没有,至少要在关键节点飞线,别指望盲调。
4.1 硬件自检清单:先排除物理层的硬伤
第一步测供电。这颗 PHY 通常需要多路电源,常见的是 1.2V 内核、2.5V 或 3.3V 的 IO。每一路都要测两个值:静态电压和纹波。静态电压偏离超过 5% 就直接不合格,纹波峰峰值超过 50mV 也要警惕。纹波大的话,SerDes 的抖动会明显恶化,眼图闭合,表现为随机误码。测纹波要用示波器的 AC 耦合模式,带宽限制到 20MHz,探头用弹簧地针,别用长地线,否则测到的全是空间耦合的噪声。
第二步测上电顺序。多路电源如果有明确的先后要求(手册里一般会写),要示波器多通道同时抓。顺序错了可能导致内部 ESD 保护二极管导通,虽然不一定立刻烧,但会让器件工作在异常状态。顺便看一下复位信号的释放时刻,复位必须在所有电源稳定并且参考时钟稳定之后才释放,提前释放会让内部状态机锁死。
第三步测参考时钟。用频率计量频率,常温下应该在标称值 ±50ppm 以内。然后用示波器看波形,注意峰峰值是否达到要求(一般是 CMOS 电平,接近 IO 电压),以及上升沿是否有过冲或者台阶。有些板子为了省成本,时钟走线没有做阻抗控制,走到 PHY 引脚上已经是严重畸变的方波了,这种情况 PLL 会锁得不稳。如果条件允许,直接飞线接一个外部的信号发生器,输出标准 25MHz 方波,看看问题是否消失,这是最快的判定方法。
第四步检查 SGMII 差分对。AC 耦合电容有没有装、容值对不对(常见 0.1µF)、差分阻抗是否接近 100Ω、长度是否匹配。这些在原理图上都能看出来,回板后先用万用表量一下通断,别等到调软件调两天才发现耦合电容没贴。
4.2 MAC 侧与设备树的配置要点
软件这边第一件事是把接口模式配对。以常见的 Linux 平台为例,设备树里要明确写出 PHY 的连接方式、地址、以及速率限制。
&gmac0 { status = "okay"; phy-mode = "sgmii"; phy-handle = <&phy0>; max-speed = <1000>; /* 时钟由外部晶振提供时不需要以下两行 */ clocks = <&clk_125m>; clock-names = "stmmaceth"; }; &mdio0 { phy0: ethernet-phy@0 { reg = <0>; /* 若硬件上复位引脚有接,可在此声明 */ reset-gpios = <&gpio 12 GPIO_ACTIVE_LOW>; reset-assert-us = <10000>; reset-deassert-us = <300000>; }; };这里有两个参数值得单独说。phy-mode写错是最常见的低级错误,写成rgmii但硬件是 SGMII,驱动会按 RGMII 的时序去配置 MAC 控制器,结果就是完全不通或者大量错误。reset-deassert-us这个值我一般会设成 300ms 而不是手册上写的最小值,前面说过,复位后内部就绪的时间有批次差异,多等一点不会有害,等少了可能出诡异问题。
另外注意max-speed。有些参考设计把它设成 100 来“求稳”,这个做法在调试阶段会掩盖问题——你以为是链路稳了,其实是根本没跑到千兆。调试期间一定设成 1000,等问题解决了再考虑要不要限制。
4.3 寄存器初始化代码:一段能直接用的骨架
下面这段是我常用的初始化骨架,C 语言,基于一套最简的 MDIO 读写接口。实际移植时你只需要实现mdio_read和mdio_write两个函数,其余逻辑可以直接用。
#define PHY_ADDR 0x00 #define REG_BMCR 0x00 #define REG_BMSR 0x01 #define REG_PHYSTS 0x17 #define REG_SPEC_CTRL 0x14 #define REG_PAGE 0x15 static int phy_wait_ready(int timeout_ms) { int i; u16 v; for (i = 0; i < timeout_ms; i++) { if (mdio_read(PHY_ADDR, REG_BMCR, &v) == 0 && !(v & 0x8000)) return 0; mdelay(1); } return -1; } int phy_init_sgmii(void) { u16 v; /* 1. 软复位,然后等它自己清掉 */ mdio_write(PHY_ADDR, REG_BMCR, 0x8000); if (phy_wait_ready(500) != 0) return -1; mdelay(50); /* 2. 确认当前速率/双工状态可读,顺手打印出来 */ mdio_read(PHY_ADDR, REG_PHYSTS, &v); pr_info("phy status after reset: 0x%04x\n", v); /* 3. 配置自协商能力:十兆/百兆 + 千兆全双工 */ mdio_write(PHY_ADDR, 0x04, 0x01E1); /* 全双工+半双工 10/100 */ mdio_write(PHY_ADDR, 0x09, 0x0200); /* 千兆全双工 */ /* 4. 使能并重启自协商 */ mdio_read(PHY_ADDR, REG_BMCR, &v); v |= (0x1000 | 0x0200); mdio_write(PHY_ADDR, REG_BMCR, v); /* 5. 轮询自协商完成 */ for (int i = 0; i < 3000; i++) { mdio_read(PHY_ADDR, REG_BMSR, &v); if (v & 0x0020) break; mdelay(1); } /* 6. 读实际协商结果,交给 MAC 侧使用 */ mdio_read(PHY_ADDR, REG_PHYSTS, &v); pr_info("negotiated: speed=%d duplex=%d link=%d\n", (v >> 10) & 0x3, (v >> 13) & 0x1, (v >> 11) & 0x1); return 0; }这段代码里有几处刻意为之的设计。第 2 步在读状态之前不做任何配置,目的是拿到一个干净的复位后快照,跟正常板子对比时用得上。第 5 步的轮询上限设成 3000ms,比标准要求的超时时间长很多,因为实际遇到过对端设备自协商很慢的情况,按标准超时判失败会导致误报。第 6 步打印出来的信息一定要留着,这是判断“PHY 认为链路是什么状态”的唯一可靠来源。
如果你的平台支持 SGMII 的 in-band 自协商,还需要额外确认 MAC 侧有没有使能这个功能。有些 MAC 控制器默认关闭,需要单独写一个配置寄存器打开,具体看主控的手册。打开之后,MAC 会自动从收到的 SGMII 配置字里解析速率和双工,不需要软件再手动配置,能省掉一整类参数不一致的问题。
4.4 验证:别只看 link up 就收工
链路起来只是第一步,真正的验证要看三层。
第一层看协商结果是否如预期。用ethtool确认速率和双工,Speed: 1000Mb/s、Duplex: Full、Link detected: yes,三个都对才算过关。如果显示Unknown!,说明驱动没拿到有效的协商信息,多半是 in-band 没通。
第二层看错误计数。连续跑几分钟流量之后,读一下接口统计:
watch -n 2 'ethtool -S eth0 | egrep "err|drop|crc|align|carrier"'正常情况下这些计数应该全程为 0 或者只在最开始有个位数。如果 CRC 错误持续增长,直接回到 4.1 检查信号完整性。如果 carrier 变化次数在涨,说明链路在反复断连,回到 3.3 检查配置顺序。
第三层看实际吞吐。用 iperf3 双向跑,时长至少 60 秒,多线程并发,这样才能压满带宽。
# 服务端 iperf3 -s -p 5201 # 客户端,上行 iperf3 -c 192.168.1.100 -p 5201 -t 60 -P 4 # 客户端,下行(反向) iperf3 -c 192.168.1.100 -p 5201 -t 60 -P 4 -R千兆链路的合理预期是 930~950Mbps,低于 900 就值得查一查。另外建议同时跑一个长时间的 ping,包大小设成 1472(加上 28 字节头刚好 1500),间隔设成 0.01 秒,跑十分钟,观察有没有偶发丢包。有些问题只有在长时间高负载下才会暴露,短时间的 iperf 测不出来。
5. 问题速查与实战踩坑记录
前面四节讲的是“应该怎么做”,这一节讲“做的时候会出什么岔子”。我把这几年在这颗 PHY 上踩过的坑整理成一张速查表,后面再展开讲三个印象最深的案例。案例里的具体场景做了脱敏处理,但技术细节是真实的。
5.1 症状与处置速查表
| 现象 | 最可能原因 | 处置方法 |
|---|---|---|
| 复位后读寄存器全 0xFF | 复位未释放或 MDIO 不通 | 查复位引脚电平,查 MDIO 上拉电阻 |
| 媒体侧 link up 但 MAC 侧 down | SGMII in-band 未使能 | 使能 MAC 侧带内自协商 |
| link 反复 up/down,周期数秒 | 参考时钟不稳或频率偏移 | 换外部时钟源验证,量晶振频率 |
| 能 ping 通但大包不通 | 双工不匹配或 MTU 配置不一致 | 检查两端双工设置,确认 MTU |
| 吞吐只有 300~600Mbps | SerDes 参数不当或 CRC 错误重传 | 看错误计数,必要时调均衡参数 |
| 协商成千兆但实际百兆速率 | 速率字段与 SerDes 实际速率不同步 | 回读 PHY 状态寄存器比对 |
| 冷启动不通,热重启正常 | 上电时序或时钟就绪顺序问题 | 延后复位释放时刻 |
| 多口板子只有部分口工作 | PHY 地址冲突或 MDIO 广播写 | 检查地址引脚,确认无广播操作 |
5.2 案例一:等了 100ms 但 PHY 还没准备好的教训
这是最早踩的一个坑,也是后来所有初始化代码都加长延时习惯的由来。当时的现象是板子冷启动后网络完全不通,重新加载驱动就好了。第一反应是驱动初始化顺序有问题,查了半天没找到原因。后来在初始化函数里加了详细的日志,发现软复位之后立刻读 0x00 寄存器,读回来是 0x8000,也就是复位位还挂着,说明内部根本没复位完。
按手册上写的“复位完成后位自动清零,最长 100ms”,我们的代码是延时 100ms 之后直接往下走,没有做轮询确认。把那块板子的延时改成 500ms 之后问题消失。后来陆续测了几批不同的板子,复位完成时间从 80ms 到 400ms 不等,跟供电爬升速度和温度都有关系。所以现在的做法是固定用轮询而不是固定延时,轮询上限给到 800ms,正常情况下几十毫秒就出来了,极端情况也能兜住。
这个经验推广开来:凡是手册上写“典型值 XX”的地方,都当成“最小值”,代码里按 3 到 5 倍去留余量。嵌入式开发里因为卡着典型值写延时导致的诡异问题,比因为参数配错导致的还多。
5.3 案例二:in-band 没开,两个状态机各说各话
第二个坑更隐蔽。现象是链路在大部分情况下正常,但每隔十几分钟就断一次,断开时间大约 2 秒,然后又自己恢复。这种周期性抖动最难查,因为它看起来像外部干扰。
后来在 dmesg 里抓到一条规律:每次断连之前,MAC 侧都会打一条“link partner not responding”,而 PHY 侧的铜缆 link 状态始终是 up。这就说明 PHY 认为链路好好的,但 MAC 认为对端没响应。查 MAC 的配置发现,带内自协商那个使能位是关着的,MAC 是按固定千兆全双工在跑。两边在绝大多数时间能对上,因为交换机也是千兆全双工,但交换机在做某种周期性的端口状态刷新时,会短暂地在控制字里改一些位,MAC 因为没有解析控制字的能力,直接把这当成链路异常处理了。
打开 MAC 侧的 in-band 之后,问题彻底消失。这个案例说明一件事:SGMII 链路的两端必须使用同一套协商机制。一端强制一端自协商的配置,在实验室里跑一小时可能都没事,但放到实际环境里就是一颗定时炸弹。
5.4 案例三:节能特性与 SGMII 的兼容性
第三个坑跟链路稳定性本身没关系,但表现完全一样。有一批设备在接入某些交换机之后,会出现不定期的丢包,丢包率不高,大约十万分之一,但 TCP 重传会让长连接的性能明显下降。抓包看是随机的 CRC 错误,但几十次之后又恢复正常。
排查了很久,最后是在交换机的端口配置里找到了原因:那台交换机默认开启了节能特性(EEE),它会在链路空闲时让 PHY 进入低功耗状态。SGMII 这条链路不支持这种低功耗握手,链路进入低功耗状态之后,恢复的过程中会有短暂的不稳定,就产生了随机的 CRC 错误。
处理办法有两个。一是本地 PHY 侧把 EEE 能力关掉,让自协商时不通告这个特性,对端就不会尝试进入低功耗状态。二是在交换机侧把对应端口的节能特性关掉,如果是自研设备就直接改配置。前者更可控,因为是我们在自己的代码里做,不依赖对端。具体做法是在扩展寄存器里找到 EEE 相关的通告位,清零之后重启自协商,然后用寄存器回读确认那个位确实变成了 0。
这个坑给我最大的启示是:排查链路问题时,除了本地配置,一定要把对端的能力集合也考虑进来。越是“默认开启”的特性越要警惕,因为它们在双方都支持时才会激活,而你的实验室里可能压根就没有支持它的对端设备。
提示:遇到“偶尔丢一点点包”这种问题,先别怀疑信号完整性。看看两端的自协商结果里都通告了哪些高级特性,把非必需的特性全关掉,很多时候问题就没了。功能越少,出问题的概率越低。
5.5 几条不写在手册里的经验
第一条是关于测试覆盖的。任何一次配置改动之后,不要只测正常路径。至少要覆盖:对端强制千兆、对端强制百兆、对端自协商三种组合。我遇到过配置只在自协商场景下正常,一旦对端强制百兆就彻底不通,原因是 PHY 侧的速率字段没有正确跟随自协商结果更新。这种问题在全是新设备的实验室里永远发现不了。
第二条是关于日志的。所有跟 PHY 相关的寄存器读写,我都在驱动里加了一级可开关的调试日志,把地址、寄存器号、读写的值、时间戳全打出来。平时关着不影响性能,出问题的时候打开,能完整回放整个初始化过程。这块代码只花半小时写,但每次救我都是几小时起步。
第三条是关于版本管理的。PHY 的初始化参数在新版硬件上可能需要微调,所以配置文件要跟硬件版本绑定。我见过有人用同一套参数烧到不同批次的板子上,一半正常一半有问题,查了两个星期才发现是批次差异。让硬件同事在板子上留一个版本标识,软件启动时读出来选择对应的参数集,这个投入产出比极高。
5.6 关于扩展寄存器和页切换的补充
最后补充一个操作细节,这个细节在文档里通常一句话带过,但实际很容易翻车。这颗 PHY 的很多关键配置放在扩展页里,访问方式是先往页地址寄存器写页号,再访问目标寄存器,完了之后要切回标准页。问题在于,如果中间有任何一次访问失败,比如 MDIO 时序出错或者总线被别的设备抢了,页指针就会停在错误的页上,后续所有操作全部错乱。
我的做法是封装成一对函数,phy_ext_write(page, reg, val)和phy_ext_read(page, reg),内部强制遵守“切页 → 操作 → 切回”的流程,而且切回不管成功失败都执行。另外在多口设备上,页指针是每个 PHY 独立的还是全局共享的,这个要看具体型号,如果是共享的,还要考虑并发访问的加锁问题。多核系统上两个核同时操作不同的 PHY,很容易因为页指针打架出现随机错误,这种错误极难复现,加锁成本又极低,别省这一步。
static DEFINE_MUTEX(phy_page_lock); int phy_ext_write(u8 addr, u16 page, u8 reg, u16 val) { int ret; mutex_lock(&phy_page_lock); ret = mdio_write(addr, REG_PAGE, page); ret |= mdio_write(addr, reg, val); ret |= mdio_write(addr, REG_PAGE, 0x0000); /* 无论成败都切回 */ mutex_unlock(&phy_page_lock); return ret; }写完这段再回看前面那些坑,你会发现大部分“不稳定”其实都能归结成两类:一类是时序没等够,一类是两端的认知没对齐。把这两类问题的检查点固化进初始化流程,剩下真正需要靠经验和仪器解决的,其实不多。我在实际项目里形成的习惯是,每块新板子第一次上电,先不跑业务,花二十分钟把上面这些检查点全部走一遍,把正常状态下的寄存器快照存下来。这份快照的价值在半年后会体现出来,当同一批次的某块板子出现问题时,把它和快照一比对,差异点基本就是根因所在。