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 内部有完整的链路状态机:
- 上电复位:进入初始状态,开始自动协商。
- 自动协商进行中:收发 FLP 脉冲,确定速度和双工模式。
- 链路建立:协商成功,Link Up。
- 数据收发:正常工作状态。
- 链路断开:检测不到 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 | 读写的数据内容 |
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 总线的关系画了出来。你看一眼就明白数据是怎么走的了:
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 就行了。后面的章节会详细讲回调函数的实现,这里先混个脸熟就行。