2. MDIO 管理接口:时钟、帧格式与读写时序

MDIO 管理接口,很多人也叫它 SMI 或者 MIIM,说白了就是 MAC 侧用来读写 PHY 内部寄存器的通道。你想想看,PHY 的链路状态、协商结果、中断标志,全都得靠这条串行总线才能拿到或修改。我个人觉得,搞明白 MDIO 的时序和帧格式,是写底层驱动、排 PHY 疑难杂症的基本功。

2.1 协议基础与时钟规范

MDIO 采用两条线:MDC(时钟)和 MDIO(数据)。总线是一个共享结构,允许多个 PHY 挂在同一条总线上,通过 PHY 地址来区分器件。嗯,这里要注意:MDC 必须由 MAC 持续产生,PHY 只有时钟同步功能。

参数 规范
最大 MDC 频率 2.5 MHz(标准模式),某些 PHY 支持 10 MHz(快速模式)
MDC 高/低电平持续时间 ≥ 200 ns(2.5 MHz 时)
MDIO 数据变化沿 在 MDC 下降沿改变,在上升沿被采样
总线空闲状态 MDIO 为高阻,MDC 保持低
我踩过的坑: 刚开始做项目时,我把 MDC 频率设到了 10 MHz,结果调试了好几天发现某些 PHY 读出来的寄存器值总是不对。后来查手册才发现那款 PHY 不支持快速模式。记住:低频率更可靠,2.5 MHz 兼容性最好。

2.2 帧格式深度拆解

MDIO 传输的基本单位是一帧 64 个 MDC 周期(读操作)或 64 个周期(写操作)。帧结构是固定的,我习惯把它拆成五个阶段来理解:

  1. 前导码(Preamble):32 个连续 1,用来同步 PHY 的接收逻辑。
  2. 起始码(Start of Frame):固定 2 位 01,帧开始的标志。
  3. 操作码(Op Code):读操作为 10,写操作为 01。
  4. 地址段:5 位 PHY 地址 + 5 位寄存器地址。
  5. 数据段:16 位数据 + 2 位翻转周期(Turnaround)。读操作时需要 PHY 在 TA 阶段释放总线、切换方向。

具体每个阶段的位域分配,我整理成了一张表,方便你查:

阶段 位数 读帧内容 写帧内容
前导码 32 111...111(32个1) 111...111(32个1)
起始码 2 01 01
操作码 2 10 01
PHY 地址 5 PHY_ADDR[4:0] PHY_ADDR[4:0]
寄存器地址 5 REG_ADDR[4:0] REG_ADDR[4:0]
翻转 (TA) 2 由 PHY 驱动:先 Z,再 0 由 MAC 驱动:先 1,再 0
数据 16 PHY 输出 DATA[15:0] MAC 输出 DATA[15:0]
注意: 标准规范要求前导码至少 32 位。有些 PHY 允许跳过前导码(省略模式),但我建议你不要省略,兼容性会差很多。我遇到过一台交换机,省略前导码后其中一个 PHY 始终不响应,折腾了半天。

2.3 读写操作时序

读写时序的核心差异在于 TA 阶段的方向控制。下面我用一段 C 代码演示最普通的软件模拟 MDIO 读写(以 STM32 的 GPIO 为例),你可以在没有硬件 MDIO 的 MCU 上直接跑。

// 模拟 MDC 脉冲
void MDC_pulse(void) {
    MDC = 1;
    delay_ns(200);   // 等待上升沿采样
    MDC = 0;
    delay_ns(200);
}

// 读寄存器(32 位前导码版本)
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_out(1);
        MDC_pulse();
    }
    // 2. 起始码 01
    MDIO_out(0); MDC_pulse();
    MDIO_out(1); MDC_pulse();
    // 3. 操作码:读 10
    MDIO_out(1); MDC_pulse();  // SMI:读操作码的 MSB 先发
    MDIO_out(0); MDC_pulse();
    // 4. PHY 地址 (5 bit)
    for (i = 4; i >= 0; i--) {
        MDIO_out((phy_addr >> i) & 0x01);
        MDC_pulse();
    }
    // 5. 寄存器地址 (5 bit)
    for (i = 4; i >= 0; i--) {
        MDIO_out((reg_addr >> i) & 0x01);
        MDC_pulse();
    }
    // 6. TA:MAC 释放总线,第1个周期高阻,第2个周期 PHY 将数据线拉为0
    MDIO_set_input();  // 释放 MDIO 引脚
    MDC_pulse();       // 采样到高阻(忽略)
    MDC_pulse();       // 采样到 0(PHY 准备完成)
    // 7. 读取 16 位数据(MSB 在先)
    for (i = 15; i >= 0; i--) {
        MDC_pulse();   // 上升沿采样
        data |= (MDIO_read_pin() << i);
    }
    // 8. 最后再给一个 MDC 时钟让 PHY 恢复空闲
    MDC_pulse();
    return data;
}
我曾经就在这里栽过: 读取数据时,很多工程师容易忘记 MDIO 的方向切换是在 TA 阶段完成的。如果 TA 的第1个周期你还没释放引脚,PHY 会始终处于高阻,你只能读到 0xFFFF 或者乱码。所以一定要确保 MDIO_set_input() 的执行时机精确无误。

写操作的时序类似,但 TA 阶段由 MAC 主动驱动 10,随后连续输出 16 位数据。标准帧结束后还有一个额外时钟周期用于恢复总线。如果你用硬件 MDIO 控制器(比如 RTL8211 系列),这些时序逻辑会自动完成,不过我还是建议你先用 GPIO 模拟一版,方便调试。

2.4 本章核心流程总图

MDIO 读操作帧结构(64 个 MDC 周期) 前导码 (32 bit 1) 01 10 PHY_ADDR REG_ADDR TA (Z/0) DATA[15:0] MAC 驱动 PHY 驱动 时钟与采样规则 • MDIO 在 MDC 下降沿变化,上升沿采样 • 帧必须连续发送,不可插入空闲周期 • 读取操作前 MDIO 引脚必须设为输入(高阻) • 写操作时 MDIO 全程由 MAC 驱动,TA 阶段输出 1 0

2.5 调试经验总结

  • 时钟极性: 规范要求数据在上升沿采样,但有些 FPGA 的 MDIO 控制器实现用了下降沿。挂示波器确认一下,别上来就怀疑 PHY 坏了。
  • 高阻处理: TA 阶段如果 MDIO 线没有被外部上拉或下拉,悬空状态可能导致读数据全是 0xFFFF。我建议在设计板上给 MDIO 加一个 10kΩ 上拉电阻。
  • 地址冲突: 如果总线上挂了两颗相同地址的 PHY,读操作会冲突。商用交换机一般通过硬件 strapping 引脚保证地址唯一。
  • 默认值: 多数 PHY 上电后寄存器 0(BMCR)默认 0x3100(复位=1,loopback=0)。我第一次调试时读出来不对,还以为通信有问题,后来才发现是 PHY 还在复位状态。
我自己的习惯: 开始调试一个新板子时,先写一个简单的读 ID 寄存器(寄存器 2 和 3)的程序。如果能读出 PHY ID(如 RTL8211 的 ID 是 0x001CC916),说明 MDIO 时序和地址都对了。如果读到 0x0000 或 0xFFFF,大概率是时序有问题,优先检查 MDC 频率和 TA 切换时机。