2. MDIO 帧格式详解
MDIO 帧结构并不复杂。说白了,它就是一串按照固定顺序排列的比特流。但每个比特位都有明确的用途,搞错了哪个字段,总线就没法正常握手。
我最早调 D-PHY 的时候,对着示波器数过帧。一开始觉得前导码没用,后来才发现——嗯,没有它反而更容易乱掉,这个后面细说。总之,一条完整的 MDIO 帧长这样。
MDIO 帧总长:64 个时钟周期,对应 64 个比特位。
你想想看,要完成一次读写访问,控制器的状态机得按顺序发送这 64 个比特。下面这张图把帧结构拆成了八个阶段。
2.1 前导码(PRE)—— 32 个连续的 1
前导码,说白了就是让总线先「热个身」。主机连续送出 32 个高电平(逻辑 1),持续时间 32 个 MDC 时钟周期。PHY 设备检测到这一串 1 之后,就知道接下来要开始新的一帧了。
我遇到过有人在这里栽跟头。前导码时序不对,PHY 根本不理你。实际上,IEEE 802.3 规范明确要求前导码必须是 32 个连续的 1。少一个或者中间插了个 0,帧就废了。
实战经验:有些 PHY 的兼容性比较好,少于 32 个前导码也能工作。但我建议你不要赌。老老实实发满 32 个 1,这是最省事的做法。我曾在某个项目里偷懒只发了 28 个,结果换个批次的 PHY 就罢工了。
2.2 ST 码(起始码)—— 2 位固定模式
ST 码紧跟在前导码后面,长度为 2 位。规范规定 ST = 01(先 0 后 1)。
为什么不是 00 或 11?我理解这是一种简单的校验手段。前导码全是 1,ST 码以 0 开头,总线电平突然跳变,给 PHY 一个明确的「帧开始」信号。PHY 看到这个跳变,就知道前导码结束了,准备解析地址和命令。
注意:MDIO 帧里没有校验码。ST 码和后面的转向位承担了部分错误检测功能。如果 ST 码不是 01,整帧会被丢弃。
2.3 操作码(OP)—— 读还是写?
OP 码也是 2 位。它告诉 PHY 本次要做什么:
| OP 码 | 含义 |
|---|---|
| 01 | 写操作(主机 → PHY) |
| 10 | 读操作(主机 → PHY 请求,PHY 回复) |
你想想看,OP 码和 PHY 地址、寄存器地址一起决定了帧的后续行为。如果是写操作,主机要把 16 位数据写入 PHY 的指定寄存器。如果是读操作,主机发出地址后,转向位切给 PHY,PHY 再把寄存器的值返回给主机。
我记得第一次设计读时序时犯了个低级错误——我忘了读操作里数据方向会变化。写的时候数据流一直从主机到 PHY;读的时候,转向位之后数据流就反过来了。
2.4 PHY 地址 —— 5 位,一共 32 个设备
PHY 地址占 5 位。这意味着总线上最多可以挂 32 个 PHY 设备(地址 0~31)。
实际应用中,每个 PHY 通过硬件引脚(例如 ADDR0~ADDR4)来设置自己的地址。我习惯用 8 位单片机控制 MDIO,那就要注意:PHY 地址和寄存器地址都是 5 位,但打码的时候一定要对齐。示例里这么发送:
// 伪代码:写 PHY 地址 0x03 的寄存器 0x0A
// 高位在前!
// 前导码(发 32 个 1)
for(i=0; i<32; i++) mdio_write(1);
// ST=01, OP=01(写), PHY_ADDR=00011, REG_ADDR=01010
mdio_write(0); mdio_write(1); // ST
mdio_write(0); mdio_write(1); // OP
mdio_write(0); mdio_write(0); mdio_write(0); mdio_write(1); mdio_write(1); // PHY addr
mdio_write(0); mdio_write(1); mdio_write(0); mdio_write(1); mdio_write(0); // REG addr
// TA = 10, 然后 16 位数据……
2.5 寄存器地址 —— 也是 5 位,同样 32 个寄存器
寄存器地址紧跟在 PHY 地址后面。5 位决定了每个 PHY 内部最多 32 个寄存器。D-PHY 的 CSR 寄存器就落在这个范围里。
寄存器地址的含义由 PHY 厂商自行定义。IEEE 802.3 只规定了地址 0~0x0F 为标准寄存器,用来管理基本状态和协商。0x10~0x1F 通常留给厂商扩展,D-PHY 相关的配置一般就在 0x10 之后。
一个隐晦的坑:有的 PHY 把寄存器 0x10 的数据读出后直接映射到 D-PHY 的 Lane 配置。我第一版代码没看 datasheet,直接读了 0x10 以为拿到了状态,其实是空的。一定要去看 PHY 的寄存器映射表。
2.6 转向位(TA)—— 2 位,控制总线归属
转向位是最容易被轻视的字段。它只有 2 位,但决定了 MDIO 线的驱动权归谁。
写操作时:
- TA 阶段主机仍然驱动 MDIO,发送 10(先 1 后 0)。
- 之后数据位 16 位仍然由主机发送。
读操作时:
- TA 的第一位(Z),主机释放 MDIO 线(高阻),靠上拉电阻保持 1。
- TA 的第二位(0),PHY 接管 MDIO 线,并且主动拉低——嗯,这相当于一个应答信号。
- 之后 16 位数据由 PHY 返回给主机。
我以前在软件模拟 MDIO 的时候,读操作的 TA 总是写不对。因为我忘了释放 GPIO 方向寄存器,这导致总线冲突,PHY 拉低时 MCU 还在输出,读回来的数据全是错的。
小心:做软件模拟 MDIO 时,读操作必须先把 MDIO 引脚切到输入模式,等 TA 到位后再切回输出判断电平。另一个办法是用开漏输出,IH=高阻。我推荐后者,切换更少,时序更稳定。
2.7 数据位 —— 16 位,CSR 寄存器的血肉
数据位是帧里最长的一部分,16 位。高位在前(MSB first)。
对于写操作,主机在 MDIO 线上送出 16 位数据。对于读操作,PHY 在 MDIO 线上返回 16 位数据。D-PHY 的 CSR 寄存器宽度就是 16 位,所以正好一一对应。
我说个细节。有些 PHY 的数据位里前 2 位是保留位。例如读回来的 bit[15:14] 总是 0,bit[13:0] 才是真实值。这时候别傻傻地把整个 16 位都当成有效值。我第一版程序就是直接用了全 16 位,结果配置错了好几个 lane。
2.8 IDLE 状态 —— 帧间的安静时刻
一帧结束后,MDIO 线进入高阻态(IDLE)。靠上拉电阻把电平拉到 1。直到主机开始发送下一帧的前导码。
IDLE 状态的长度没有硬性要求,主机可以在任意空闲时长后发起下一帧。但要注意,两帧之间的 IDLE 太短可能会让 PHY 的状态机混乱。我习惯留至少 1 个 MDC 周期的 IDLE。
实用建议:可以用一个计数器检查 MDIO 线是否连续超过 32 个时钟为 1,判断总线是否处于 IDLE。这在调试时非常有用。
好了,帧结构的七段(加上 IDLE 就是八段)到这里讲完了。你平时调代码时可以把这张表打印出来放在手边。对照着看,99% 的时序问题都能找到原因。