1、PHYLIB驱动框架概述:PHY设备的概念、PHY寄存器映射、MII管理接口、Linux网络子系统中的PHY层

做网络驱动这么多年,PHY层一直是个绕不开的坎。很多初学者一上来就看datasheet,对着上千页的寄存器表发懵。其实你想想看,PHY无非就是物理层收发器——它负责把MAC出来的数字信号转成模拟信号怼到网线上,反过来也一样。搞懂它,就能搞定八成网络底层问题。

1.1 PHY设备的概念

PHY(Physical Layer)说白了就是一颗小芯片,挂在MAC的MII接口下面。我见过的PHY五花八门,有藏在大SoC里面的,也有独立一片的比如Realtek 8211系列。不管哪种,它们都干这么几件事:

  • 信号编解码:4B/5B、8B/10B、MLT-3等,把MAC给的并行数据转成线缆上的串行波形
  • 链路协商:Auto-Negotiation,自动匹配对端速率和双工模式
  • 状态反馈:Link Up/Down、速率、全半双工这些信息,通过寄存器暴露给驱动
  • 管理接口:通过MDIO/MDC读写内部寄存器

我在一个IP摄像头项目里遇到过一件怪事——插上网线灯亮,但死活ping不通。最后抓MII信号发现,PHY把速率协商成了10M半双工,而MAC侧默认配成了100M全双工。你看,PHY和MAC的配置必须一致,否则数据就乱套了。

核心要点:PHY是一个独立的状态机,它自己不处理IP包,只负责物理信号的收发和链路状态管理。驱动通过寄存器间接控制它。

1.2 PHY寄存器映射

IEEE 802.3标准给PHY定义了0~31这32个基本寄存器,前16个(0~15)是强制实现的,后16个可选。我习惯把这张表记在心里,因为九成的调试问题都出在这些寄存器上。

寄存器名称描述
0BMCR基本控制寄存器,软件复位、速率/双工选择、Loopback开关
1BMSR基本状态寄存器,Link状态、协商能力、扩展能力
2PHYIDR1PHY标识符高16位
3PHYIDR2PHY标识符低16位(含OUI和厂商型号)
4ANAR自动协商通告能力,告诉对端我能跑什么
5ANLPAR对端协商能力,看看对面能跑啥
6ANER自动协商扩展状态,协商过程中的细节
15MII PHY Specific Status厂家自定义或标准扩展状态寄存器

我记得有一次定位千兆降级到百兆的问题,就是读BMSR发现Link没起来,但看BMCR又配对了。最后查ANLPAR才知道对端只支持100M。这种事用ethtool也能看,但读寄存器才最直接。我建议调试时先读BMSR比特1.2(Link状态),再读ANLPAR看看协商结果,九成问题能锁定。

小经验:写代码读寄存器前,一定先软件复位(BMCR比特15置1再清0)。否则PHY可能还停留在上电乱态,你读到的值根本不可靠。

1.3 MII管理接口

MDIO/MDC两条线就是PHY的管理通道。MDC是时钟,MDIO是数据,时序类似I²C但简单得多。帧结构固定:起始码+操作码+PHY地址+寄存器地址+数据。一次读或写一个16位寄存器。

你想想看,MAC侧(也就是我们的网卡驱动)要想跟PHY说话,得通过这两根线。Linux内核里把这个操作抽象成mii_bus结构体,里面挂了一组read和write回调。

// mii_bus 结构体核心成员(简化)
struct mii_bus {
    const char *name;          // 总线名称
    int (*read)(struct mii_bus *bus, int phy_addr, int regnum);
    int (*write)(struct mii_bus *bus, int phy_addr, int regnum, u16 val);
    int phy_mask;              // 位掩码标记哪些PHY地址被占用
    struct phy_device *phy_map[PHY_MAX_ADDR]; // 挂载的PHY设备指针
};

某次我做FPGA验证,自己写MDIO时序模拟,忘了加起始码的空闲位,结果读回来的值全是0xFFFF。调了整整一下午,最后拿示波器看波形才明白。所以说,硬件协议栈这种东西,依赖底层时序,软件再折腾也绕不过物理信号。

1.4 Linux网络子系统中的PHY层

Linux内核把网络子系统分成了几层:套接字层 → 协议栈(TCP/IP) → 网络设备层 → MAC驱动 → PHY驱动。PHY驱动属于最底层,它直接操作MDIO总线读写寄存器,然后把PHY的状态汇报给MAC驱动。

整个框架的核心是phy_device和phy_driver。phy_device代表一颗物理PHY芯片,phy_driver则是操作这个芯片的驱动代码。Linux PHYLIB提供了通用的状态机迁移:PHY_DOWN → PHY_READY → PHY_UP → PHY_RUNNING。我每次写PHY驱动,都会对照这个状态机画一遍流程图,确保每个状态下的中断、轮询、suspend/resume都处理干净。

下图是PHY在Linux网络子系统中的位置关系,我常拿它给新同事做快速入门:

Linux网络子系统中的PHY层结构 软件层 网络协议栈 (TCP/IP) 网卡驱动 (MAC Driver) PHYLIB 核心层 硬件层 MII管理接口 (MDIO/MDC) PHY设备 (寄存器映射) 寄存器 BMCR / BMSR ANAR / ANLPAR

看到图就清楚了:网卡驱动不直接碰PHY寄存器,它通过PHYLIB提供的API(如phy_read,phy_write)间接操作。PHYLIB内部会调用mii_bus的read/write回调,最终经过MDIO总线到达PHY寄存器。这种分层设计让驱动代码复用性非常高——换颗PHY只需新写一个phy_driver,上层完全不动。

注意:有些SoC自带GMAC控制器,内部集成了MDIO控制器(如STMMAC、FEC)。这时mii_bus的回调要用SoC提供的硬件寄存器实现,不能拿GPIO模拟。我在移植Freescale i.MX6的FEC驱动时就吃过这个亏——以为通用,结果MDIO时序不对,反复复位才找到原因。

最后提一句ethool。很多人以为ethool是靠网卡驱动实现的,其实它读PHY寄存器时走的是phy_ethtool_ksettings_get,最终调用PHYLIB。所以调试PHY问题,ethtool是利器,但直接读寄存器才能拿到最原始的值。嗯,第一讲就到这里,先把框架刻在脑子里,后面深入寄存器配置时才不会迷路。