第2章:MDIO总线协议基础——时钟、数据、帧格式

做硬件调试这么多年,我接触过不少串行总线。MDIO 算是比较"温柔"的一种——只有两根线,时序也清晰。但温柔不代表不会出问题,很多通信故障的根源就藏在最基础的协议细节里。

这一章,咱们把 MDIO 的底子夯实。时钟、数据、帧格式,挨个捋一遍。

2.1 物理层信号:MDC 与 MDIO

MDIO 总线就两根线:管理数据时钟(MDC)和管理数据输入输出(MDIO)。

  • MDC:时钟线,由主设备(通常是 MAC 或 CPU)驱动。频率最高能跑到多少?标准里写 2.5 MHz,但实际很多器件能跑到 12.5 MHz。我个人习惯先按 2.5 MHz 起步调试,稳定了再往上提。
  • MDIO:双向数据线。主设备写的时候输出数据,读的时候由从设备(PHY)拉高或拉低。这就带来了一个问题:你想想看,两个设备共一根线,不做控制就会打架。所以协议规定了个严格的三态切换机制。

小经验:我在项目中遇到过 MDC 信号没加终端电阻,导致边缘抖动严重。排查了两天才发现——示波器探头一搭上去波形就正常,一拿走就乱码。嗯,后来在每个 PHY 的 MDC 脚旁边放了个 22 Ω 串阻,问题消失。

2.2 帧格式:一笔一笔拆开看

MDIO 的每一笔传输都是一个固定格式的帧。标准帧长度为 64 个时钟周期,支持 32 个 PHY 地址和 32 个寄存器地址。

时序概览(读帧):
预置空闲 → 起始 [2位] → 操作码 [2位] → PHY地址 [5位] → 寄存器地址 [5位] → TurnAround [2位] → 数据 [16位] → 空闲

下面这个表格把每个字段讲清楚:

MDIO 帧格式字段说明(Clause 22)
字段 长度 描述
起始 (Preamble) 32 个"1" 同步用。主设备先发 32 个 MDC 周期,MDIO 一直为高。这是在告诉 PHY:"注意,准备说正事了。"
SFD (起始定界符) 2 位 "01" 标记帧开始。如果收到其他组合,PHY 会忽略这一帧。
操作码 (Op Code) 2 位 01 = 写;10 = 读。00 和 11 保留。
PHY 地址 5 位 选中哪个 PHY(0 ~ 31)。通常通过 PHYAD[4:0] 引脚配置。
寄存器地址 5 位 PHY 内部寄存器的编号(0 ~ 31)。
TurnAround (TA) 2 位 控制权切换时间。写帧: 主设备发 "10";读帧: 主设备发 "Z",PHY 发 "0" 作为第一个 TA 位,再发第二位(数据量程控制,通常为 0)。
数据 (Data) 16 位 读写的数据内容。
空闲 (Idle) 至少 1 位高阻 MDIO 释放为高阻,由外部上拉电阻拉高。

坑点提醒:我曾经遇到一个 PHY 在 TA 阶段不按套路出牌——读帧时第一个 TA 位不置 "0",直接输出数据。查了勘误手册才知道那个型号有 bug。所以排查通信失败时,一定要用示波器抓 TA 段的波形,确认主从设备谁在驱动。

2.3 读帧 vs 写帧:区别在哪儿?

说白了,读写帧的前半段一模一样——都从起始、操作码、地址、寄存器地址。区别就在 TA 和后面的数据驱动者。

  • 写帧:主设备全程“控场”。TA 阶段发 "10",让 MDIO 仍然由主设备驱动,紧接着发 16 位数据。
  • 读帧:主设备在 TA 第一个周期释放 MDIO(三态),PHY 接管。第二个 TA 周期 PHY 把驱动拉低(表示准备好),随后 PHY 在接下来的 16 个周期输出数据。

你可以把 TA 想象成一根接力棒——写帧不交棒,读帧必须交棒。如果 PHY 没接过这棒子,MDIO 会一直保持高阻状态,主设备读回来的全是 "1"(或者浮空电平乱跳)。

2.4 时序参数:别放松任何一个细节

协议对时序只有两个硬要求:MDC 占空比 40%~60%,MDIO 相对 MDC 建立/保持时间 > 10 ns。但实际硬件没这么宽容。

常见 MDIO 时序参数(参考值)
参数 最小值 典型值 最大值
MDC 频率 — 2.5 MHz 12.5 MHz
MDC 高电平时间 40 ns 200 ns —
MDC 低电平时间 40 ns 200 ns —
MDIO 建立时间(相对 MDC 上升沿) 10 ns 20 ns —
MDIO 保持时间(相对 MDC 上升沿) 10 ns 20 ns —

关键:MDIO 在 MDC 的下降沿改变状态,PHY 在上升沿采样。这意味着主设备写数据时,要在 MDC 下降沿之前把 MDIO 设好;读数据时,PHY 在下降沿更新数据,主设备在下一个上升沿锁存。

2.5 帧结构总览图

下面这张 SVG 把整个帧流程串起来,读帧和写帧的 TA 差异用颜色区分开了。

MDIO 帧结构总览(读帧与写帧对比) 写帧路径 读帧路径 1. Preamble (32'b1) 32 个连续 "1" 主设备发送同步序列,MDIO 持续为高 2. SFD (01) 01 3. Op Code 01 (写) 10 (读) 4. PHY Addr + Reg Addr PHY Addr (5 bit) Reg Addr (5 bit) 共 10 位 写帧 (写操作) TA=10 Data (16 bit) 读帧 (读操作) TA=Z0 Data (16 bit) Idle:MDIO 释放为高阻 至少 1 个时钟周期 整个帧共 64 个 MDC 周期 注:Preamble 在某些实现中可缩短至 1 个周期(速度模式),但兼容性差,不建议

2.6 常见陷阱与调试小贴士

最后聊几个我踩过的坑,希望能帮你少走弯路。

  • 陷阱一:Preamble 长度被压缩。有些 PHY 支持高帧率模式(Preamble 只发 1 周期 "1"),但默认都要求 32 个。如果通信时好时坏,先看看初始化代码里 Preamble 是不是被偷懒了。
  • 陷阱二:TA 时刻的电平冲突。读帧时主设备要释放总线,但释放延迟、上拉电阻强弱都会影响 PHY 能否正常接管。我曾经用 4.7 kΩ 上拉导致 TA 切换时间不够,换成 1 kΩ 就好了。
  • 陷阱三:寄存器地址乱序。MDIO 帧里 PHY 地址和寄存器地址是 MSB-first。如果你习惯 LSB-first 编程,写寄存器 0x15 却发成了 0x2A,PHY 不会理你。

我的惯用调试法宝:逻辑分析仪抓 MDC+MDIO,再配合一个 Python 脚本把帧解析成字段。比盯着示波器手动数脉冲快十倍。

好了,这一章的基础就这些。MDIO 协议不复杂,但每个细节都可能变成故障的导火索。下一层咱们就要拿着这些知识,去排查实际总线上的各路妖孽了。