第2章 MDIO/MDC接口协议:MII管理接口的读写时序与帧格式

MII管理接口,说白了就是PHY芯片的”配置通道“。你要调速率、改电平、查状态,全得靠这两根线——MDIO和MDC。我在项目里见过不少同事,配寄存器前根本不关心时序,结果读回来的数据全乱套。嗯,这一章咱们就把它彻底捋清楚。

2.1 什么是MDIO/MDC?

MDIO(Management Data Input/Output)是双向数据线,MDC是时钟线。它们组成了一个类似SPI的串行总线,但比SPI更“薄”——只有一主多从、固定帧长。我习惯叫它”小两线“。

  • MDIO:数据信号,由主设备(MAC或CPU)驱动,PHY在特定相位应答。
  • MDC:时钟信号,最高速率通常2.5MHz(某些支持4MHz),由主设备产生。
  • 拓扑:MDC共享给所有PHY,MDIO需要上拉电阻(典型值1.5kΩ~4.7kΩ)。
经验:上拉电阻别太小,否则MDIO上升沿会变缓,边距吃紧。我一般用2.2kΩ,32个PHY挂一条总线也杠杠的。

2.2 帧格式详解

为什么说MDIO协议的帧结构特别”固执“?因为IEEE 802.3规定死了:一帧必须是32个时钟周期+数据段。我当年第一次写驱动时,少了1个时钟就吞了整个世界……

具体的帧由下面几个字段组成(以Clause 22为例):

字段名位数方向说明
PREAMBLE32主→从连续32个“1”,MDIO保持高阻前同步
ST (Start of Frame)2主→从固定为 01
OP (Op Code)2主→从10 读操作;01 写操作
PHYAD5主→从5位PHY地址,范围0~31
REGAD5主→从5位寄存器地址,范围0~31
TA (Turn Around)2高阻抗→主/从读时:第1个时钟高阻, 第2个时钟PHY驱动0;写时:主设备驱动第2个时钟0
DATA16主/从写:主设备输出;读:PHY输出

简单归纳:一帧要读或写一个16位寄存器。前32位preamble是“清场”,之后依次送地址、操作码、PHYAD、REGAD,最后是TA和DATA。我在项目里调试时,最喜欢用逻辑分析仪抓TA段——如果PHY没正确回应0,大概率是地址配错了。

// 读写操作对照表(Clause 22)
// OP码: 10 = 读, 01 = 写
// PREAMBLE: 32个1
// 示例:读PHY地址1,寄存器0x10
// 帧序列:
// [1]*32 + 01 + 10 + 00001 + 10000 + Z/0 + 16-bit data
// 写:同上,但TA位由主设备驱动

2.3 读写时序图

先看读操作时序。MDC上升沿采样MDIO。前32个1之后,ST、OP、PHYAD、REGAD都在MDC上升沿被PHY捕获。TA段特别有意思:第33个时钟MDIO进入高阻,第34个时钟PHY拉低MDIO作为响应。如果你用GPIO模拟,这个高阻必须靠三态门实现,否则总线冲突。

写操作类似,只不过TA段第2个时钟由主设备拉低,然后直接输出16位数据。嗯,这里要注意:写操作后PHY会立即把新值写入寄存器,所以连续读写同一个寄存器时,中间最好插一个空周期(或等待至少10个MDC)让PHY稳定。

曾经的坑:我曾经在某个FPGA项目里,写寄存器后紧接着读同一地址,发现读回的还是旧值。排查半天——原来PHY内部的更新需要1~2个MDC时钟延迟。解决办法:写完后发一个dummy帧(16个1),再读。

2.4 SV图:MDIO/MDC句协议状态机

MDIO/MDC 帧协议状态机 IDLE MDIO=高阻 PREAMBLE 32个1 ST + OP 4位 PHYAD + REGAD 10位 TA 2位,Z/0 DATA 16位 END 回IDLE ① 空闲 ② 同步 ③ 操作字 ④ 地址 ⑤ 周转 ⑥ 数据 ⑦ 结束

2.5 Clause 22 vs Clause 45

你可能已经听说过Clause 45——这是后来为了扩展寄存器地址(最多32个页面,每个页面最多32个寄存器)而设计的。我做千兆PHY时常用Clause 22,但遇到10GBase-T这类复杂PHY,Clause 45是必选项。

对比项Clause 22Clause 45
帧长度64个MDC(PRE+ST+OP+ADDR+TA+DATA)64个MDC(PRE+ST+OP+PORT+DEV+TA+DATA)
ST字段0100
OP字段10读,01写00读(递增),11读(固定),01写
PHYAD vs DEVADPHYAD 5位 + REGAD 5位PORTAD 5位 + DEVAD 5位 + DATA/ADDR
地址空间32 PHY × 32 寄存器(1KB)32 PORT × 32 DEV × 64KB 地址空间
典型应用百兆/千兆PHY万兆/多端口/MACsec PHY

你想想看,如果你的PHY支持Clause 45,但MAC只实现了Clause 22,那怎么办?有些Phy可以通过strap引脚或shadow寄存器切换模式。我曾在某款国产PHY上踩过这个坑——寄存器手册写的是Clause 45,但读操作却按Clause 22也能得到部分数据,其实是内部做了映射,但地址重叠了……

核心要点:MDIO/MDC协议的精髓在于帧结构严格遵守顺序和位宽。无论是Clause 22还是Clause 45,PREAMBLE、TA段的高阻控制、数据写入后的延迟都是最容易出问题的地方。调试时建议先用示波器或逻辑分析仪抓一次完整的读操作,确认ST、OP、TA和data都在正确时间窗口内。

2.6 软件模拟的注意事项

很多嵌入式系统用GPIO模拟MDIO/MDC。我个人的习惯是:先拉低MDC,然后设置MDIO,再拉高MDC,保持至少1μs高电平脉冲。为什么?因为PHY内部有采样窗口,如果MDC高电平太短,采样可能不稳。

  1. 初始化时GPIO配置为开漏输出,外部上拉电阻拉到3.3V。
  2. 每次发送位前,MDC先置低,延时至少100ns。
  3. MDIO设置电平后,MDC置高,延时至少100ns(如果CPU主频高,最好用NOP循环或定时器)。
  4. 读操作时,在MDC高电平期间采样MDIO电平(用读IO指令)。
  5. TA段的高阻态:通过GPIO方向寄存器输入模式实现。
// 伪代码:写一位到MDIO
void mdio_write_bit(int bit) {
    gpio_set(MDC_PIN, 0);      // 拉低MDC
    delay_ns(200);
    gpio_set(MDIO_PIN, bit);   // 设置MDIO电平
    delay_ns(200);
    gpio_set(MDC_PIN, 1);      // 拉高MDC
    delay_ns(200);
}

// 读一位:在MDC高电平时采样
int mdio_read_bit(void) {
    gpio_set(MDC_PIN, 0);
    delay_ns(200);
    gpio_set(MDC_PIN, 1);
    delay_ns(100);            // 等PHY输出稳定
    int val = gpio_get(MDIO_PIN);
    delay_ns(100);
    return val;
}
注意:GPIO模拟时,延迟精度不够会导致读数据漂移。我曾经在一个跑400MHz的Cortex-M7上,用简单delay_loop反而比原厂驱动快了一倍,结果所有读回数据都左移了一位。后来查原因:编译器优化把循环展开了,实际延时缩短。加个volatile变量和__NOP解决。

好了,MDIO/MDC协议的基本内容就这些。时序和帧格式是后续寄存器配置的地基,理解得越透,后面踩坑越少。接下来你可以照着帧结构自己动手抓一次读写波形,再结合PHY的数据手册验证地址和数据的对应关系。嗯,看到波形和预期一致时,那种成就感——我当年第一次看到MDIO上回来的16位数据完全正确,高兴了整整一天。