第2章:MDIO帧格式详解

咱们搞硬件的,天天跟各种协议打交道。MDIO的帧格式,说白了就是一条指令的结构。你得告诉从设备:“我要干嘛、找谁、查什么、结果放哪”。这章我就把它的帧格式掰开了讲清楚。

一个标准的MDIO帧,总共64个时钟周期。每个周期传1个比特。我刚开始接触这个协议时,总觉得64个周期太长。后来调试时才发现,每个字段都有它的作用,少一个都不行。

MDIO帧结构图 MDIO帧结构(64位) 前导码(32位) 全部为 1 ST 01 OP码 10/01 PHY ADR 5位 REG ADR 5位 TA Z0 DATA 16位 读操作帧: 前导码(32位1) → ST(01) → OP(10) → PHY_ADR(5位) → REG_ADR(5位) → TA(Z0) → DATA(16位) 写操作帧: 前导码(32位1) → ST(01) → OP(01) → PHY_ADR(5位) → REG_ADR(5位) → TA(10) → DATA(16位)

2.1 前导码(Preamble)- 32个1的“热身”

前导码占前32个时钟周期,全部发送逻辑“1”。它的作用只有一个:让MDIO从设备同步时钟。

为什么需要这个?你想想看,主设备刚上电时,时钟可能还不稳定。从设备需要一段时间来锁定时钟边沿。我个人习惯在设计PHY时,把前导码检测做得严一些——少于32个1就直接丢弃这一帧。

一点经验: 有些老的PHY芯片对前导码要求很严格。我遇到过一种情况,主设备在前导码中间少发了2个时钟,结果PHY死活不响应。折腾了半天,才发现是时序问题。

2.2 起始码(Start of Frame)- 固定为“01”

紧跟在前导码后面的是2位的起始码,固定为“01”。这是帧开始的标志。

你可能会想:为什么不是“00”或者“11”?其实这是为了增加可靠性。“01”这种跳变,从设备很容易检测到。我当时也好奇过这个问题,后来查了IEEE 802.3的规范才知道,设计者是有意选的这个模式。

// C代码例子:检测起始码
uint8_t sof_detected = 0;
if ((mdi_rx_bits[0] == 0) && (mdi_rx_bits[1] == 1)) {
    sof_detected = 1;  // 检测到起始码
} 
// 注意:索引0对应第33个bit(前导码之后的第一个bit)

2.3 操作码(Operation Code)- 读还是写?

操作码也是2位。它决定了这次访问是读操作还是写操作。

操作码 操作类型 说明
10 读操作 主设备读取PHY的寄存器
01 写操作 主设备写入数据到PHY的寄存器

这里有个容易搞错的地方。很多初学者会把读操作码记成“01”,其实是反的。我之前在一个项目里就写反过,结果读回来的数据全是错的。排查了两天才发现是操作码搞反了。说白了,就是没认真看手册。

2.4 地址域(Address Field)- 找对设备,找对寄存器

地址域分两部分,各占5位。

  • PHY地址(5位):总共可以寻址32个PHY设备。
  • 寄存器地址(5位):每个PHY内部最多有32个寄存器。

5位地址能覆盖32个设备,这在大多数应用场景下是够用的。但有些复杂的交换机,可能会用到多个MDIO总线来管理更多PHY。

地址域的范围:PHY地址 0~31,寄存器地址 0~31。你需要通过硬件配置引脚给每个PHY分配一个唯一的地址。

2.5 转向符(Turnaround)- 切换数据方向的关键

转向符占2位,是帧格式里最容易被忽视的地方。

  • 读操作:TA为“Z0”。第一个周期是高阻态,第二个周期是0。
  • 写操作:TA为“10”。第一个周期是1,第二个周期是0。

为什么读操作要有个高阻态?我解释一下:因为MDIO是双向总线,主设备发送完地址后,需要把总线控制权让给从设备。高阻态这段时间,就是给从设备一个“接管”的机会。我曾经调试一块板子,发现读回来的数据始终不对,后来用示波器一看,转向符的时序完全乱掉了。

避坑指南: 转向符的高阻态必须严格控制在1个时钟周期。我曾经因为代码逻辑没处理好,高阻态持续了3个周期,结果PHY以为总线空闲,直接退出了传输。

2.6 数据域(Data Field)- 真正的“干货”

数据域是16位,也就是2个字节。读操作时,由从设备驱动;写操作时,由主设备驱动。

数据的传输顺序是MSB优先,也就是高位在前。举个例子:如果我要写入0xABCD,先传0xAB的高8位,再传0xCD的低8位。

// 写操作:主设备发送16位数据
// 假设要写入的数据是 data_16
for (int i = 15; i >= 0; i--) {
    MDIO = (data_16 >> i) & 0x01;
    MDC ^= 1;  // 产生时钟上升沿
    MDC ^= 1;  // 产生时钟下降沿
}

2.7 帧格式总结

我整理了一张表,把各字段的细节全部列出来,方便你查阅:

字段 位数 内容 驱动方
前导码 32 全部为1 主设备
起始码 2 01 主设备
操作码 2 10(读)/ 01(写) 主设备
PHY地址 5 0~31 主设备
寄存器地址 5 0~31 主设备
转向符 2 Z0(读)/ 10(写) 读:从设备 / 写:主设备
数据域 16 2字节数据(MSB优先) 读:从设备 / 写:主设备

嗯,内容基本就是这样。你把这些字段记熟,之后再分析MDIO时序,心里就有谱了。

快速记忆: 32位前导码全1 → 2位起始码01 → 2位操作码(读10/写01)→ 5位PHY地址 → 5位寄存器地址 → 2位转向符 → 16位数据。总共正好64位。