4. MDIO 帧格式详解:前导码、起始码、操作码、地址、数据
MDIO 通信说到底就是一套约定好的“暗号”。
主控(MAC 或 CPU)往总线上发一串电平变化,PHY 芯片听到后,就知道你要干嘛——读哪个寄存器、写什么值。这套“暗号”就是 MDIO 帧格式。
我最早看手册里那个时序图时,说实话,有点懵。一堆箭头、一堆延迟参数。后来啃了几个实际项目,回头再看才发现,帧结构其实很清晰,就是五个固定部分拼起来。
4.1 帧格式总览
一个标准的 MDIO 帧,无论读还是写,长度都是 64 个时钟周期。你想想看,64 个比特,正好对应一个完整的事务。
我把帧结构拆成一张表格,这样看更直观:
| 字段 | 比特数 | 读操作值 | 写操作值 | 说明 |
|---|---|---|---|---|
| 前导码 (Preamble) | 32 | 全 1 | 全 1 | 同步总线,让 PHY 准备好接收命令 |
| 起始码 (Start) | 2 | 0 1 | 0 1 | 标志一个有效帧的开始 |
| 操作码 (OP) | 2 | 1 0 | 0 1 | 区分是读还是写 |
| PHY 地址 (PHY ADDR) | 5 | AAAAA | AAAAA | 选择与哪个 PHY 通信(0x00 - 0x1F) |
| 寄存器地址 (REG ADDR) | 5 | RRRRR | RRRRR | 选择 PHY 内部的哪个寄存器(0x00 - 0x1F) |
| 转态位 (TA) | 2 | Z0 | 1 0 | 总线方向切换控制,读操作有高阻态 |
| 数据 (Data) | 16 | 读回数据 | 写入数据 | 实际要读或写的寄存器值 |
| 总计 | 64 | - | - | 固定 64 个 MDC 时钟周期 |
注意看,读和写的区别主要在于操作码和转态位。这个转态位是很多初期调试容易翻车的地方,我后面会细讲。
4.2 前导码和起始码——总线的“起手式”
前导码 (Preamble)
说白了,前导码就是 32 个连续的“1”。主控在 MDIO 线上连续拉高 32 个时钟周期。
为什么要这么多?我个人的理解是:PHY 芯片上电后内部状态不确定,32 个连续的 1 能让它进入一个已知的“等待命令”状态。尤其是对于多端口交换芯片,前导码长了反而更可靠。
我记得有一次调试一款国产 PHY,前导码长度写死了 32 位,但那个 PHY 要求至少 48 位才能唤醒。嗯,这里要注意:虽然 IEEE 802.3 标准规定是 32 位,但个别芯片会有特殊要求,最好看下数据手册里的“Timing Specification”章节。
起始码 (Start)
前导码结束后,紧跟着就是起始码“01”。这是个固定序列,告诉 PHY:“我要开始发正经命令了,注意听。”
你可能会问,前导码不也是“1”吗,为什么不会混淆?
关键就在这个“0”到“1”的跳变。前导码全是 1,起始码先拉低再拉高,这个下降沿就是 PHY 用来对齐后续数据采样的参考点。
4.3 操作码——你是读还是写?
操作码就 2 个比特,决定了这个帧是读还是写:
- 读操作:10(先发 1,再发 0)
- 写操作:01(先发 0,再发 1)
这个顺序我记了很久才记牢。一开始总以为读是 01、写是 10。后来我自己总结了一个小技巧:
读的时候,主控要先“发话”(1),再“松口”(0)——因为后面 PHY 要开始回数据了。写的时候,主控先“闭嘴”(0),再“开讲”(1)——因为全程都是主控在驱动。
当然这只是我自己的记忆法,不一定适合每个人。
技巧提示: 在写 MDIO 驱动时,操作码最好用宏定义。比如:
#define MDIO_OP_WRITE 0x02 // 二进制 01,左对齐到高两位
#define MDIO_OP_READ 0x01 // 二进制 10,左对齐到高两位
这样代码可读性会好很多,也便于后期维护。
4.4 地址域——选哪个 PHY、哪个寄存器?
PHY 地址 (5 位)
MDIO 可以挂多个 PHY 在同一条总线上。每个 PHY 有一个物理地址,范围 0x00 到 0x1F,一共 32 个。
地址怎么来的?一般是硬件管脚决定的。PHY 芯片会有一组地址配置引脚(比如 AD0~AD4),上电或复位时硬件采样电平决定地址。
我在项目中遇到过一个问题:板子上两个 PHY 的地址配置都默认是 0x01,结果通信时两个 PHY 同时回应,数据完全乱掉。后来加了个上拉电阻改掉其中一个的地址才解决。你想想看,总线竞争时读到的东西有多离谱就明白了。
寄存器地址 (5 位)
每个 PHY 内部有 32 个寄存器(0x00 ~ 0x1F)。其中前 16 个是 IEEE 802.3 强制规定的基本寄存器,后 16 个由芯片厂商自定义。
常用的基本寄存器包括:
| 地址 | 名称 | 说明 |
|---|---|---|
| 0x00 | BMCR (Basic Mode Control Register) | 控制复位、Loopback、速度、全/半双工等 |
| 0x01 | BMSR (Basic Mode Status Register) | 链接状态、能力协商结果等 |
| 0x02 | PHY ID 寄存器 (高 16 位) | 芯片厂商和型号标识 |
| 0x03 | PHY ID 寄存器 (低 16 位) | 芯片厂商和型号标识 |
| 0x04 | ANAR (Auto-Negotiation Advertisement) | 宣告本端支持的能力 |
| 0x05 | ANLPAR (Auto-Negotiation Link Partner) | 对端宣告的能力 |
4.5 转态位和数据——最关键也最容易出问题的地方
转态位 (TA, Turnaround)
这是 2 个时钟周期,用于总线方向切换。读和写在这里行为完全不同。
- 写操作:TA = 10。主控一直驱动总线,没有方向变化。第一个周期发 1,第二个周期发 0。
- 读操作:TA = Z0。第一个周期主控释放总线(输出高阻),让给 PHY 驱动;第二个周期 PHY 把数据线拉低(发 0),作为数据开始的标志。
为什么读操作需要高阻态?因为只有这样才能让 MDIO 变成一个双向总线。主控不发数据了,PHY 才能接过去。
我在调试一块 FPGA 加 PHY 的板卡时,驱动里读操作的 TA 第一个时钟没有设成高阻,而是强行拉低了。结果 PHY 永远收不到正确的读回复。查了两天才发现是这里出了问题。所以,读操作千万要记住:第一个 TA 时钟必须释放总线。
数据位 (Data, 16 位)
数据部分固定 16 位,MSB 先传输。不管是读还是写,格式都一样。
- 写操作:主控在 TA 之后立刻输出 16 个比特的数据。
- 读操作:PHY 在 TA 之后把 16 个比特的数据逐位放到总线上,主控在每个 MDC 上升沿采样。
写数据示例:
// 写 PHY 地址 0x03、寄存器 0x0A,写入值 0x1234
// MDIO 帧比特序列 (MSB first):
// 前导码(32个1): 1111...1111
// 起始码: 01
// 操作码(写): 01
// PHY地址(0x03): 00011
// 寄存器(0x0A): 01010
// TA: 10
// 数据(0x1234): 0001 0010 0011 0100
// 总比特: 32+2+2+5+5+2+16 = 64
读数据示例:
// 读 PHY 地址 0x03、寄存器 0x0A,读回值假设为 0x5678
// MDIO 帧比特序列 (主控发送部分):
// 前导码(32个1): 1111...1111
// 起始码: 01
// 操作码(读): 10
// PHY地址(0x03): 00011
// 寄存器(0x0A): 01010
// TA(主控释放): Z (高阻)
// MDIO 帧比特序列 (PHY 回复部分):
// TA(继续): 0
// 数据(0x5678): 0101 0110 0111 1000
警告: 读操作时,如果主控没有正确释放总线,PHY 会检测到总线被占用而放弃回应。常见的表现就是读回的数据全为 0 或全为 1。遇到这种情况,先检查 TA 阶段的总线控制逻辑——我吃了这个亏不少次。
4.6 时序参数(写在前面的话)
帧结构讲清楚了,但硬件上还有几个关键时序要求:
- MDC 最高频率: 规范上写 2.5 MHz,但很多现代 PHY 可以跑到 25 MHz 甚至更高。保险起见,前期按 2.5 MHz 跑。
- MDIO 数据建立时间: 在 MDC 上升沿之前,数据需要至少保持 10 ns 稳定。
- MDIO 数据保持时间: 在 MDC 上升沿之后,数据至少再保持 10 ns。
这些时序参数不是固定的,不同厂家的 PHY 会略有差异。我个人习惯是在驱动里把 MDC 频率配得保守一些,等所有功能都调通了,再慢慢把频率往上提。
4.7 本章小结
MDIO 帧格式其实就是一套固定长度的串行协议:前导码唤醒 -> 起始码对齐 -> 操作码选模式 -> 地址选目标 -> 转态位切换方向 -> 数据传输。读和写的差异主要在操作码和 TA 阶段。
我建议你在看波形的时候,先把 64 个时钟周期数清楚,然后一帧一帧对。这样排查问题会快很多。