MDIO帧结构详解:从前导码到Turnaround,每一步都不能错
MDIO的通信说白了就是一段固定格式的串行数据帧。你想想看,上位机要跟PHY芯片对话,总得有个双方都认的“语言”吧?这个帧结构就是它们的通用语言。我当年第一次调PHY的时候,总以为帧结构背下来就行,结果读回来的寄存器全是0xFFFF——后来才发现是Turnaround时序没对。嗯,咱们今天就把它彻底拆开讲清楚。
一句话总结MDIO帧: 32位前导码 + 2位起始码 + 2位操作码 + 5位PHY地址 + 5位寄存器地址 + 2位Turnaround + 16位数据。总共64位(写操作)或64位(读操作,数据由PHY驱动)。
前导码(Preamble)—— 32个连续的1
前导码的作用是让PHY同步时钟,把接收器从空闲状态拉回来。帧开始前,主机需要连续输出32个逻辑1。
- 长度:32位,全是1。
- 注意事项:有些PHY对前导码的占空比有要求,不能跳变太快。
我在项目中遇到过一款国产PHY,前导码少了一个时钟周期,直接就失同步了。后来加上32个1就稳定了。我的习惯是一开始就传满32个,绝不偷工减料。
起始码(Start of Frame)—— 0b01
前导码之后紧跟着2位起始码,固定为01。这个组合告诉PHY:“真正的交易要开始了”。
有人问:“为什么不直接用11?” 我个人理解,01的跳变(1→0→1)能提供一个清晰的相位参考,让PHY内部的采样逻辑对齐。说白了就是给个明确的开始信号。
操作码(Opcode)—— 读还是写,全靠它
操作码是2位,定义如下:
| 操作 | Opcode [1:0] | 说明 |
|---|---|---|
| 写操作 | 01 | 主机向PHY写入数据 |
| 读操作 | 10 | 主机向PHY读取数据 |
你可能会奇怪:为什么读写不是00或11? 实际上MDIO规范里这两个码是保留的,我猜是为了避免与空闲状态(高阻态)混淆。记得有一次我在设计里把操作码写反了,读操作给了01,结果PHY死活不响应——检查了半小时才发现字节序搞错了。
小技巧: 如果你用FPGA写MDIO控制器,建议把操作码单独定义一个宏,比如 #define OP_WRITE 2'b01 和 #define OP_READ 2'b10,代码可读性提升一个档次。
PHY地址与寄存器地址—— 5 + 5 = 10位的寻址空间
操作码之后紧跟5位PHY地址(高字节在前)和5位寄存器地址(同样高字节在前)。
- PHY地址范围:0~31。通常一颗PHY芯片有一个默认地址(比如0x00或0x01),通过硬件配置引脚可以改变。
- 寄存器地址范围:0~31。IEEE 802.3定义了一些标准和可选寄存器,0~15是必选的基本寄存器。
避坑指南:我曾经遇到板子上两个PHY的地址都设成了0x00,结果总线冲突,读回来的数据经常错位。后来强制给每个PHY分配唯一地址,问题解决。建议设计时至少保留一个地址可配置。
Turnaround时间(TA)—— 这里最容易踩坑
Turnaround是2位,它的作用非常特别:在地址发送完后,总线需要切换驱动方向(如果是读操作)。
具体规则:
- 写操作: 主机持续驱动MDIO线。TA为
10(主机发送),然后主机直接发送数据。 - 读操作: 主机释放MDIO线(输出高阻),PHY必须拉低MDIO一个周期(第1位为0),再在第2位拉高(第2位为1),组合为
Z0(第一拍高阻)0(PHY驱动为低)1(PHY驱动为高)—— 等效于PHY输出01。
警告: 很多新手在读操作时,忘记在TA阶段把MDIO线转为输入,导致主机和PHY同时驱动,轻则读回错误数据,重则烧坏IO。我自己的检查清单:读之前,先把MDIO方向寄存器改为输入,再延时至少一个时钟周期,让PHY拿到总线控制权。
你可能会问:“为什么TA不干脆设计成固定值?” 嗯,其实规范里写操作TA就是固定10,读操作TA的0和1是PHY发出的,所以主机必须准备好接收。这一点在时序仿真中特别容易忽略,我习惯写一个简单的状态机来处理方向切换,而不是靠延时。
数据位(Data)—— 16位,MSB first
最后是16位数据,高比特在先(MSB first)。写操作时数据由主机驱动,读操作时数据由PHY驱动。
- 写操作:主机在16个时钟周期内依次送出 bit15..bit0。
- 读操作:PHY在16个时钟周期内依次送出 bit15..bit0,主机采样。
注意:数据位之后没有额外的校验或停止位。整个帧结束,MDIO线回到空闲状态(高阻,通常由上拉电阻拉高)。
帧结构一览——一张图看懂
下面我用SVG画了一张典型的MDIO读操作帧,你可以直观看到每个字段在时间轴上的位置和长度。
常见问题排查
根据我多年的调试经历,帧结构问题通常集中在以下几点:
- 前导码不足32位 → PHY无法同步,返回全部为1或0。
- 起始码不是01 → 帧起始错误,PHY忽略。
- 操作码/地址顺序搞反 → 比如把PHY地址放在操作码前面,总线协议错乱。
- TA时序不满足 → 读操作时主机没有及时释放总线,导致冲突。
- 数据位MSB/LSB理解错误 → 读回来的数值完全不对头,比如应该是0x0F,结果变成0xF0。
我的调试习惯: 先用示波器抓MDC和MDIO的波形,数一下每个字段的跳变次数。如果前导码只看到31个正的脉冲,那就是少了1位。这种方法比看代码log更直观。
掌握了帧结构,下一步就是把这些位组合成实际的读写时序。不过在此之前,我建议你先自己手写一个简单的MDIO发送函数,把上面7个字段拼起来跑通一次——相信我,踩过这个坑,后面所有时序调整都会变得清晰很多。