MDIO 总线简介:MDIO/MDC 接口规范

为什么要有 MDIO?

MDIO(Management Data Input/Output)说白了,就是用来管理 PHY 芯片的串行总线。PHY 内部有几十个寄存器,像控制寄存器、状态寄存器、协商能力寄存器等等。没有这根线,CPU 怎么知道当前网线插没插?速率是 100M 还是 1000M?自动协商成功没?

我记得最早做交换机项目时,第一次看到 PHY 的数据手册,满篇的寄存器地址看得我头皮发麻。后来才明白,所有底层操作全靠 MDIO 这两根线(MDC + MDIO)来读写。你掌握了它,就等于拿到了 PHY 的遥控器。

信号定义

MDIO 总线只有两根信号线,外加一个可选的中断输出。我列个表,一看就清楚:

信号名方向描述
MDC主设备 → 从设备管理时钟,由 MAC/控制器提供,最高频率通常 2.5 MHz
MDIO双向管理数据,主设备驱动输出或 PHY 驱动输出(三态)
INT#(可选)PHY → 主设备中断请求,低电平有效,用于链路状态变化通知

很多初学者以为 MDIO 是 I²C,其实它更像 SPI 的单线半双工模式。区别在于:MDIO 只有一条数据线,方向靠帧中的“转向位”控制。

MDIO 帧结构

标准帧格式是 IEEE 802.3 第 22 条款定义的。嗯,你把它拆开看,实际上就是一段固定序列。记住这个结构,读写 PHY 寄存器就只剩细节问题了。

一句话:所有 MDIO 操作都从 32 个连续的 1(前导码)开始,后面紧跟着 起始码 01,然后才是真正的地址和数据。

下面这张图就是 MDIO 帧的完整组成,我把它按顺序画出来了:

前导码 32位 全1 起始 2位 01 操作码 2位 PHY地址 5位 寄存器地址 5位 转向 2位 数据 16位 写帧:PR(32) + ST(01) + OP(01) + PHY_ADDR(5) + REG_ADDR(5) + TA(10) + DATA(16) 读帧:PR(32) + ST(01) + OP(10) + PHY_ADDR(5) + REG_ADDR(5) + TA(Z0→PHY驱动) + DATA(16) ※ 转向位:写帧由主设备驱动 10;读帧先释放总线,PHY 驱动 Z0 后输出数据

操作码与转向

操作码(OP)只有两种:01 表示写,10 表示读。你想想看,为什么没有 00 和 11?因为这两个码被保留了,实际中你永远用不到。

转向位(TA)是很多新手掉坑的地方。写操作时,主设备必须持续驱动数据线,转向位填 10;读操作时,主设备在转向位前一个时钟释放 MDIO(输出高阻),由 PHY 接管,先拉低一位(Z0 状态)表示准备就绪,然后再输出 16 位数据。我曾经因为读操作忘记加转向位的等待时钟,结果数据全错位,查了好几天才找到问题。

注意:读帧的转向位中,PHY 输出 Z0 的意思是:在第 1 个 TA 时钟周期内,PHY 保持 MDIO 为高阻(由外部上拉拉高),第 2 个时钟周期拉低。你代码里必须预留这两个时钟的切换时间,不能直接读数据。

读写操作示例代码

下面这段 C 代码是我从之前一个项目中扒下来的核心逻辑,用 GPIO 模拟 MDIO 接口。实际芯片内部都集成有 MDIO 控制器,但理解模拟方式能帮你彻底搞懂时序。

// 伪代码:通过 GPIO 模拟 MDIO 读操作
uint16_t mdio_read(uint8_t phy_addr, uint8_t reg_addr)
{
    uint16_t data = 0;
    int i;

    // 1. 前导码:32 个连续 1
    for (i = 0; i < 32; i++) {
        mdio_set(MDC, 0);
        mdio_set(MDIO, 1);
        mdio_set(MDC, 1);
    }

    // 2. 起始码 01
    mdio_write_bit(0); // ST 第一位
    mdio_write_bit(1); // ST 第二位

    // 3. 操作码 10(读)
    mdio_write_bit(1);
    mdio_write_bit(0);

    // 4. PHY 地址(5 位,高位先发)
    for (i = 4; i >= 0; i--)
        mdio_write_bit((phy_addr >> i) & 1);

    // 5. 寄存器地址(5 位)
    for (i = 4; i >= 0; i--)
        mdio_write_bit((reg_addr >> i) & 1);

    // 6. 转向位:主设备释放总线,PHY 接管
    mdio_set_input();       // MDIO 切换为输入
    mdio_set(MDC, 0);
    mdio_set(MDC, 1);       // TA 第一位(Z)
    mdio_set(MDC, 0);
    mdio_set(MDC, 1);       // TA 第二位(0)

    // 7. 读取 16 位数据(上升沿采样)
    for (i = 15; i >= 0; i--) {
        mdio_set(MDC, 0);
        if (mdio_read_pin() == 1)
            data |= (1 << i);
        mdio_set(MDC, 1);
    }

    return data;
}

实际工程中,如果 MDIO 挂载多个 PHY,每个 PHY 的地址由芯片引脚硬件决定(通常是 AD0~AD4 拉高/拉低)。读写前一定要先确认这个地址,否则操作无效。

时序要求与常见坑

  • MDC 最大频率:标准规定 2.5 MHz(周期 400 ns)。现在很多 PHY 支持到 12.5 MHz,但兼容性起见,我建议你开始时用 1 MHz 验证。
  • MDIO 上拉电阻:MDIO 是双向开漏?不对,它是推挽输出(主设备或 PHY 驱动),但总线空闲时需要一个外部上拉(4.7 kΩ 到 3.3 V)确保数据线保持高电平。
  • 多 PHY 冲突:如果 PHY 地址冲突,两个 PHY 都会在 MDIO 上抢着响应,这时数据就会变成未知。我踩过这个坑——焊接时 PHY 地址引脚虚接,导致地址一直是 0,多个 PHY 同时响应,读回来的寄存器值完全不对。

终极建议:在调试 MDIO 时,先用逻辑分析仪抓一下波形。你看看前导码是不是 32 个 1?起始码对不对?地址是不是你期望的值?有了波形,所有问题一眼就能看出来。

好了,MDIO 的协议层面就这么多。你只要把帧结构刻在脑子里,读写 PHY 寄存器就是个机械活儿。下一节我们会用实战方式,在 Linux 用户空间直接操作 MDIO 总线读取实际 PHY 的 ID。