MDIO总线驱动开发实战Linux内核4.19下PHY寄存器读写3种方法在嵌入式Linux驱动开发中MDIO总线作为MAC与PHY之间的管理接口其驱动开发是网络设备开发的关键环节。本文将深入探讨Linux内核4.19环境下三种不同的PHY寄存器操作方法每种方法都配有可直接编译的内核驱动代码示例。1. MDIO总线基础与内核框架MDIOManagement Data Input/Output总线是以太网标准中定义的二线制串行接口由MDC时钟线和MDIO数据线组成。在Linux内核中MDIO子系统提供了完整的框架支持主要包括以下几个核心组件mii_bus结构体表示一个MDIO总线实例phy_device结构体表示连接的PHY设备MDIO总线驱动实现具体SoC的MDIO控制器操作内核中MDIO操作遵循两种协议规范Clause 22传统5位地址空间最大支持32个PHYClause 45扩展16位地址空间支持更多PHY设备以下是一个典型的MDIO总线注册代码框架static struct mii_bus *my_mii_bus; my_mii_bus mdiobus_alloc(); if (!my_mii_bus) { return -ENOMEM; } my_mii_bus-name my_mdio; my_mii_bus-read my_mdio_read; my_mii_bus-write my_mdio_write; my_mii_bus-priv private_data; if (mdiobus_register(my_mii_bus)) { mdiobus_free(my_mii_bus); return -ENXIO; }2. 方法一通过mii_bus结构体直接操作这是最基础的MDIO操作方法直接使用mii_bus提供的读写函数。这种方法适合需要精细控制MDIO时序或特殊PHY设备的情况。2.1 核心API函数int mdiobus_read(struct mii_bus *bus, int addr, int regnum); int mdiobus_write(struct mii_bus *bus, int addr, int regnum, u16 val);2.2 完整示例代码#include linux/module.h #include linux/mii.h static struct mii_bus *mdio_bus; static int __init mdio_demo_init(void) { int phy_id 1; // PHY地址 int reg MII_BMCR; // 基本模式控制寄存器 u16 value; // 获取系统中第一个MDIO总线 mdio_bus mdiobus_get_by_name(fixed-0); if (!mdio_bus) { pr_err(Failed to get MDIO bus\n); return -ENODEV; } // 读取PHY寄存器 value mdiobus_read(mdio_bus, phy_id, reg); if (value 0) { pr_err(Read failed\n); return value; } pr_info(PHY 0x%x reg 0x%x 0x%04x\n, phy_id, reg, value); // 写入PHY寄存器 value | BMCR_ANENABLE; // 使能自动协商 if (mdiobus_write(mdio_bus, phy_id, reg, value) 0) { pr_err(Write failed\n); return -EIO; } return 0; } static void __exit mdio_demo_exit(void) { if (mdio_bus) mdiobus_put(mdio_bus); } module_init(mdio_demo_init); module_exit(mdio_demo_exit); MODULE_LICENSE(GPL);2.3 操作要点通过mdiobus_get_by_name()获取已注册的MDIO总线实例读写操作需要指定PHY地址和寄存器号Clause 45设备需要使用特殊寄存器访问方式操作完成后需要调用mdiobus_put()释放引用3. 方法二通过phy_device抽象层操作Linux内核提供了更上层的PHY抽象层通过phy_device结构体管理PHY设备这种方法更符合内核设计理念。3.1 PHY设备核心APIstruct phy_device *phy_find_first(struct mii_bus *bus); int phy_read(struct phy_device *phydev, u32 regnum); int phy_write(struct phy_device *phydev, u32 regnum, u16 val);3.2 完整示例代码#include linux/module.h #include linux/phy.h static struct phy_device *phydev; static int __init phy_demo_init(void) { struct mii_bus *bus; int reg MII_BMCR; u16 value; // 获取MDIO总线 bus mdiobus_get_by_name(fixed-0); if (!bus) { pr_err(MDIO bus not found\n); return -ENODEV; } // 查找第一个PHY设备 phydev phy_find_first(bus); if (!phydev) { pr_err(No PHY device found\n); mdiobus_put(bus); return -ENODEV; } // 读取PHY寄存器 value phy_read(phydev, reg); pr_info(PHY reg 0x%x 0x%04x\n, reg, value); // 修改并写回寄存器 value | BMCR_FULLDPLX; phy_write(phydev, reg, value); mdiobus_put(bus); return 0; } static void __exit phy_demo_exit(void) { // PHY设备引用由内核管理无需显式释放 } module_init(phy_demo_init); module_exit(phy_demo_exit); MODULE_LICENSE(GPL);3.3 操作优势自动处理PHY设备的发现和初始化支持PHY状态变化通知机制提供更高级的功能如PHY状态机管理内置PHY驱动框架支持多种PHY芯片4. 方法三内存映射直接操作寄存器对于某些嵌入式SoCMDIO控制器寄存器可能直接映射到内存空间此时可以直接操作硬件寄存器。4.1 寄存器映射关键步骤获取寄存器物理地址使用ioremap映射到虚拟地址空间通过读写操作寄存器4.2 完整示例代码#include linux/module.h #include linux/io.h #define MDIO_BASE 0x1E720000 #define MDIO_CTRL 0x00 #define MDIO_DATA 0x04 #define MDIO_ADDR 0x08 static void __iomem *mdio_regs; static u16 mdio_direct_read(int phy_id, int reg) { // 设置PHY地址和寄存器号 writel((phy_id 8) | reg, mdio_regs MDIO_ADDR); // 触发读操作 writel(0x1, mdio_regs MDIO_CTRL); // 等待操作完成 while (readl(mdio_regs MDIO_CTRL) 0x1); return readw(mdio_regs MDIO_DATA); } static void mdio_direct_write(int phy_id, int reg, u16 val) { // 写入数据 writew(val, mdio_regs MDIO_DATA); // 设置PHY地址和寄存器号 writel((phy_id 8) | reg, mdio_regs MDIO_ADDR); // 触发写操作 writel(0x3, mdio_regs MDIO_CTRL); // 等待操作完成 while (readl(mdio_regs MDIO_CTRL) 0x1); } static int __init direct_mdio_init(void) { int phy_id 1; int reg MII_BMCR; // 映射寄存器 mdio_regs ioremap(MDIO_BASE, 0x100); if (!mdio_regs) { pr_err(Failed to map MDIO registers\n); return -ENOMEM; } // 读取演示 u16 val mdio_direct_read(phy_id, reg); pr_info(Direct read: 0x%04x\n, val); // 写入演示 mdio_direct_write(phy_id, reg, val | BMCR_SPEED100); return 0; } static void __exit direct_mdio_exit(void) { if (mdio_regs) iounmap(mdio_regs); } module_init(direct_mdio_init); module_exit(direct_mdio_exit); MODULE_LICENSE(GPL);4.3 注意事项需要准确了解SoC的MDIO控制器寄存器布局必须处理内存屏障和字节序问题通常需要实现完整的MDIO总线操作函数不如内核标准接口稳定可能随硬件变化而失效5. 三种方法对比与选择建议方法复杂度可移植性功能完整性适用场景mii_bus直接操作中高基础功能需要精细控制的场景phy_device抽象低最高完整功能大多数标准PHY设备寄存器直接操作高低自定义实现特殊硬件或性能敏感场景在实际项目中建议优先考虑phy_device抽象层方法它提供了最完整的PHY管理功能。只有在遇到以下情况时才考虑其他方法PHY设备不符合标准MDIO协议需要极高的时序控制精度内核现有驱动无法满足性能要求调试和分析底层MDIO通信问题6. 高级技巧与调试方法6.1 Clause 45设备访问对于支持Clause 45的PHY设备需要使用MMDMDIO Manageable Device访问方式static int phy_read_c45(struct phy_device *phydev, int devad, int reg) { return mdiobus_c45_read(phydev-mdio.bus, phydev-mdio.addr, devad, reg); }6.2 MDIO总线调试内核提供了MDIO总线调试文件系统接口# 查看系统中所有MDIO总线 ls /sys/bus/mdio_bus/devices/ # 查看特定PHY寄存器 cat /sys/bus/mdio_bus/devices/fixed-0/phy_registers6.3 常见问题排查读取返回0xFFFF检查PHY地址是否正确确认MDIO总线时钟配置验证硬件连接操作超时检查MDIO控制器驱动是否正常确认PHY设备上电排查总线冲突Clause 45设备无法识别确保使用正确的访问方法检查PHY的C22兼容模式寄存器7. 实战案例自定义PHY驱动开发以下是一个简单PHY驱动的框架示例展示如何将PHY操作封装为内核模块#include linux/phy.h #include linux/module.h static int myphy_config_init(struct phy_device *phydev) { int err; // 硬件初始化 err phy_write(phydev, MII_BMCR, BMCR_RESET); if (err 0) return err; // 配置自动协商 err phy_write(phydev, MII_ADVERTISE, ADVERTISE_ALL | ADVERTISE_PAUSE_CAP); if (err 0) return err; return 0; } static struct phy_driver myphy_driver { .phy_id 0x01410c00, .name My PHY, .phy_id_mask 0xfffffff0, .features PHY_BASIC_FEATURES, .config_init myphy_config_init, .suspend genphy_suspend, .resume genphy_resume, }; module_phy_driver(myphy_driver); MODULE_DESCRIPTION(Custom PHY driver); MODULE_AUTHOR(Your Name); MODULE_LICENSE(GPL);这个驱动框架可以扩展实现更复杂的PHY功能如自定义状态检测特殊功耗管理硬件特定优化诊断接口