3. MDIO 协议基础:时钟与数据格式

MDIO 协议说白了就是一根时钟线加一根数据线,在 MAC 侧和 PHY 侧之间来回倒腾寄存器数据。很多人刚接触时觉得它很简单,不就两根线嘛,但实际上踩过的坑一点不少。我最早用 FPGA 做 MDIO 控制器时,就因为没搞懂前导码的时序,读回来的值全是 0xFF——嗯,那次排查了整整半天。

3.1 时钟线 MDC 与数据线 MDIO

先看硬件层面。MDIO 接口只有两条信号:

  • MDC — 管理时钟,由 MAC 侧驱动。最高频率 IEEE 802.3 规定不超过 2.5 MHz。不过我个人习惯留点余量,一般跑 2.0 MHz,省得信号质量出问题。
  • MDIO — 双向数据线,需要上拉电阻。MAC 发数据时做输出,PHY 回数据时切为输入。

有一条不成文的规矩:MDC 的上升沿采样数据。换句话说,MAC 在 MDC 上升沿之前把数据准备好,PHY 也在 MDC 上升沿之前把回读数据送上总线。我在项目中遇到过一块 PHY 芯片,数据在 MDC 下降沿才稳定,差点以为是协议理解错了——后来查手册发现它要求数据在 MDC 下降沿输出,算是少数异类。

信号 方向 典型频率 注意事项
MDC MAC → PHY ≤ 2.5 MHz 上升沿采样,驱动能力要够
MDIO 双向 — 需外接上拉(常见 1.5kΩ)

有些 FPGA 开发板的 GPIO 上拉驱动较弱,MDIO 信号容易振铃。我建议在 PCB 布局时让 MDIO 走线尽量短,并在 PHY 侧串一个 22Ω 电阻。

3.2 帧格式:一次完整的读写事务

MDIO 的一次事务固定 64 个 MDC 时钟周期(读)或 64 个周期(写)。帧结构如下:

 位索引      比特域         说明
 0         PRE (32)      前导码,全 1
 32        ST (2)        起始码,固定 01
 34        OP (2)        操作码,10=读,01=写
 36        PHYAD (5)     PHY 地址,5 位
 41        REGAD (5)     寄存器地址,5 位
 46        TA (2)        转向位,读时 PHY 拉低高阻再输出,写时全 0
 48        DATA (16)     数据段,高位在前
 64        空闲           总线空闲(高阻)

这里提两个容易搞混的地方:

  • 前导码 32 个 1:它不是可选项!有些 PHY 能容忍前导码略短,但我建议严格按照 32 个 1 来发。我之前用过一颗国产 PHY,前导码少于 30 个 bit 它就根本不回应。
  • TA 转向位:读事务时,第 46 位由 MAC 输出高阻,PHY 在第 47 位开始驱动 0,第 48 位起输出 16 位数据。写事务时 MAC 在第 46-47 位输出 00,然后继续送数据。

记住这个口诀:「32 个 1,01 开头,读 10 写 01,5 位地址,16 位数据,TA 要小心。」

3.3 读事务与写事务的差异

阶段 读操作 写操作
PRE (32) MAC 驱动全 1 MAC 驱动全 1
ST (2) 01 01
OP (2) 10 01
PHYAD (5) MAC 驱动 MAC 驱动
REGAD (5) MAC 驱动 MAC 驱动
TA (2) MAC: Z, PHY: 0 MAC: 00
DATA (16) PHY 驱动 MAC 驱动

实际操作中,很多人分不清转向位的 Z 态如何处理。我的做法是:在 FPGA 里用一个三态门,读事务的第 46 位时将 MDIO 设为输入,第 47 位再采样 PHY 的 0 确认总线已被接管。如果采样到的不是 0,说明 PHY 没响应——这时候就应该打印错误日志了。

我曾经遇到过 PHY 地址写错导致转向位永远采样不到 0 的案例。排查了老半天,最后发现是地址线序搞反了——PHYAD[4:0] 在数据手册里是高位在前,而我习惯低位在前。记住:MDIO 帧里 MSB 先发!

3.4 时序约束要点

如果你是用 CPU GPIO 模拟 MDIO,那时序要求主要靠软件延时来保证。但用 FPGA 或专用 MAC 时,需要关注:

  • MDC 占空比:40%~60% 都 OK,别太离谱就行。
  • 数据建立/保持时间:普通 PHY 要求 MDIO 在 MDC 上升沿前至少 10 ns 稳定,上升沿后保持 10 ns。
  • 最小空闲时间:两次帧之间至少留 1 个 MDC 周期的空闲,让总线回到高阻。

我一般会在两次 MDIO 事务之间加 1 µs 的延迟,保证 PHY 有足够时间消化上一笔操作。虽然 IEEE 标准没强制,但实践下来这样最稳。

3.5 多 PHY 共享 MDIO 总线

一根 MDIO 总线可以挂多个 PHY,靠 PHYAD 区分。每个 PHY 的地址由外部引脚(或 strapping 电阻)设定,通常为 0x00~0x1F。如果多个 PHY 地址冲突,那总线就乱套了——两个 PHY 同时驱动数据,造成数据碰撞。我在项目中遇到过一次,就是因为板子上两个 PHY 的地址缺省都是 0x10,谁都不理睬。解决方案是:焊接时把其中一个 PHY 的 ADDR 引脚拉高或拉低,错开地址。

3.6 一张图看懂 MDIO 读写帧

下面这张 SVG 图把读写帧的结构画清楚了,大家可以直接照着写代码。

MDIO 读/写帧结构 前导码 PRE (32-bit),全 1 ST 01 OP (10/01) PHYAD 5bit REGAD 5bit TA (Z/0) 读: Z→0 DATA 16bit 说明 读操作:OP=10,TA 第 1 位 MAC 高阻,PHY 接管输出 0,然后输出 16 位数据 写操作:OP=01,TA 两位置 0,MAC 连续输出 16 位数据 帧结束:总线空闲,MDIO 回到高阻

3.7 常见坑:前导码和转向位

前导码 32 个 1,看似简单,但有个地方很多人中招——前导码结束后紧接着的 ST 码必须是 01,不能是 10。如果你用 FPGA 的移位寄存器直接拼接,容易把前导码最后一个 1 和 ST 的第一个 0 搞混。我习惯在状态机里单独计数前导码,发完 32 个 1 后再进入 ST 状态,这样逻辑清晰得多。

转向位的 Z 态处理也容易出错。读操作时,MAC 在第 46 位必须释放总线(三态输出高阻),然后 PHY 在第 47 位驱动一个 0 作为响应。如果 MAC 释放晚了,两个器件同时驱动,总线电流会很大,严重时可能烧坏 IO 口。所以我的代码里会在第 45 位结束时就把 MDIO 方向切为输入,预留一个时钟周期的余量。