1. MDIO子系统概述
MDIO(Management Data Input/Output)是IEEE 802.3标准定义的两线制串行接口协议,主要用于以太网物理层设备(PHY)与介质访问控制层(MAC)之间的管理通信。这个看似简单的接口协议,实际上承载着现代网络设备中PHY寄存器配置、状态监控等关键功能。
我第一次接触MDIO是在调试一块定制网卡时,PHY芯片始终无法正确初始化。通过示波器抓取MDIO总线信号,发现时钟频率设置不当导致时序违规。这个经历让我深刻认识到,理解MDIO的底层机制对网络设备开发至关重要。
MDIO协议栈在Linux内核中经历了多次架构重构。早期的实现直接操作硬件寄存器,现在则形成了完整的子系统架构,包含:
- MDIO总线核心(drivers/net/mdio)
- 物理层抽象(PHY驱动)
- 设备树绑定支持
- 调试基础设施
2. MDIO协议层详解
2.1 电气特性与帧结构
标准MDIO采用两线制:
- MDC:时钟线,由MAC驱动,频率上限2.5MHz(IEEE 802.3 Clause 22)
- MDIO:双向数据线,采用三态门实现方向控制
Clause 22帧格式(经典模式):
code复制| PRE | ST | OP | PHYAD | REGAD | TA | DATA |
- 前导码(PRE):32个连续"1"
- 起始位(ST):"01"
- 操作码(OP):"10"读,"01"写
- PHY地址(PHYAD):5位,支持32个PHY
- 寄存器地址(REGAD):5位,32个寄存器
- 应答位(TA):"Z0"读,"Z1"写
- 数据(DATA):16位有效载荷
Clause 45扩展帧增加了设备类型和地址分段机制,支持更多寄存器和操作类型。
2.2 时钟同步与时序约束
正确的时序配置是MDIO可靠通信的关键。以Marvell 88E1111 PHY为例,其要求:
- 时钟高电平最小持续时间:160ns
- 时钟低电平最小持续时间:160ns
- 数据建立时间(setup):10ns
- 数据保持时间(hold):10ns
在嵌入式系统中,当MDIO控制器与PHY时钟不同源时,需要特别注意时钟偏移补偿。我曾遇到过由于FPGA内部时钟抖动导致MDIO通信失败的案例,最终通过调整MDC时钟相位解决。
3. Linux MDIO子系统架构
3.1 核心数据结构
c复制struct mii_bus {
const char *name;
int (*read)(struct mii_bus *, int, int);
int (*write)(struct mii_bus *, int, int, u16);
/* ... */
};
struct phy_device {
struct mdio_device mdio;
u32 phy_id;
int (*read_status)(struct phy_device *);
/* ... */
};
MDIO总线驱动需要实现mii_bus结构体,重点包括:
- 总线名称(如"platform-mdio")
- 读写回调函数
- 中断处理(可选)
3.2 典型驱动实现流程
以Freescale FEC MDIO控制器为例:
- 初始化MDIO控制器寄存器:
c复制writel(MDIO_CTRL_CLKDIV(0x0F) | MDIO_CTRL_ENABLE,
fec->hwp + FEC_MII_CTL);
- 实现读写操作:
c复制static int fec_mii_read(struct mii_bus *bus, int phy_id, int reg)
{
/* 1. 设置PHY地址和寄存器 */
writel(phy_id << 23 | reg << 18, fec->hwp + FEC_MII_DATA);
/* 2. 触发读操作 */
writel(MDIO_CTRL_READ, fec->hwp + FEC_MII_CTL);
/* 3. 等待完成 */
while (!(readl(fec->hwp + FEC_MII_CTL) & MDIO_CTRL_READ));
/* 4. 返回数据 */
return readl(fec->hwp + FEC_MII_DATA) & 0xFFFF;
}
- 注册MDIO总线:
c复制bus = mdiobus_alloc();
bus->name = "fec-mdio";
bus->read = fec_mii_read;
bus->write = fec_mii_write;
mdiobus_register(bus);
4. PHY设备驱动开发
4.1 PHY探测与绑定
内核通过以下步骤发现PHY设备:
- MDIO总线扫描PHY地址(0-31)
- 读取PHY ID(寄存器2/3)
- 匹配phy_driver结构:
c复制static struct phy_driver marvell_drvs[] = {
{
.phy_id = MARVELL_PHY_ID_88E1111,
.name = "Marvell 88E1111",
.config_init = marvell_config_init,
.read_status = marvell_read_status,
}
};
4.2 典型PHY配置流程
以千兆以太网PHY初始化为例:
c复制static int marvell_config_init(struct phy_device *phydev)
{
/* 1. 软件复位PHY */
phy_write(phydev, MII_BMCR, BMCR_RESET);
/* 2. 等待复位完成 */
while (phy_read(phydev, MII_BMCR) & BMCR_RESET);
/* 3. 配置自动协商 */
phy_write(phydev, MII_ADVERTISE,
ADVERTISE_1000FULL | ADVERTISE_100FULL |
ADVERTISE_10FULL | ADVERTISE_CSMA);
/* 4. 启动自动协商 */
phy_write(phydev, MII_BMCR, BMCR_ANENABLE | BMCR_ANRESTART);
}
5. 调试与性能优化
5.1 常见问题排查
-
PHY无法探测:
- 检查MDIO总线时钟频率(示波器测量MDC)
- 验证PHY地址(部分PHY通过硬件引脚配置)
- 确认电源和复位信号
-
寄存器读写异常:
bash复制# 通过sysfs调试 echo 0x1e > /sys/class/mdio_bus/.../phy_registers/address cat /sys/class/mdio_bus/.../phy_registers/value -
性能优化技巧:
- 批量读写使用
phy_read_mmd/phy_write_mmd - 启用MDIO总线时钟门控降低功耗
- 对于频繁访问的寄存器,考虑缓存机制
- 批量读写使用
5.2 实时监控PHY状态
c复制static void monitor_phy(struct phy_device *phydev)
{
int status = phy_read(phydev, MII_BMSR);
if (status & BMSR_LSTATUS)
printk("Link up\n");
else
printk("Link down\n");
if (status & BMSR_ANEGCOMPLETE)
printk("Auto-negotiation complete\n");
}
6. 设备树配置实例
典型MDIO控制器配置:
dts复制mdio0: mdio@12340000 {
compatible = "fsl,fec-mdio";
reg = <0x12340000 0x1000>;
clocks = <&clks 123>;
#address-cells = <1>;
#size-cells = <0>;
phy0: ethernet-phy@0 {
reg = <0>;
max-speed = <1000>;
reset-gpios = <&gpio0 12 GPIO_ACTIVE_LOW>;
};
};
关键点:
#address-cells和#size-cells必须正确设置- PHY节点必须有
reg属性指定地址 - 复杂PHY需要配置额外属性(如RGMII延迟)
7. 高级话题:MDIO多路复用
在交换机等设备中,常需要扩展MDIO总线:
-
硬件方案:
- 使用MDIO多路复用器(如PCA9548)
- 通过GPIO控制片选信号
-
软件实现:
c复制struct mux_mdio {
struct mii_bus *parent;
int mux_id;
};
static int mux_mdio_read(struct mii_bus *bus, int phy_id, int reg)
{
struct mux_mdio *priv = bus->priv;
/* 1. 选择多路复用通道 */
set_mux_channel(priv->mux_id);
/* 2. 通过父总线访问 */
return priv->parent->read(priv->parent, phy_id, reg);
}
8. 实战案例:定制PHY驱动
假设我们需要为新型PHY芯片编写驱动:
- 识别PHY ID:
c复制#define MYPHY_ID 0x0141bc00
#define MYPHY_MASK 0xffffff00
static struct phy_driver myphy_driver = {
.phy_id = MYPHY_ID,
.name = "My PHY",
.phy_id_mask = MYPHY_MASK,
.features = PHY_GBIT_FEATURES,
.config_init = myphy_config_init,
};
- 实现特殊功能:
c复制static int myphy_read_temp(struct phy_device *phydev)
{
/* 1. 选择扩展寄存器页 */
phy_write(phydev, MII_MMD_CTRL, MII_MMD_CTRL_DEVAD_MASK);
/* 2. 读取温度传感器值 */
return phy_read(phydev, MII_MMD_DATA);
}
- 注册驱动:
c复制module_phy_driver(myphy_driver);
在开发过程中,我总结出几个关键点:
- 仔细研究PHY数据手册的时序图
- 使用逻辑分析仪验证MDIO波形
- 注意PHY的电源序列要求
- 考虑不同工作模式(如节能模式)下的行为差异
