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 里看到轮询链路状态不是没有道理的。
在 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,超时退出。
另外要注意,自协商完成位 不会自动清空。你拔了网线再插,它还是 1。所以每次链路断开重新连接时,驱动必须重新轮询这个位。怎么知道链路断过?那就是上一节讲的链接状态位变化。
3.4 远程故障位:别忽略对端递来的刀
Bit 7(远程故障),这是个容易被忽视的位。我前面提到的那个查了三天的断网问题,罪魁祸首就是它。
远程故障位是 SC(Self-Clearing) 类型。意思是它一旦被硬件置位,会一直锁存,直到你读它。你把它读出来之后,硬件自动清掉。
它表示什么?表示在对端 PHY(也就是你链路的另一头),检测到了故障。常见的触发场景:
- 对端 PHY 过热关断。
- 对端 PHY 检测到发送路径错误。
- 对端电源不稳导致芯片复位。
我在实际项目中遇到过这样的情景:交换机那边的光模块不稳定,时不时丢包。交换机端口的 PHY 检测到故障,就把远程故障位置起来了。我们这边的网卡读到了这个位,却只是 phydev_err 打印了一条日志就过去了。结果链路虽然没有彻底断开,但跑起来惨不忍睹。
/* 我当时加的补丁大致长这样 */
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.7 小结
BMSR 寄存器,说复杂也不复杂。核心就三个位:链接状态(Bit 5)、自协商完成(Bit 6)、远程故障(Bit 7)。能力通告位用来确认 PHY 支持什么,平时看得少。
给你三个实战建议:
- 读 BMSR 一定要读两次。第一次算热身,第二次才算数。第一次读之前顺手读一下 BMCR,刷新锁存。
- 远程故障置位时别无视。我曾经因为没正确处理它,让产品在上线后频繁断网,后来被客户投诉。最好触发链路重新自协商。
- 能力通告位只在初始化阶段读取一次。这玩意固化在硬件里,不随环境变化。别在每个中断里都读,浪费总线时间。
嗯,BMSR 就讲到这里。搞清楚它,后面你读 ANAR、LPAR、扩展状态寄存器就容易多了。这些寄存器经常组合使用,BMSR 是基础中的基础。