3. PHY 基础与 MDIO 总线:从物理层到寄存器

咱们做网络驱动的,打交道最多的外设就是 PHY 了。PHY 这玩意儿,说白了就是物理层收发器。你把 MAC 送出来的数字信号转成网线上的模拟信号,反过来也一样。我刚开始接触这块的时候,总以为 PHY 就是个简单的数模转换器,后来发现这里面门道不少。

3.1 PHY 芯片工作原理

PHY 芯片干的事儿,我总结成三步:

  • 编码/解码:把 MAC 过来的数据编成适合线缆传输的码型,比如 4B/5B、8B/10B。接收端再解码回去。
  • 并/串转换:MAC 那边是并行数据,PHY 转成串行比特流送出去。反过来也一样。
  • 电平适配:把芯片内部的数字电平转成符合以太网标准的差分信号,比如 1000Base-T 的 PAM-5。

这里有个关键点:自动协商。嗯,我个人觉得这是 PHY 最实用的功能。两个 PHY 通电后,通过 FLP 脉冲互相告诉对方自己能跑多快、支持什么模式。最后协商出两边都支持的最佳配置。

一个小坑: 自动协商的结果不是实时更新的。你改了 PHY 的广告寄存器后,需要触发一次软复位或者重新开始协商,否则 PHY 会一直用旧配置。我曾经因为这个查了整整一个下午。

PHY 内部有完整的链路状态机:

  1. 上电复位:进入初始状态,开始自动协商。
  2. 自动协商进行中:收发 FLP 脉冲,确定速度和双工模式。
  3. 链路建立:协商成功,Link Up。
  4. 数据收发:正常工作状态。
  5. 链路断开:检测不到 Link 脉冲,回到初始状态。

3.2 MDIO/MDC 总线协议

MAC 怎么和 PHY 沟通呢?靠的就是 MDIO 总线。MDIO(Management Data Input/Output)是两线制串行总线,就两根线:

  • MDC:时钟线,由 MAC 提供,最高频率我见过 2.5MHz 和 12.5MHz 两种。
  • MDIO:数据线,双向,半双工。

协议帧结构很简单。你想想看,每次读写操作都是 64 个比特:

字段 长度 说明
Preamble 32 bit 全 1,同步用
Start of Frame 2 bit 固定 01
OP Code 2 bit 10 表示读,01 表示写
PHY Address 5 bit 最大支持 32 个 PHY 设备
Register Address 5 bit 每个 PHY 最多 32 个寄存器
Turnaround 2 bit 读操作时 MAC 释放总线,PHY 驱动;写操作时 MAC 直接输出 10
Data 16 bit 读写的数据内容
注意: 有的 PHY 芯片支持 Clause 45 协议,它的帧结构不一样。Clause 45 用 00 作为起始码,地址范围扩展到 16 位。这个在老项目里不常见,但做新芯片适配时一定要确认清楚。我自己就吃过这个亏,拿 Clause 22 的驱动去读 Clause 45 的寄存器,读回来的数据全是 0xFFFF。

3.3 PHY 寄存器读写方式

咱们写驱动的时候,最常用的就是 mdiobus_read 和 mdiobus_write 这两个接口。底层的物理时序,内核的 MDIO 子系统已经帮我们封装好了。

先看一个读操作的例子:

// 从 PHY 地址 0x03,寄存器 0x10 读 16 位数据
int val = mdiobus_read(bus, 0x03, 0x10);
if (val < 0) {
    pr_err("MDIO read failed: %d\n", val);
    return val;
}
pr_info("Register 0x10 = 0x%04x\n", val);

写操作类似:

// 向 PHY 地址 0x03,寄存器 0x10 写入 0x8000
int ret = mdiobus_write(bus, 0x03, 0x10, 0x8000);
if (ret < 0) {
    pr_err("MDIO write failed: %d\n", ret);
    return ret;
}

我习惯在实际项目中,读写前后加一点调试信息。尤其是排查 PHY 初始化失败的时候,光靠看代码很难定位是 MDIO 时序问题还是 PHY 芯片本身没工作。用示波器抓一下 MDC 和 MDIO 的波形,再对比一下寄存器读回来的是不是预期的值,很快就能锁定问题。

3.4 核心知识体系一览

下面的图我把 PHY、MAC、MDIO 总线的关系画了出来。你看一眼就明白数据是怎么走的了:

PHY-MDIO-MAC 核心关系图 PHY 与 MDIO 总线核心架构 MAC(主机侧) MDIO 总线 MDC(时钟) MDIO(数据) 帧结构:32bit前缀 + 起始码 + OP + 地址 + 数据 支持 Clause 22 / 45 PHY(物理层) PHY 内部结构 PMA(物理介质接入层) PCS(物理编码子层) 自动协商状态机 核心寄存器:BCR(0x00) BSR(0x01) PHYID1/2(0x02/0x03) ANAR(0x04) LPAR(0x05) 读/写操作通过 MDIO 总线,每个操作 64 个时钟周期,数据 16 位

3.5 实战经验:寄存器调试技巧

讲个我踩过的坑。有一次调试板子,PHY 死活协商不上。我用万用表量了 MDC 和 MDIO 引脚的电压,都在正常范围。然后用示波器抓波形,发现 MDC 频率太高了——驱动里配的时钟分频不对,实际跑到了 25MHz,而 PHY 只支持 12.5MHz。MDIO 的数据根本读不回来。

怎么排查的呢?我写了个内核模块,先读 PHY 的 ID 寄存器:

// 读取 PHY ID 寄存器
u32 phy_id;
phy_id = mdiobus_read(bus, phy_addr, MII_PHYSID1) << 16;
phy_id |= mdiobus_read(bus, phy_addr, MII_PHYSID2);
pr_info("PHY ID: 0x%08x\n", phy_id);

// 如果读到 0x00000000 或 0xFFFFFFFF,说明 MDIO 通信有问题

读回来全是 0xFFFF。嗯,那基本可以断定是 MDIO 时序问题了。后来把时钟分频系数改对,再读,ID 就正常了。所以我建议大家在适配一开始,先把 PHY ID 读出来确认一下。这一步能帮你省下大量排查时间。

关键寄存器一览:
  • BCR(0x00): 基本控制寄存器,包含软复位、速度选择、双工模式、自动协商使能。
  • BSR(0x01): 基本状态寄存器,包含链路状态、自动协商完成状态。
  • PHYID1(0x02)/ PHYID2(0x03): 厂商和设备识别码。
  • ANAR(0x04): 自动协商广告寄存器,告诉对方自己能支持哪些模式。
  • LPAR(0x05): 链路伙伴能力寄存器,读回对方支持的模式。

说回 Phylink 框架本身。Phylink 并不会直接调用 mdiobus_read/write,而是通过 phy_read/phy_write 这几个封装函数间接操作。它把 PHY 的状态、速率、双工、流控等信息抽象出来,咱们只需要在 phylink_mac_link_up 等回调里拿到配置,配给自己 MAC 就行了。后面的章节会详细讲回调函数的实现,这里先混个脸熟就行。