MDIO总线驱动开发实战:基于Linux内核4.19的PHY寄存器读写与调试
在嵌入式Linux开发中,网络设备的稳定性和性能往往取决于底层驱动的质量。MDIO总线作为MAC与PHY芯片之间的管理通道,其驱动实现直接影响着网络接口的配置、状态监控和故障排查效率。本文将深入探讨如何在Linux内核4.19环境下构建完整的MDIO驱动框架,并提供可直接应用于项目的代码范例和调试技巧。
1. MDIO总线驱动框架构建
MDIO总线在内核中被抽象为mdio_bus结构体,其核心职责是提供PHY设备的注册机制和读写操作接口。现代Linux内核已经实现了标准的MDIO总线框架,开发者需要重点关注的是特定硬件平台的适配层实现。
典型的MDIO驱动包含以下关键组件:
- 总线控制器驱动:实现硬件特定的MDIO时序控制
- PHY设备驱动:处理PHY芯片的寄存器访问和状态管理
- 设备树绑定:描述硬件连接关系和特性参数
以下是一个基础MDIO总线控制器的注册示例:
#include <linux/of_mdio.h> #include <linux/phy.h> static int my_mdio_read(struct mii_bus *bus, int phy_id, int regnum) { struct my_private_data *priv = bus->priv; unsigned int val; /* 硬件特定的MDIO读取操作 */ val = readl(priv->base + MDIO_DATA_REG); return val & 0xFFFF; } static int my_mdio_write(struct mii_bus *bus, int phy_id, int regnum, u16 value) { struct my_private_data *priv = bus->priv; /* 硬件特定的MDIO写入操作 */ writel(value, priv->base + MDIO_DATA_REG); return 0; } static int my_mdio_probe(struct platform_device *pdev) { struct mii_bus *mdio_bus; struct my_private_data *priv; mdio_bus = devm_mdiobus_alloc(&pdev->dev); if (!mdio_bus) return -ENOMEM; priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL); priv->base = devm_platform_ioremap_resource(pdev, 0); mdio_bus->name = "my_mdio_bus"; mdio_bus->read = my_mdio_read; mdio_bus->write = my_mdio_write; mdio_bus->parent = &pdev->dev; mdio_bus->priv = priv; /* 从设备树获取PHY连接信息 */ if (of_mdiobus_register(mdio_bus, pdev->dev.of_node)) return -ENXIO; platform_set_drvdata(pdev, mdio_bus); return 0; }提示:现代内核推荐使用设备树描述MDIO总线拓扑。典型的设备树节点如下:
mdio { compatible = "my,mdio-controller"; reg = <0x1e200000 0x1000>; #address-cells = <1>; #size-cells = <0>; phy0: ethernet-phy@0 { reg = <0>; max-speed = <1000>; }; };
2. PHY寄存器访问模式实现
根据IEEE 802.3标准,MDIO协议支持两种访问模式:Clause 22和Clause 45。现代PHY芯片通常同时支持这两种模式,但寄存器组织和访问方式存在显著差异。
2.1 Clause 22模式实现
Clause 22是传统的5位地址空间模式,适用于早期PHY芯片。其寄存器访问函数实现如下:
int phy_read_c22(struct mii_bus *bus, int phy_id, int regnum) { /* 检查PHY ID有效性 */ if (phy_id < 0 || phy_id >= 32) return -EINVAL; /* 添加前导码和起始位 */ u32 frame = (0xFFFFFFFF | (0x01 << 30) | (phy_id << 23) | (regnum << 18) | (0x02 << 16)); /* 发送帧并读取响应 */ return mdio_transfer(bus, frame); }Clause 22常用寄存器及其功能:
| 寄存器 | 名称 | 功能描述 |
|---|---|---|
| 0x00 | BMCR | 基本模式控制,包含重启、环回等控制位 |
| 0x01 | BMSR | 基本模式状态,反映链路状态和能力 |
| 0x02 | PHYID1 | PHY标识符第一部分 |
| 0x03 | PHYID2 | PHY标识符第二部分 |
| 0x04 | ANAR | 自协商通告寄存器 |
| 0x05 | ANLPAR | 自协商链路伙伴能力寄存器 |
2.2 Clause 45模式实现
Clause 45扩展了地址空间到32位设备类型+16位寄存器,支持更复杂的PHY设备。其访问需要分两步操作:
int phy_read_c45(struct mii_bus *bus, int phy_id, int devad, int regnum) { /* 地址阶段 */ u32 addr_frame = (0xFFFFFFFF | (0x00 << 30) | (phy_id << 23) | (devad << 18) | (regnum & 0xFFFF)); mdio_transfer(bus, addr_frame); /* 数据阶段 */ u32 data_frame = (0xFFFFFFFF | (0x11 << 30) | (phy_id << 23) | (devad << 18)); return mdio_transfer(bus, data_frame); }Clause 45设备类型分类:
- 1.x:10G及以下速率PHY
- 3.x:10G及以上速率PHY
- 4.x:背板以太网PHY
- 7.x:光模块诊断监控
2.3 兼容模式实现
许多现代PHY通过Clause 22寄存器13和14提供Clause 45兼容访问:
int phy_read_c45_via_c22(struct mii_bus *bus, int phy_id, int devad, int regnum) { /* 写入C45设备地址和寄存器号 */ bus->write(bus, phy_id, 13, (devad << 16) | regnum); /* 通过寄存器14读取数据 */ return bus->read(bus, phy_id, 14); }3. 调试方法与实战技巧
3.1 使用ethtool进行PHY诊断
ethtool是Linux下最常用的网络调试工具,可以读取PHY寄存器并显示链路状态:
# 查看PHY状态 ethtool eth0 # 读取特定寄存器 ethtool --phy-statistics eth0 # 寄存器dump(Clause 22) ethtool --register-dump eth0 # Clause 45设备需要指定页和寄存器 ethtool --set-phy-tunable eth0 mdix auto3.2 mii-tool的经典用法
虽然功能不如ethtool全面,但mii-tool在简单诊断中仍然有用:
# 基本链路检测 mii-tool -v eth0 # 强制设置模式 mii-tool -F 100baseTx-FD eth03.3 内核调试接口
Linux内核通过sysfs和debugfs提供了丰富的调试接口:
/sys/class/net/eth0/phydev/ |- address |- phy_interface |- registers |- statistics /debug/mdio_bus/ |- mdio-bus-addr |- phy_registers |- phy_state3.4 常见问题排查指南
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| MDIO读取超时 | 总线时钟配置错误 | 检查MDC频率(通常2.5-25MHz) |
| 寄存器值异常 | 总线竞争或PHY未就绪 | 增加读写间隔,检查PHY复位状态 |
| 链路不稳定 | 自协商配置冲突 | 比较ANAR和ANLPAR寄存器值 |
| 无法识别PHY | 设备地址错误 | 扫描所有可能PHY地址(0-31) |
4. 高级功能实现
4.1 中断驱动式PHY状态监控
传统轮询方式会增加系统负载,现代PHY支持状态变化中断:
static irqreturn_t phy_interrupt(int irq, void *dev_id) { struct phy_device *phydev = dev_id; int status = phy_read(phydev, MII_BMSR); if (status & BMSR_LSTATUS) phy_trigger_machine(phydev); return IRQ_HANDLED; } int phy_driver_init(void) { struct phy_driver *drv; drv->config_intr = phy_config_intr; drv->ack_interrupt = phy_ack_interrupt; drv->did_interrupt = phy_did_interrupt; }4.2 硬件时间戳支持
IEEE 1588(PTP)需要精确的硬件时间戳,PHY寄存器配置示例:
void configure_ptp(struct phy_device *phydev) { /* 启用PTP功能 */ phy_write_mmd(phydev, 7, 0x8010, 0x0001); /* 配置时钟模式 */ phy_write_mmd(phydev, 7, 0x8012, 0x0700); /* 设置时间戳寄存器 */ phy_write_mmd(phydev, 7, 0x8020, 0xFFFF); }4.3 节能以太网(EEE)配置
通过MDIO配置EEE功能可以显著降低功耗:
int enable_eee(struct phy_device *phydev) { int adv = phy_read_mmd(phydev, MDIO_MMD_AN, MDIO_AN_EEE_ADV); int lpa = phy_read_mmd(phydev, MDIO_MMD_AN, MDIO_AN_EEE_LPA); /* 协商EEE能力 */ phy_write_mmd(phydev, MDIO_MMD_AN, MDIO_AN_EEE_ADV, adv | MDIO_EEE_1000T | MDIO_EEE_100TX); /* 激活EEE */ phy_write_mmd(phydev, MDIO_MMD_AN, MDIO_AN_EEE_CTRL, MDIO_EEE_CTRL_ENABLE); }在实际项目中,MDIO驱动的稳定性往往取决于对硬件特性的精确把握。建议在开发阶段建立完善的寄存器操作日志系统,记录每次MDIO访问的时序和结果,这对后期调试有极大帮助。