2、PHY ID 的概念:为什么需要 PHY ID、如何定义

各位好,我是做嵌入式网络开发的老工程师了。今天聊一个新手容易忽略的点——PHY ID。你们可能已经习惯调 MDIO 去读寄存器,但有个问题:总线上要是挂了 3 个 PHY,你读的是哪个?怎么确认它是你要的那个芯片?嗯,PHY ID 就是干这个的。

为什么需要 PHY ID

我个人刚接触 Ethernet 驱动时,以为每个 PHY 只要配好地址就行。后来在项目里碰到一批混乱的 PCBA,PHY 地址冲突、型号混贴,调试时根本分不清哪个是哪个。说白了,PHY ID 就是 PHY 芯片的身份证,让你在 MDIO/MDC 总线上唯一识别出具体设备。

归纳下来,需要 PHY ID 的原因很直接:

  • 多设备区分——MDIO 总线可以挂最多 32 个 PHY,地址相同只能靠 ID 确认。
  • 型号/厂商验证——同一地址可能用不同厂商的 PHY,驱动初始化流程完全不一样。
  • 复位后自检——读回 ID 的预期值可以判断链路是否松动、芯片是否正常。
  • 热插拔/动态发现——工业网关里 PHY 可能带电接入,主机需要枚举所有 ID 来建立设备树。
  • 固件兼容判断——有些 PHY 的版本号会变,ID 中的版本域能帮助你决定是否更新驱动补丁。

大家想想看,如果你连手里这颗 PHY 是什么都不知道,后面所有的寄存器操作都是瞎蒙。所以我一直强调,读 PHY 之前,先把 ID 读完、校验完,再做其他配置。

一个小技巧:我曾经在量产测试中抓到一个怪问题——某批 PHY 读回的 ID 总是 0x0000。后来发现 MDIO 上拉电阻没焊牢,总线上根本没 PHY 应答。用 ID 全 0 这个特征可以作为“设备缺失”的判断依据。

PHY ID 的定义

标准定义来自 IEEE 802.3 第 22 章。PHY 的 ID 被固化在寄存器 2(PHY ID Register 1)和寄存器 3(PHY ID Register 2)里,这两个寄存器是只读的。整个 ID 共 32 位,分成三块:OUI(Organizationally Unique Identifier)、型号、版本。

寄存器 地址 (十进制) 位域 宽度 含义
PHY_ID1 2 15:10 6 bit OUI 的前 6 位(位 23:18 of OUI)
PHY_ID1 2 9:4 6 bit OUI 的中间 6 位(位 17:12 of OUI)
PHY_ID1 2 3:0 4 bit OUI 的最后 4 位(位 11:8 of OUI)
PHY_ID2 3 15:10 6 bit OUI 的剩余 6 位(位 7:2 of OUI)
PHY_ID2 3 9:4 6 bit 厂商定义的型号编号
PHY_ID2 3 3:0 4 bit 版本号 (Revision)

OUI 是 IEEE 分配给每家厂商的 24 位唯一码,比如 Realtek、Broadcom、Marvell 都有自己的 OUI。OUI 的低 2 位(bit 1:0)被 IEEE 保留不用,所以 PHY ID 只取了其中的 22 位。这部分我刚开始看标准时也迷惑过,后来翻到了 IEEE 802.3 的勘误表才搞明白。

实际用起来很简单:把寄存器 2 左移 16 位,再与寄存器 3 的值合并成 32 位数。然后将这个数右移 2 位(因为 OUI 原始数据少了最低 2 位),就能还原出完整的 OUI。厂商的型号字段直接查表就知道是哪个子型号。

怎么读取和验证 PHY ID

我习惯用一个简单的状态机来枚举所有可能的 PHY 地址(0~31),每读到非零且非全 F 的值就认为该地址有 PHY。下面是一个典型的读取流程(伪代码):

// 假想 MDIO 读写函数
uint16_t mdio_read(uint8_t phy_addr, uint8_t reg_addr);

void read_phy_id(uint8_t phy_addr) {
    uint16_t id1 = mdio_read(phy_addr, 2);
    uint16_t id2 = mdio_read(phy_addr, 3);
    
    if (id1 == 0x0000 || id1 == 0xFFFF) {
        // 设备不存在或链路异常
        return;
    }
    
    uint32_t oui_full = ((uint32_t)id1 << 16) | id2;
    oui_full >>= 2;  // 还原 OUI
    
    uint8_t model   = (id2 >> 4) & 0x3F;
    uint8_t revision = id2 & 0x0F;
    
    printf("PHY[%d] OUI=0x%06X Model=%d Rev=%d\n",
           phy_addr, oui_full, model, revision);
}

这段代码我在好几个项目里直接用过。注意读 PHY_ID1 时必须先确认 MDIO 总线已稳定,否则可能读到全 0 或全 F。另外,有些老 PHY(比如 10M 时代的)可能不遵循这个标准,读回来的 ID 是厂商自定义的格式,这种情况下我建议直接查对应芯片的 datasheet 确认。

注意:MDIO 读时序有严格的要求——CLK 频率通常不超过 2.5 MHz,读操作的前导码(preamble)不能省略。我曾在某个 MCU 上用 GPIO 模拟 MDIO,因为时钟没处理好,一直读回全 0。后来用示波器一看,根本没产生完整的前导码。这个坑希望大家别踩。
PHY ID 寄存器结构与读取流程 MDIO 总线访问 主机 (Master) MDIO 总线 PHY 设备 1 PHY 设备 2 PHY_ID 寄存器位域 寄存器 2 (0x02) [15:10] OUI前6位 [9:4] OUI中6位 [3:0] OUI后4位 寄存器 3 (0x03) [15:10] OUI剩余6位 [9:4] 型号编号 [3:0] 版本号 总线上每个 PHY 都须读出唯一的 32 位 ID,用于驱动识别与设备校验 解析结果 OUI + 型号 + 版本 管理侧 定义侧

这张图你大概能看清楚整体逻辑:左边是 MDIO 主机通过总线访问 PHY 设备,右边展示了寄存器 2 和寄存器 3 里 OUI、型号、版本的存放位置。最后所有信息落地成唯一标识。

实际项目中的避坑点

最后,分享几点我个人趟过的坑:

  1. 地址重复——某些 PHY 芯片上电默认地址相同(比如 0x00),如果板子上焊了两颗且没做地址引脚拉电阻,结果就是 ID 读回来都是同一颗的。我建议在硬件设计时就给每个 PHY 分配不同地址,万一无法改硬件,就靠 PHY ID 做二次校验。
  2. 读取时机——PHY 复位后需要等待至少 2 ms 才能稳定读取 ID。我见过有人在复位后立即读,结果读到 0x0000,然后误判为设备不存在。嗯,记得加延时。
  3. OUI 还原的坑——标准里 OUI 的 24 位是“倒序”放置的,你在合并 32 位后的右移 2 位操作不可省略。我之前不做这个移位,拿着 OUI 查厂商表怎么都对不上,后来翻标准才发现问题。
经验之谈:如果你在调试时不想每次都手动计算 ID,可以在驱动里加一行打印,把读取的原始 id1/id2 和还原后的 OUI 一并输出。我曾经在产测脚本里用这个方法快速定位了一批假 PHY 芯片——它们的 OUI 全是 0xFFFFFF,明显是打磨过的冒牌货。