4、PHY标识符寄存器(PHYIDR1/PHYIDR2):制造商ID、型号、版本号的读取与匹配
驱动一上电,第一个要确认的问题就是:「这片PHY是谁家的?」 说白了,我们需要拿到PHY的身份证——PHYIDR1和PHYIDR2这两个寄存器。
我个人习惯,在调试新板子的时候,第一件事就是先把这两个寄存器的值dump出来看一眼。芯片通电后复位,PHY会自动把这些信息填好,咱们只需要通过MDIO总线把它们读回来。喂,别小看这两步,我曾经就是因为在设备树里写错了PHY地址,结果读回来的全是0xFFFF,白折腾半天。
4.1 寄存器的位定义
先来看看这两个寄存器的长相。标准IEEE 802.3定义的PHY ID寄存器,一共32位,拆成两个16位寄存器:
| 寄存器 | 地址 | 位域 | 描述 |
|---|---|---|---|
| PHYIDR1 | 0x02 | bit[15:0] | OUI(组织唯一标识符)的高16位 |
| PHYIDR2 | 0x03 | bit[15:10] | OUI低6位 |
| PHYIDR2 | 0x03 | bit[9:4] | 制造商定义的型号编号(6位) |
| PHYIDR2 | 0x03 | bit[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示例 |
|---|---|---|---|
| Broadcom | 0x001018 | BCM5461 | 0x2062e000 |
| Marvell | 0x005043 | 88E1512 | 0x01410e00 |
| Realtek | 0x001cc1 | RTL8211E | 0x1cc10000 |
| Micrel/KSZ | 0x0022b2 | KSZ9031 | 0x0022b100 |
为什么有的型号可以到几十种?因为版本号占了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匹配流程
下面这张图把整个流程串起来了——从复位到驱动绑定:
图中绿框是读取步骤,橙框是组合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数据读出来看看,确认无误再写驱动层。这样能省掉很多无谓的调试时间。