深入解析MDIO总线:PHY设备扫描与驱动注册机制

📅 发布时间:2026/7/11 14:05:08 👁️ 浏览次数:
深入解析MDIO总线:PHY设备扫描与驱动注册机制
1. MDIO总线网络硬件的“电话总机”如果你拆开过家里的路由器或者开发板大概率会看到一颗主要的网络芯片MAC和一颗或多颗独立的PHY芯片。它们之间通过几根线连接进行数据和控制信息的交换。这个通信的“幕后功臣”就是MDIO总线。你可以把它想象成网络硬件内部的“电话总机”系统。MAC控制器是“大脑”负责处理高层网络协议和数据处理PHY芯片则是“翻译官”和“信号兵”负责把数字信号转换成能在网线上跑的模拟信号并管理物理连接状态比如连接速度是100M还是1000M是全双工还是半双工。MDIO总线就是大脑用来指挥和询问翻译官的那条专用指挥线路。它基于经典的I2C/SPI那种主从架构但更简单通常就两根线——MDC时钟线和MDIO双向数据线。主设备通常是MAC或CPU通过这条总线去读写从设备PHY内部的一系列寄存器从而完成PHY的配置、状态读取和控制。在Linux内核的网络子系统里MDIO总线不仅仅是一条物理线路更是一套完整的软件抽象和管理框架。这套框架的核心目的是让内核能够以一种统一、标准化的方式去发现、识别和管理网络上各种各样的PHY芯片无论它是Marvell的、Realtek的还是Microchip的。这就好比电话总机系统它不关心你接的是哪个品牌的电话机它只提供一套标准的拨号、接听和挂断流程。MDIO总线框架负责扫描总线上挂了哪些“电话机”PHY设备并为它们找到对应的“使用说明书”PHY驱动。理解这套机制对于从事嵌入式网络开发、内核网络驱动调试甚至是简单的网络故障排查比如为什么网卡识别不到PHY都至关重要。很多新手在调试网卡时遇到“No PHY found”这样的错误就一头雾水其实问题往往就出在MDIO总线扫描或驱动匹配这个环节。接下来我就带你深入这个“电话总机”的内部看看它是如何工作的。2. 总线基石MDIO总线类型的注册与初始化任何设备驱动模型都需要一个“舞台”在Linux内核里这个舞台就是总线类型struct bus_type。USB设备挂在USB总线上PCI设备挂在PCI总线上同样PHY设备也需要挂在一条虚拟的、逻辑上的MDIO总线上。这条逻辑总线是连接PHY设备和PHY驱动的桥梁。内核启动早期在子系统的初始化阶段会调用mdio_bus_init()函数。这个函数干了两件关键事代码虽然不长但意义重大int __init mdio_bus_init(void) { int ret; ret class_register(mdio_bus_class); if (!ret) { ret bus_register(mdio_bus_type); if (ret) class_unregister(mdio_bus_class); } return ret; }首先它注册了一个名为mdio_bus_class的类。在/sys/class/目录下你会看到mdio_bus这个文件夹这就是它的体现。类的概念主要用于在用户空间提供一个统一的视图方便进行电源管理等。其次也是更核心的一步它注册了MDIO总线类型mdio_bus_type。我们来看看这个总线类型的定义它决定了这条总线上的“游戏规则”struct bus_type mdio_bus_type { .name mdio_bus, .match mdio_bus_match, .pm MDIO_BUS_PM_OPS, }; EXPORT_SYMBOL(mdio_bus_type);.name: 总线的名字就是“mdio_bus”。.match: 这是总线最核心的函数指针之一指向mdio_bus_match函数。它的职责就是当一个设备device和一个驱动driver都被注册到这条总线上时判断它们俩是否“看对眼”了即这个驱动是否能管理这个设备。我们后面会详细拆解这个匹配过程它是PHY驱动能否绑定到PHY设备的关键。.pm: 电源管理操作集定义了总线级别电源管理的回调函数。当bus_register(mdio_bus_type)执行成功后内核的设备模型里就正式有了mdio_bus这条逻辑总线。以后所有的PHY设备struct phy_device在创建时都会声明自己是属于mdio_bus_type的同样所有的PHY驱动struct phy_driver在注册时也会声明自己要服务于mdio_bus_type。总线就像婚介所设备和驱动就像男女嘉宾.match函数就是婚介所的匹配算法。这里有个非常重要的概念需要厘清这个mdio_bus_type是全局的、唯一的逻辑总线。而我们在具体网卡驱动比如stmmac、igb里通过mdiobus_register注册的是一个具体的struct mii_bus实例你可以把它理解为一条具体的“电话线路”或者一个“分机交换机”。这个mii_bus实例包含了具体的硬件操作函数read,write它最终会“接入”到全局的mdio_bus_type这个总机系统中。设备是挂在具体的mii_bus实例上但驱动是注册在全局的mdio_bus_type上由总线类型来负责全局的匹配。3. 设备发现MDIO总线扫描与PHY设备创建逻辑总线搭好了接下来就要在具体的硬件总线上“找人”了。这个过程通常发生在具体的网络控制器MAC驱动初始化时。以常见的STMMACSynopsys的以太网控制器驱动为例在它的stmmac_mdio_register函数中会进行以下操作3.1 创建并注册一条具体的MDIO总线首先驱动需要分配并初始化一个struct mii_bus对象。这个结构体是具体硬件总线的抽象里面包含了操作这块特定硬件所必需的所有信息。new_bus mdiobus_alloc(); // 1. 分配内存 if (new_bus NULL) return -ENOMEM; // 2. 填充关键的操作函数 new_bus-name stmmac; new_bus-read stmmac_mdio_read; // 硬件相关的读PHY寄存器函数 new_bus-write stmmac_mdio_write; // 硬件相关的写PHY寄存器函数 new_bus-reset stmmac_mdio_reset; // 硬件相关的复位函数 new_bus-priv ndev; // 通常把网络设备私有数据挂在这里方便回调时使用 // 3. 注册这条总线 err mdiobus_register(new_bus);mdiobus_register是这个环节的核心入口。它不仅仅是将这个mii_bus实例记录到内核更关键的是它会立刻触发对这条总线上所有可能地址的扫描试图发现PHY设备。3.2 深入扫描过程mdiobus_scan与get_phy_devicemdiobus_register内部会调用mdiobus_scan。这个函数的行为非常直观它就像一个尽职的接线员拿着电话本PHY地址从0到31依次拨打每个分机号看看有没有人接听。// 简化逻辑非实际代码 for (addr 0; addr PHY_MAX_ADDR; addr) { // 跳过被掩码屏蔽的地址 if (bus-phy_mask (1 addr)) continue; // 关键调用尝试在地址addr上获取PHY设备 phydev get_phy_device(bus, addr, is_c45); if (!IS_ERR(phydev)) { // 获取成功说明该地址有设备然后注册它 err phy_device_register(phydev); ... } }get_phy_device函数是“确认设备身份”的关键一步。它通过调用我们刚才设置的bus-read函数去读取PHY芯片上的两个标准寄存器PHYIDR1 (Register 2): 读取厂商ID的高16位。PHYIDR2 (Register 3): 读取厂商ID的低16位和型号ID的高4位。如果这两个寄存器的值都是0或者全10xffff通常认为这个地址上没有有效的PHY设备。如果读到了有效的、非0非0xffff的ID内核就确信“啊哈这里有一个PHY”。接着它会调用phy_device_create来为这个PHY创建一个内核设备对象struct phy_device。3.3 创建设备对象phy_device_create这个函数创建了代表PHY的“身份证”和“档案袋”。我们看看它做了哪些关键初始化struct phy_device *phy_device_create(struct mii_bus *bus, int addr, int phy_id, bool is_c45, struct phy_c45_device_ids *c45_ids) { struct phy_device *dev; struct mdio_device *mdiodev; dev kzalloc(sizeof(*dev), GFP_KERNEL); ... mdiodev dev-mdio; // 将设备与具体的总线实例关联 mdiodev-bus bus; mdiodev-addr addr; // PHY的硬件地址0-31 // **至关重要**指明这个设备属于哪种总线类型 mdiodev-dev.bus mdio_bus_type; // 指向全局的MDIO总线类型 mdiodev-dev.parent bus-dev; // 父设备是具体的mii_bus // 设置匹配函数 mdiodev-bus_match phy_bus_match; // 这是PHY设备特有的匹配逻辑 // 保存PHY的唯一标识 dev-phy_id phy_id; // 从寄存器读出的ID dev-is_c45 is_c45; // 是Clause 22还是Clause 45协议 ... }创建完成后phy_device_register会被调用最终通过device_add将这个phy_device内部的mdio_device-dev一个标准的struct device添加到内核设备模型中。至此一个PHY设备就在系统中“上户口”了。你可以在/sys/bus/mdio_bus/devices/目录下看到它它的名字通常包含了总线名和地址比如stmmac-0:00。这里我踩过一个坑早期调试时发现/sys/bus/mdio_bus/devices/目录下空空如也。排查了半天最后发现是MAC驱动里的bus-read函数实现有误读取PHY ID寄存器总是返回0导致get_phy_device认为所有地址都没有设备。所以这个扫描过程完全依赖于底层驱动提供的read函数能正确工作。4. 驱动登场PHY驱动的注册与匹配机制设备已经被总线扫描到并注册好了静静地躺在总线上等待它的“真命天子”——也就是驱动。PHY驱动通常以内核模块的形式存在比如drivers/net/phy/realtek.c里面定义了特定型号PHY如RTL8211F的驱动。4.1 驱动的注册宏module_phy_driverPHY驱动的编写者通常不会直接调用复杂的注册函数而是使用一个非常方便的宏// 在 realtek.c 的末尾 module_phy_driver(realtek_drivers); // 这个宏展开后实际上创建了一个模块初始化函数 static int __init phy_module_init(void) { return phy_drivers_register(realtek_drivers, ARRAY_SIZE(realtek_drivers), THIS_MODULE); } module_init(phy_module_init);phy_drivers_register会遍历传入的驱动数组对每个驱动调用phy_driver_register。这个函数是驱动注册的核心它完成了驱动与总线类型的绑定int phy_driver_register(struct phy_driver *drv, struct module *owner) { // 标记这是一个PHY驱动 drv-mdiodrv.flags | MDIO_DEVICE_IS_PHY; // 设置驱动名称 drv-mdiodrv.driver.name drv-name; // **关键绑定**声明本驱动服务于 mdio_bus_type 总线 drv-mdiodrv.driver.bus mdio_bus_type; // 设置探针和移除函数 drv-mdiodrv.driver.probe phy_probe; drv-mdiodrv.driver.remove phy_remove; drv-mdiodrv.driver.owner owner; // 将驱动注册到内核设备模型 return driver_register(drv-mdiodrv.driver); }当driver_register被调用时内核的设备模型核心代码会被触发。它会将这个驱动加入到mdio_bus_type的驱动列表中并立刻尝试为这条总线上所有已经注册的、还没有绑定驱动的设备进行匹配。4.2 核心匹配流程从总线.match到驱动.probe匹配过程是一个经典的“总线-设备-驱动”三角恋由总线类型充当裁判。流程如下驱动注册触发匹配driver_register-bus_add_driver-driver_attach。遍历设备内核会遍历mdio_bus_type总线上所有的设备就是我们之前注册的phy_device。调用总线匹配函数对每个设备调用总线的.match方法也就是我们之前看到的mdio_bus_match。PHY专用匹配mdio_bus_match会调用设备在创建时设置的mdiodev-bus_match方法对于PHY设备这就是phy_bus_match函数。phy_bus_match是决定“姻缘”成败的最终裁判。它的逻辑清晰而严格static int phy_bus_match(struct device *dev, struct device_driver *drv) { struct phy_device *phydev to_phy_device(dev); struct phy_driver *phydrv to_phy_driver(drv); // 1. 检查驱动是否标记为PHY驱动 if (!(phydrv-mdiodrv.flags MDIO_DEVICE_IS_PHY)) return 0; // 2. 如果驱动提供了自定义的匹配函数则优先使用 if (phydrv-match_phy_device) return phydrv-match_phy_device(phydev); // 3. 标准匹配比较PHY ID if (phydev-is_c45) { // Clause 45 复杂一些需要匹配多个设备ID ... } else { // **最常用的Clause 22匹配逻辑** // 用驱动定义的掩码(phy_id_mask)取出设备PHY ID和驱动PHY ID的有效位进行比较 return (phydrv-phy_id phydrv-phy_id_mask) (phydev-phy_id phydrv-phy_id_mask); } }这就是最精髓的部分每个PHY驱动在定义时都会声明自己的phy_id和phy_id_mask。例如Realtek RTL8211F驱动的定义中static struct phy_driver realtek_drivers[] { { .phy_id 0x001cc916, .name RTL8211F Gigabit Ethernet, .phy_id_mask 0x001fffff, ... } }phy_id_mask 0x001fffff意味着它只关心PHY ID的21个低位因为不同封装的RTL8211F可能高位有细微差别。当总线扫描到一个PHY读出的ID是0x001cc916那么(0x001cc916 0x001fffff) (0x001cc916 0x001fffff)成立匹配成功匹配成功调用探针如果phy_bus_match返回一个正数表示匹配内核就会调用该驱动的.probe方法即phy_probe。在phy_probe中驱动会完成对PHY芯片的最终初始化配置中断、链接状态回调等并将phydev-drv指针指向自己表示“这个设备归我管了”。5. 实战串联从MAC驱动到PHY就绪的完整链条现在我们把整个链条串起来看一个真实的网卡从初始化到PHY就绪的完整过程。以Linux内核中常见的stmmac以太网驱动为例平台/PCI探测内核发现一个STMMAC控制器硬件。网络设备初始化stmmac_probe被调用初始化MAC控制器分配net_device。注册MDIO总线调用stmmac_mdio_register。分配mii_bus。填充read,write,reset等硬件操作函数。这些函数是直接操作SoC或MAC内部MDIO控制器的寄存器与具体PHY型号无关。调用mdiobus_register。触发mdiobus_scan遍历地址0-31。对每个地址通过bus-read读取PHY ID。发现有效PHY后创建并注册phy_device到mdio_bus_type。加载PHY驱动模块系统自动或手动加载realtek.ko。module_init调用phy_driver_register。驱动被添加到mdio_bus_type。触发总线匹配过程遍历所有PHY设备。phy_bus_match将设备的phy_id与驱动的phy_id/phy_id_mask比对。匹配成功调用phy_probe驱动初始化PHY芯片。MAC与PHY连接MAC驱动如stmmac在后续初始化中会调用phy_connect函数。phydev phy_connect(netdev, bus_id, stmmac_adjust_link, interface);这个函数通过总线名和地址找到之前已经注册并绑定好驱动的phy_device并建立一个重要的联系设置PHY状态改变链接UP/DOWN、速度变化时的回调函数stmmac_adjust_link。这样当网线插拔或协商速度改变时PHY驱动会通过这个回调函数通知MAC驱动MAC驱动随之调整自己的工作模式。一个常见的调试技巧如果网卡不起来你可以按这个链条检查。查看dmesg是否有stmmac和PHY如rtl8211f的探测成功日志。在/sys/bus/mdio_bus/devices/下查看PHY设备是否存在。在/sys/bus/mdio_bus/drivers/下查看PHY驱动是否绑定。使用ethtool -m命令可以读取PHY的厂商和型号ID与驱动中的定义对比确认是否匹配。理解MDIO总线的扫描与驱动注册机制就像掌握了网络硬件启动的“开关顺序”。它虽然藏在内核底层但却是网络功能正常工作的基石。下次再遇到PHY相关的问题希望你能沿着这条线索快速定位到问题的根源。