MDIO 读写协议:帧格式、操作码、地址
MDIO 这个协议,说白了就是 MAC 层和 PHY 芯片之间的一条“私聊通道”。你给它一条指令,它告诉你 PHY 当前的状态。这条指令长什么样?我带你拆开看看。
帧格式长什么样?
MDIO 的每一帧数据,都严格按照 64 个时钟周期来走。你想想看,64 个时钟就是一帧,不多不少。我最早调 MDIO 的时候,总以为少几个周期没关系,结果读回来的数据全是 0xff——嗯,那是我踩的第一个坑。
这 64 个时钟被分成下面这些字段:
| 字段 | 位数 | 描述 |
|---|---|---|
| Preamble(前导码) | 32 | 连续 32 个“1”,用来同步时钟 |
| Start(起始码) | 2 | 固定 “01”,表示帧开始 |
| Opcode(操作码) | 2 | 决定是读还是写 |
| PHY Address | 5 | 指定你要找哪个 PHY |
| Register Address | 5 | 指定 PHY 内部的哪个寄存器 |
| Turnaround(转向位) | 2 | 总线切换的缓冲时间 |
| Data(数据) | 16 | 读回来的数据,或要写进去的数据 |
| Idle(空闲位) | — | 高阻态,总线释放 |
操作码:读还是写?
操作码就两位,但这两位的意义重大。它决定了你是要问 PHY 要数据,还是给 PHY 发指令。
| Opcode | 功能 | 说明 |
|---|---|---|
| 00 | 写操作 | MAC 向 PHY 写入 16 位数据 |
| 01 | 保留 | 标准里没定义,不要用 |
| 10 | 读操作 | PHY 向 MAC 返回 16 位数据 |
| 11 | 保留 | 同上 |
你看,读操作是 “10”,写操作是 “00”。这里有个细节,读和写的操作码故意留了位差异,就是为了防止误判。我在一个项目里就遇到过 MAC 层发错了操作码,把读写成写,结果把整个 PHY 的寄存器写乱了——设备直接断网。从那以后,我每次发帧前都会先打印操作码看一眼。
地址:怎么找到目标 PHY?
地址分两层:PHY 地址和寄存器地址。5 位 PHY 地址,最多可以挂 32 个 PHY 设备。5 位寄存器地址,每个 PHY 内部有 32 个寄存器。
| 地址类型 | 位宽 | 范围 | 说明 |
|---|---|---|---|
| PHY Address | 5 位 | 0x00 – 0x1F | 由硬件管脚决定,不能动态改 |
| Register Address | 5 位 | 0x00 – 0x1F | 标准前 16 个有定义,后 16 个厂家自定义 |
PHY 地址通常由 PHY 芯片的 ADDR 管脚上的上下拉电阻决定。你想想看,如果板子上有 5 个 PHY,它们的地址分别是 0x01、0x02、0x04、0x08、0x10,这其实就是靠管脚的二进制编码实现的。我最早做这个实验时犯过一个很初级的错误——我把两个 PHY 的地址都设成了 0x01,结果读到的一直是 0xffff。
寄存器地址 0x00 到 0x0F 是 IEEE 802.3 标准规定的,所有 PHY 必须实现。比如 0x00 是控制寄存器(BMCR)、0x01 是状态寄存器(BMSR)。后面的 0x10 到 0x1F 则由 PHY 厂家自己定义,用来存放扩展能力或诊断信息。我记得见过一个 PHY,它的厂家自定义寄存器里存了温度值,简直离谱。
你记住这个 64 时钟的帧结构,后面所有的 PHY_ID 读取都要靠它来发。我个人习惯是把帧的每个字段都定义成一个宏,调试的时候逐段检查,从 preamble 到 data,哪里断了都能快速定位。