在Linux网络设备驱动开发中,MDIO(Management Data Input/Output)子系统扮演着至关重要的角色。作为连接MAC控制器和PHY芯片的桥梁,MDIO总线负责管理和配置物理层设备。本文将深入剖析MDIO子系统中的三大核心数据结构:mii_bus、phy_device和phy_driver,揭示它们的设计哲学和实现细节。
struct mii_bus是内核中对MDIO总线控制器的软件抽象,相当于一个交通指挥中心,负责协调总线上所有PHY设备的通信。这个结构体定义在include/linux/phy.h中,包含了管理物理MDIO总线所需的所有信息和操作接口。
总线操作函数指针:
c复制int (*read)(struct mii_bus *bus, int addr, int regnum);
int (*write)(struct mii_bus *bus, int addr, int regnum, u16 val);
这两个函数指针是MDIO控制器的核心,相当于总线的"读写能力"。驱动开发者必须实现这两个函数,它们将高层MDIO请求转换为具体的硬件寄存器操作。例如,在Marvell的88E1111 PHY驱动中,read函数最终会操作SoC的MDIO控制寄存器来完成实际的数据读取。
总线状态管理:
c复制enum {
MDIOBUS_ALLOCATED = 1,
MDIOBUS_REGISTERED,
MDIOBUS_UNREGISTERED,
MDIOBUS_RELEASED,
} state;
这个枚举类型跟踪总线的生命周期状态,确保总线在被使用时不会被意外释放。内核通过状态检查来防止竞态条件的发生。
设备映射表:
c复制struct mdio_device *mdio_map[PHY_MAX_ADDR];
这是一个重要的数组,保存了总线上每个可能地址(0-31)对应的设备指针。当我们在地址5发现一个PHY时,mdio_map[5]就会指向对应的phy_device结构。
在编写MDIO控制器驱动时,开发者需要完成以下关键步骤:
c复制struct mii_bus *bus = mdiobus_alloc();
if (!bus)
return -ENOMEM;
c复制bus->name = "my_mdio_bus";
bus->read = my_mdio_read;
bus->write = my_mdio_write;
bus->parent = &pdev->dev;
c复制err = mdiobus_register(bus);
if (err) {
mdiobus_free(bus);
return err;
}
注意:在实现read/write函数时,必须处理好并发访问问题。内核已经提供了mdio_lock互斥锁,但硬件层面的原子性仍需驱动开发者保证。
struct phy_device代表一个具体的PHY芯片实例,相当于每个网络设备的"身份证"和"健康档案"。它不仅记录PHY的静态信息(如厂商ID、型号),还维护动态状态(如链路状态、速度等)。
标识信息:
c复制u32 phy_id;
struct phy_c45_device_ids c45_ids;
unsigned is_c45:1;
phy_id是PHY的身份证号,由两个16位寄存器组成(ID1和ID2)。对于符合IEEE 802.3 Clause 45标准的PHY,is_c45标志会被设置,此时c45_ids将包含更详细的设备信息。
链路状态信息:
c复制unsigned link:1;
int speed;
int duplex;
这些字段反映了PHY的当前连接状态。link表示是否有物理连接,speed和duplex记录协商结果(如SPEED_1000、DUPLEX_FULL)。驱动需要定期更新这些状态,通常通过轮询或中断实现。
能力描述:
c复制__ETHTOOL_DECLARE_LINK_MODE_MASK(supported);
__ETHTOOL_DECLARE_LINK_MODE_MASK(advertising);
这两个位掩码分别表示PHY支持的能力和当前广告的能力。例如,当PHY支持1000BaseT全双工时,会在supported掩码中设置相应的位。
phy_device内部维护了一个状态机,通过state字段表示当前状态:
c复制enum phy_state {
PHY_DOWN = 0,
PHY_READY,
PHY_HALTED,
PHY_UP,
PHY_RUNNING,
PHY_NOLINK,
PHY_FORCING,
PHY_CHANGELINK,
PHY_HALTED,
PHY_RESUMING
};
典型的状态转移过程如下:
经验分享:调试PHY状态问题时,可以通过
phy_state_to_str()函数将state转换为可读字符串,方便日志记录。
struct phy_driver定义了针对特定PHY芯片的操作方法,相当于PHY的"驱动程序接口规范"。每个PHY型号都需要一个对应的phy_driver实现。
PHY驱动通过以下字段识别支持的设备:
c复制u32 phy_id;
u32 phy_id_mask;
匹配过程实际上是检查(phydev->phy_id & phy_id_mask) == phy_id。这种设计允许一个驱动支持多个相似型号的PHY芯片。
例如,Realtek 8211F系列驱动的配置:
c复制static struct phy_driver realtek_drvs[] = {
{
.phy_id = 0x001cc916,
.phy_id_mask = 0x001fffff,
.name = "Realtek RTL8211F Gigabit Ethernet",
/* 操作函数 */
}
};
这里phy_id_mask的低21位被置1,表示只关心PHY ID的前21位,这样同一个驱动可以支持RTL8211F及其衍生型号。
配置与状态读取:
c复制int (*config_init)(struct phy_device *phydev);
int (*read_status)(struct phy_device *phydev);
config_init用于初始化PHY的默认配置,而read_status则读取当前链路状态。几乎所有PHY驱动都必须实现这两个函数。
自动协商处理:
c复制int (*config_aneg)(struct phy_device *phydev);
int (*aneg_done)(struct phy_device *phydev);
自动协商是以太网PHY确定最佳连接参数的过程。config_aneg配置协商参数,aneg_done检查协商是否完成。
中断处理:
c复制int (*config_intr)(struct phy_device *phydev);
irqreturn_t (*handle_interrupt)(struct phy_device *phydev);
现代PHY通常支持中断来通知链路状态变化,这些函数负责中断的配置和处理。
注册PHY驱动的标准做法是使用module_phy_driver宏:
c复制module_phy_driver(realtek_drvs);
MODULE_DESCRIPTION("Realtek PHY driver");
MODULE_AUTHOR("Author Name");
MODULE_LICENSE("GPL");
这个宏会自动生成模块的init和exit函数,简化了驱动开发流程。
图3展示了mii_bus、phy_device和phy_driver之间的关系。我们可以用一个公司的管理架构来类比:
具体交互过程如下:
以读取PHY寄存器为例,调用栈如下:
调试技巧:在MDIO操作失败时,可以通过ftrace捕获完整的调用栈,帮助定位问题层次。
IEEE 802.3定义了两种MDIO规范:
现代驱动通常需要同时支持两种模式。phy_device中的is_c45标志指示设备使用的模式,驱动应根据此标志调整访问方式。
PHY驱动需要实现suspend/resume回调以支持电源管理:
c复制static int phy_suspend(struct phy_device *phydev)
{
/* 保存关键寄存器值 */
/* 配置低功耗模式 */
return 0;
}
static int phy_resume(struct phy_device *phydev)
{
/* 恢复寄存器配置 */
/* 重新启动自动协商 */
return 0;
}
调试技巧:
phy_register_fixup()注册修复函数,解决硬件兼容性问题ethtool -d查看PHY寄存器dump/sys/kernel/debug/mdio_bus/下的调试信息性能优化:
phydev->state_queue.delay)/sys/bus/mdio_bus/devices)当驱动未能正确绑定到PHY时:
phy_id = phy_read(phydev, MII_PHYSID1)获取实际PHY ID在实际项目中,我曾遇到过一个典型案例:某定制板卡的PHY无法被识别。通过分析发现,硬件设计将PHY地址配置为8,而驱动默认只探测0-7地址。通过在设备树中添加明确的PHY节点并指定地址,最终解决了这个问题。