3、PHY 基本状态寄存器(BMSR):链接状态、自协商能力、远程故障等字段解析

各位好,我们继续往下走。上一章聊了 BMCR 控制寄存器,那是我们发号施令的地方。这一章要看的 BMSR(Basic Mode Status Register),说白了就是 PHY 芯片的“体检报告”。

你写驱动的时候,读 BMSR 是家常便饭。比如判断网线有没有插好,自协商完成了没有,对方设备支不支持千兆……这些信息都藏在这个 16 位的寄存器里。我刚开始做网络驱动那会儿,总觉得读状态嘛,读回来就是了。直到有一次产品在客户现场间歇性断网,查了三天才发现是远程故障标志没处理好。嗯,从此我对 BMSR 就格外上心了。

3.1 BMSR 寄存器全景

BMSR 的地址是 0x01,IEEE 802.3 标准定义。它的位定义相当规整,我们直接看表:

位 (Bit) 名称 类型 说人话的用途
15 100BASE-T4 能力 RO 是否支持 T4 编码(基本淘汰)
14 100BASE-TX 全双工能力 RO 是否支持 100M 全双工
13 100BASE-TX 半双工能力 RO 是否支持 100M 半双工
12 10BASE-T 全双工能力 RO 是否支持 10M 全双工
11 10BASE-T 半双工能力 RO 是否支持 10M 半双工
10 100BASE-T2 全双工能力 RO T2 编码(极少见)
9 100BASE-T2 半双工能力 RO 同上,半双工版本
8 扩展状态 RO 是否有扩展状态寄存器
7 远程故障 RO/SC 对端 PHY 是否报错
6 自协商完成 RO 自协商搞定没?
5 链接状态 RO 物理链路是否建立
4 Jabber 检测 RO/SC 是否检测到异常帧
3 扩展能力 RO 是否有扩展寄存器
2-0 保留 RO 读回来一般是 0

读这张表,你可能会犯晕:这么多位,到底哪些是驱动里真正要管的?

我个人习惯,把 BMSR 的位分成三类:能力通告位、状态位、事件标志位。能力通告位是硬件出厂就焊死的,比如你的 PHY 支不支持千兆全双工,这我们改不了。状态位是实时变化的,比如链接有没有,自协商完没完。事件标志位,嗯,这玩意容易坑人。

3.2 链接状态位:驱动的心脏

Bit 5(链接状态),这是整个 PHY 驱动里最重要的一个位,没有之一。你想想看,网线没插,你读其他任何寄存器都没有意义。

这个位的行为很简单:

  • 1:物理链路建立,信号正常。
  • 0:链路断开,线缆问题或对端关机。

但这里有个大坑——寄存器是映射的。什么意思?有些 PHY 芯片的链接状态位不会自动清零。比如你拔掉网线,再插回去,如果你不重新读一遍,它可能还是 1。我遇到过一款 Marvell 的 PHY,读回来的链接状态永远是 1,后来查手册才发现需要先写一下 BMCR 的复位位才能刷新状态。嗯,这种事每个 PHY 厂商都可能干,所以你在 phylib 的 handle_interrupt 里看到轮询链路状态不是没有道理的。

警告: 不要假设读一次 BMSR 就能拿到实时链接状态。phy_read_status() 里通常会连续读两次 BMSR,并比较结果。如果两次结果一致才采信,否则再读。这是用来消除毛刺和寄存器缓存影响的经典做法。

在 phylib 框架里,phydev->link 就是从这个位解析出来的。你可以看看实现:

/* drivers/net/phy/phy-c45.c 或 phy-core.c 中的代码片段 */
int phy_read_status(struct phy_device *phydev)
{
    int bmsr;

    bmsr = phy_read(phydev, MII_BMSR);
    if (bmsr < 0)
        return bmsr;

    /* 关键就在这里:硬读一次 BMCR 来刷新 */
    phy_read(phydev, MII_BMCR);

    /* 再读一次 BMSR,取链接状态 */
    bmsr = phy_read(phydev, MII_BMSR);
    phydev->link = !!(bmsr & BMSR_LSTATUS);

    /* 这里还会解析自协商状态和远程故障 */
    ...
}

注意中间那个多余的 phy_read(phydev, MII_BMCR) 调用。这不是闲得慌,是因为很多 PHY 的设计是:读取 BMCR 寄存器会清掉 BMSR 里的一些锁存位。你不读 BMCR,BMSR 的链接状态可能还是上一次的值。这是芯片内部的硬件逻辑,手册通常不会明说,全靠经验。

3.3 自协商完成位:别急着发数据

Bit 6(自协商完成),名字很直白。网线插上去之后,两端 PHY 会通过 FLP 脉冲交换能力信息,达成一致的速度和双工模式。这个过程通常需要几百毫秒到一两秒。

这个位是核心的状态机变量:

  • 0:自协商进行中,或尚未开始。
  • 1:自协商已完成,能力达成一致。

我在项目里见过新人在驱动里写死延迟比如 mdelay(500) 等待自协商。这是不靠谱的。为什么?因为自协商时间受线缆长度、对端 PHY 性能、噪声等影响,差别可能很大。正确的做法应该是轮询 BMSR 的 Bit 6,超时退出。

建议: phylib 里有用 phy_polling_mode() 和 phy_state_machine() 来管理这个。如果你是裸写驱动,建议直接使用 phylib 的框架,它替你把状态机都写好了。别自己造轮子。

另外要注意,自协商完成位 不会自动清空。你拔了网线再插,它还是 1。所以每次链路断开重新连接时,驱动必须重新轮询这个位。怎么知道链路断过?那就是上一节讲的链接状态位变化。

3.4 远程故障位:别忽略对端递来的刀

Bit 7(远程故障),这是个容易被忽视的位。我前面提到的那个查了三天的断网问题,罪魁祸首就是它。

远程故障位是 SC(Self-Clearing) 类型。意思是它一旦被硬件置位,会一直锁存,直到你读它。你把它读出来之后,硬件自动清掉。

它表示什么?表示在对端 PHY(也就是你链路的另一头),检测到了故障。常见的触发场景:

  • 对端 PHY 过热关断。
  • 对端 PHY 检测到发送路径错误。
  • 对端电源不稳导致芯片复位。

我在实际项目中遇到过这样的情景:交换机那边的光模块不稳定,时不时丢包。交换机端口的 PHY 检测到故障,就把远程故障位置起来了。我们这边的网卡读到了这个位,却只是 phydev_err 打印了一条日志就过去了。结果链路虽然没有彻底断开,但跑起来惨不忍睹。

正确做法: 在读取 BMSR 后,检查远程故障位。如果置位,需要触发链路层重新协商。phylib 中有 PHY_RFTD 状态来处理这种事。至少你要把 phydev->link 设为 0,让协议栈重连。
/* 我当时加的补丁大致长这样 */
if (bmsr & BMSR_RFAULT) {
    phydev_warn(phydev, "远程故障收到,重新协商\n");
    phydev->link = 0;
    /* 这里触发一个自协商重启就行 */
    phy_start_aneg(phydev);
}

注意:不同厂商的 PHY 对远程故障的定义可能有细微差别。有些 PHY 的远程故障位是 Latching Low 行为,低位有效。一定要看数据手册的详细说明,不能只看位数。

3.5 能力通告位:PHY 的自我介绍

Bit 15 到 Bit 8 是能力通告位。它们告诉你这颗 PHY 芯片支持哪些速度和双工模式。

这些位是 只读(RO) 的,出厂就已经固化好了。比如一个只支持百兆的 PHY,Bit 13 和 Bit 14 是 1,但 Bit 12 和 Bit 11 一定是 0。

为什么驱动需要关心这个?

  • 在自协商过程中,驱动会把这里的值通过 ANAR(自协商通告寄存器)发给对端。
  • 在强制模式,驱动要确认用户设置的速度和双工是否在支持范围内。你要求千兆全双工,但 PHY 能力位里千兆是 0,那就只能报错。

读能力位的方法很简单:读回 BMSR,然后掩码提取。phylib 里有现成的宏:

#define BMSR_100FULL  0x4000  /* Bit 14 */
#define BMSR_100HALF  0x2000  /* Bit 13 */
#define BMSR_10FULL   0x1000  /* Bit 12 */
#define BMSR_10HALF   0x0800  /* Bit 11 */

驱动拿到这些能力后,通常会缓存到 phydev->supported 里,供后续使用。

3.6 状态检测的典型流程

在实际驱动里,读取 BMSR 的流程几乎都是固定的。我画了一张图,把核心逻辑走一遍:

图3-1 BMSR 状态检测典型流程 读 MII_BMCR(刷新缓存) 读 MII_BMSR 第一次 读 MII_BMSR 第二次 提取 Bit 5:phydev->link = !!(bmsr & BMSR_LSTATUS) 判断 Bit 7:远程故障? 是 触发重 新协商 否 检查 Bit 6:自协商完成? 说明:两次读 BMSR 是为了消除毛刺和缓存影响,很多 PHY 需要通过读 BMCR 来刷新 BMSR 的锁存位

3.7 小结

BMSR 寄存器,说复杂也不复杂。核心就三个位:链接状态(Bit 5)、自协商完成(Bit 6)、远程故障(Bit 7)。能力通告位用来确认 PHY 支持什么,平时看得少。

给你三个实战建议:

  1. 读 BMSR 一定要读两次。第一次算热身,第二次才算数。第一次读之前顺手读一下 BMCR,刷新锁存。
  2. 远程故障置位时别无视。我曾经因为没正确处理它,让产品在上线后频繁断网,后来被客户投诉。最好触发链路重新自协商。
  3. 能力通告位只在初始化阶段读取一次。这玩意固化在硬件里,不随环境变化。别在每个中断里都读,浪费总线时间。

嗯,BMSR 就讲到这里。搞清楚它,后面你读 ANAR、LPAR、扩展状态寄存器就容易多了。这些寄存器经常组合使用,BMSR 是基础中的基础。