4. MDIO/MDC 总线协议详解:信号定义与电气特性

MDIO 跟 MDC,这俩名字听起来挺唬人。说白了,就是管理以太网 PHY 芯片的一条串口。我刚开始接触的时候也觉得这东西挺玄乎,两根线就能搞定配置?后来做项目多了,发现就这么两根线,坑还不少。咱们今天把它彻底搞明白。

4.1 信号定义:就是两根线的事儿

MDIO 总线其实就两路信号:

  • MDC (Management Data Clock):管理数据时钟。由 MAC 侧或者控制器主动产生,是个方波。
  • MDIO (Management Data Input/Output):管理数据输入输出。这是一根双向数据线,既用来发命令,也用来收数据。

你可以把 MDC 想象成节拍器,MDIO 就是跟着节拍跳舞的人。谁打拍子?打拍子的人永远是主设备(一般是 CPU 或者 MAC 控制器)。PHY 芯片只管跟着拍子回应数据。这个关系搞清楚,后面的时序就自然明白了。

小经验: 我习惯在设计原理图时,MDIO 线路上加一个 4.7kΩ 的上拉电阻。很多 PHY 芯片内部已经有弱上拉了,但加上一个外部上拉,总线在空闲状态时电平会更稳定,不容易发生误触发。

4.2 电气特性:电压域匹配与布线

嗯,这里要注意。MDIO/MDC 的电压通常跟相连的 MAC 或控制器 IO 电压一致。常见的有 3.3V 和 1.8V 两种。我个人习惯在搞定原理图之前,一定要先确认两边的电压域是否匹配。

参数典型值说明
MDC 最高频率2.5 MHz (标准) / 12.5 MHz (高速)标准模式对齐 802.3 规范
MDIO 空闲状态高阻 / 上拉高电平待机时避免总线冲突
上拉电阻1.5kΩ ~ 10kΩ取决于总线负载和走线长度
负载电容不超过 50pF 为宜线太长或者挂的 PHY 太多会影响信号质量

布线方面,能短尽量短。MDC 虽然是时钟,但速度不高,一般不用走差分或者阻抗控制。不过,如果你想把它拉到排针或者连接器上,我建议加个小电阻(比如 22Ω)靠近发送端串一下,可以减少过冲。我曾经在一个项目里就因为 MDC 线走得太长还没加串阻,导致读回来的寄存器值老是跳变,查了整整一个下午。

避坑提醒: 同一个 MDIO 总线上可以挂多个 PHY 芯片(一般最多 32 个)。但每多挂一片,总线寄生电容就会增加。如果挂的芯片多,建议降低 MDC 频率,或者用总线缓冲器隔离一下。

4.3 协议时序:读和写,就是串行帧

MDIO 的协议帧结构,在 IEEE 802.3 规范里定义得很清楚。我把它拆成两部分来讲:写帧和读帧。你想想看,它俩的区别其实就两个 bit 的事儿。

MDIO 帧结构图解(读/写对比) 写帧 (OP=01) PRE 32'b1 ST 01 OP 01 PHYAD 5'b REGAD 5'b TA 10 DATA 16'b (MAC → PHY) 读帧 (OP=10) PRE 32'b1 ST 01 OP 10 PHYAD 5'b REGAD 5'b TA Z0 DATA 16'b (PHY → MAC) 典型读写时序波形示意 MDC MDIO 写: TA=10 后 MAC 驱动 DATA 读: TA=Z0 后 PHY 驱动 DATA 方向切换点

仔细看看这张图的帧结构。每一帧都包含以下几个字段:

  1. 前导码 (PREAMBLE):32 个连续的 '1'。这是同步用的,告诉 PHY 准备接收命令。有的控制器支持跳过前导码,但我建议除非你明确知道 PHY 支持,否则别省。
  2. 帧起始 (ST):固定为 '01'。标识一个有效管理帧的开始。
  3. 操作码 (OP):'01' 代表写,'10' 代表读。就这两 bit,决定了数据流向。
  4. PHY 地址 (PHYAD):5 bit,支持 0~31。板子上每个 PHY 芯片的地址必须唯一。
  5. 寄存器地址 (REGAD):5 bit,支持 0~31。这就是我们常说的 MMD 或者 Clause 22 的直接寄存器。
  6. 周转状态 (TA):这是个两时钟周期的空档。写操作时,MAC 驱动 '10';读操作时,MAC 释放总线,PHY 先回应一个 '0'(高阻转低),再拉高,代表准备好数据。
  7. 数据 (DATA):16 bit。写操作由 MAC 发出,读操作由 PHY 回复。
关键知识点: 读写操作的本质区别只在 OP 码和 TA 阶段的驱动方向。读操作时,TA 的第一个 bit 是 PHY 把 MDIO 线从高阻拉低,这是一个 "turn-around" 过程。如果这一步没跑通,读回来的数据全是 0xFFFF,别问我怎么知道的...

4.4 地址映射与 Clause 22 / Clause 45

讲到这里,不得不提 IEEE 802.3 的两个标准子句。

标准PHY 地址寄存器地址特点
Clause 225 bit (0-31)5 bit (0-31)基础、简单、普遍
Clause 455 bit (端口地址) + 21 bit (设备类型/地址)16 bit支持更复杂、更多扩展寄存器

大多数通用 PHY 都支持 Clause 22。如果你在做千兆网或者更复杂的 PHY,可能就需要用到 Clause 45 了。Clause 45 的帧格式略有不同,它把地址空间扩展得更大。但我个人的建议是:前期先搞定 Clause 22 的基本读写功能,把控制通路跑通之后再去看 Clause 45 的扩展访问,这样调试起来不会乱。

4.5 寄存器读写流程示例

下面我写一段伪代码,帮你捋一下实际的底层操作流程。这在嵌入式驱动开发里非常典型。

// 伪代码:写 PHY 寄存器
void mdio_write(phy_addr, reg_addr, data) {
    // 1. 构造帧字段:前导码 (32个1) + ST(01) + OP(01) + PHYAD + REGAD + TA(10) + DATA
    // 2. 驱动 MDIO 引脚为输出(写操作全程由 MAC 驱动 MDIO)
    // 3. 对于 MDC 的每个上升沿,按位送出 MDIO 数据
    // 4. 发送完后,维持 MDIO 为高阻,等待下一帧
    
    mdio_set_dir(OUTPUT);
    
    // 发送前导码 32 个 '1'
    for (int i = 0; i < 32; i++) {
        mdio_set_bit(1);
        mdio_toggle_mdc();
    }
    
    // 发送 ST + OP + PHYAD + REGAD + TA + DATA
    // 具体按位发送省略...
    
    mdio_set_dir(INPUT); // 释放总线
}
// 伪代码:读 PHY 寄存器
uint16_t mdio_read(phy_addr, reg_addr) {
    uint16_t data = 0;
    
    // 1~4 步同上,但 OP 码为 '10'
    // TA 阶段:MAC 先释放 MDIO,转为输入
    // PHY 在 TA 的第二位开始驱动 MDIO
    
    mdio_set_dir(OUTPUT);
    // 发送前导码 32 个 '1'
    // 发送 ST + OP + PHYAD + REGAD
    
    mdio_set_dir(INPUT); // TA 阶段前切换方向
    
    // 等待 TA 完成(PHY 拉低再拉高)
    // 读取 16 bit 数据
    
    for (int i = 0; i < 16; i++) {
        mdio_toggle_mdc();
        data <<= 1;
        data |= mdio_read_bit(); // MDC 上升沿读取
    }
    
    return data;
}
实战贴士: 读操作切换 MDIO 方向的那个时刻非常关键。我曾经遇到的一个坑是:MCU 的 GPIO 在输入输出方向切换时会产生一个毛刺,正好被 PHY 当成 start bit 给误判了。后来我在切换方向后加了一个 dummy 时钟周期,问题就解决了。

4.6 总线拓扑与常见电气问题

MDIO 总线通常是开漏加外部上拉的结构。由于是双向传输,时序上对上升沿和下降沿有要求。你想想看,一根线上既有主设备驱动,又有从设备驱动,信号质量处理不好就容易出问题。

  1. 总线竞争: 如果多个主设备在同一总线上,或者 PHY 未正确释放 MDIO,会导致电平冲突。我建议在设计时就明确:总线只允许一个主设备驱动。
  2. 信号斜率: MDC 频率提高后,信号边沿变缓可能会导致建立时间不够。可以使用施密特触发器输入的引脚,或者在 PCB 上加小电容滤波。
  3. 电平转换: MAC 和 PHY 电压不一致时,需要加电平转换芯片。我通常选用 BSS138 搭建的简单双向电平转换电路,便宜又好用。

嗯,关于 MDIO/MDC 总线的核心内容,就讲到这里。这部分理解了,后面讲寄存器具体配置时,你就知道怎么下命令、怎么收数据了。