4、PHY标识符寄存器(PHYIDR1/PHYIDR2):制造商ID、型号、版本号的读取与匹配

驱动一上电,第一个要确认的问题就是:「这片PHY是谁家的?」 说白了,我们需要拿到PHY的身份证——PHYIDR1和PHYIDR2这两个寄存器。

我个人习惯,在调试新板子的时候,第一件事就是先把这两个寄存器的值dump出来看一眼。芯片通电后复位,PHY会自动把这些信息填好,咱们只需要通过MDIO总线把它们读回来。喂,别小看这两步,我曾经就是因为在设备树里写错了PHY地址,结果读回来的全是0xFFFF,白折腾半天。

4.1 寄存器的位定义

先来看看这两个寄存器的长相。标准IEEE 802.3定义的PHY ID寄存器,一共32位,拆成两个16位寄存器:

寄存器地址位域描述
PHYIDR10x02bit[15:0]OUI(组织唯一标识符)的高16位
PHYIDR20x03bit[15:10]OUI低6位
PHYIDR20x03bit[9:4]制造商定义的型号编号(6位)
PHYIDR20x03bit[3:0]版本修订号(4位)

注意OUI一共22位,放在PHYIDR1的高16位和PHYIDR2的bit[15:10]。剩下的10位分别是型号和版本。嗯,这个布局很有意思——它把OUI拆成了两段,读回来之后要拼在一起才能拿到完整的厂商ID。

关键点:实际读取时,PHYIDR1和PHYIDR2的bit[15:0]都是有效的,但OUI只用了22位。PHYIDR2的bit[9:0]是型号+版本。所以,完整的32位PHY ID = (PHYIDR1 << 16) | PHYIDR2。但注意,OUI高16位直接就是PHYIDR1,OUI低6位要从PHYIDR2的bit[15:10]取。

4.2 Linux内核里的匹配机制

驱动的匹配流程其实很简单:先读PHYIDR1和PHYIDR2,合成一个32位的PHY ID,然后拿着这个ID去遍历内核里注册的PHY驱动列表,找到对应的驱动。

内核中有个结构体叫 phy_driver,里面有一个 phy_id 和 phy_id_mask。匹配的时候用的是:

if ((phy_id & drv->phy_id_mask) == (drv->phy_id & drv->phy_id_mask))
    // 匹配成功!

为什么要加掩码?因为有些驱动可能只关注OUI,不关心型号和版本。举个例子:你可以把 phy_id_mask 设为0xFFFFF000,这样只匹配OUI的高20位,版本和型号的4位不参与匹配。这在开发初期特别有用——先让驱动跑起来,再慢慢完善。

4.3 实际读取代码

下面是我随手写的读取函数,在phy_device.c里经常能看到类似的逻辑:

int phy_read_id(struct phy_device *phydev)
{
    int phy_id;
    u32 id;

    // 读取PHYIDR1
    phy_id = mdiobus_read(phydev->mdio.bus, phydev->mdio.addr, MII_PHYSID1);
    if (phy_id < 0)
        return phy_id;

    id = phy_id << 16;

    // 读取PHYIDR2
    phy_id = mdiobus_read(phydev->mdio.bus, phydev->mdio.addr, MII_PHYSID2);
    if (phy_id < 0)
        return phy_id;

    id |= phy_id & 0xffff;

    // 合成完毕,赋值给phydev
    phydev->phy_id = id;

    return 0;
}

注意看,我把PHYIDR2的完整的16位都保留了。内核最终匹配时只用 phy_id_mask 来过滤,所以保留全值更灵活。

我曾经踩过一个坑:读PHYIDR1和PHYIDR2时,有些老PHY芯片在复位后不会立即更新这些寄存器,需要等待几十毫秒才能读到正确的值。如果你的驱动在probe()中过早读取,可能拿到0xFFFF或者0x0000,结果匹配失败。

4.4 常见厂商ID速查

翻一下内核源码 include/linux/phy.h,你会发现一堆以 PHY_ID_ 开头的宏定义。我整理了几家常见的:

厂商OUI(22位)典型型号完整PHY_ID示例
Broadcom0x001018BCM54610x2062e000
Marvell0x00504388E15120x01410e00
Realtek0x001cc1RTL8211E0x1cc10000
Micrel/KSZ0x0022b2KSZ90310x0022b100

为什么有的型号可以到几十种?因为版本号占了4位,所以同一颗芯片的不同步进版本会读出不同的PHY ID。我建议你在驱动里用掩码把版本号屏蔽掉,除非你确实需要针对某个特定版本做特殊处理。

4.5 匹配优先级与降级

内核的匹配顺序是:先精确匹配(phy_id_mask == 0xffffffff),如果找不到,再尝试泛匹配(掩码不完整的驱动)。如果最后都没有匹配上,内核会使用一个通用的PHY驱动,也就是 genphy_driver。这个通用驱动只实现了最基础的AN、链路检测等功能,很多芯片私有的能力都用不了。所以,想用好PHY,厂商驱动是必须的。

实用技巧:调试时可以在设备树里强制指定phy-mode和max-speed,但PHY ID的匹配是自动完成的。如果你发现板子上的PHY驱动没加载成功,先检查读出来的ID对不对:在/sys/bus/mdio_bus/devices/xxx/phy_id里可以查到这个数值。

4.6 流程图:PHY ID匹配流程

下面这张图把整个流程串起来了——从复位到驱动绑定:

PHY ID 匹配流程 PHY上电/复位 读取PHYIDR1 (地址0x02) 读取PHYIDR2 (地址0x03) 合成32位PHY ID 遍历phy_driver列表 匹配失败 → 使用genphy 匹配成功 → 绑定厂商驱动 掩码 (phy_id_mask) 允许忽略版本号或型号位,实现泛匹配

图中绿框是读取步骤,橙框是组合ID,紫框是遍历匹配。最终走红框或青框——如果你的PHY ID没有厂商驱动,内核就只能用通用驱动撑着。这通常意味着你失去了很多高级功能,比如EEE、省电模式、环回测试等。

4.7 避坑指南

  • PHY地址别搞错:MDIO总线上最多挂32个PHY,地址由硬件引脚决定。读不出来时先检查地址对不对,别一上来就怀疑芯片坏了。
  • 复位时序:硬件复位后至少要等30ms再读ID。我在一个项目中用了一个GPIO做PHY复位,复位后立即去读,结果读了三次都是0,差点以为是焊接问题。后来加了msleep(50),一切正常。
  • 设备树覆盖:如果设备树里指定了 compatible 属性,内核会优先使用设备树的匹配,跳过PHY ID流程。这种情况下读出来的ID可能和实际芯片不一样,比如你配错了兼容字符串。调试时建议先去掉设备树的compatible,让内核自动匹配。
  • PHY ID mask陷阱:有些内核版本里,phy_id_mask 默认是0xfffffff0,把版本号屏蔽了。但如果你需要区分不同版本,记得改掩码。我遇到过一颗芯片的某个版本有errata,驱动需要特殊处理,但掩码把版本给屏蔽了,导致所有版本都走了同一份代码,问题排查了很久。

4.8 小结

PHY ID的读取与匹配是整个PHY驱动工作的第一关。你只要把ID读回来,用内核提供的接口注册驱动,剩下的工作框架都替你做了。嗯,最关键的还是读回来的值要对——如果不对,检查硬件地址、复位时序和MDIO时钟频率。我个人建议,写驱动前先写一个小工具把裸的mdio数据读出来看看,确认无误再写驱动层。这样能省掉很多无谓的调试时间。