MDIO 读写操作流程:读操作时序、写操作时序、总线仲裁
MDIO 读写,说白了就是通过 MDC 和 MDIO 两根线,把 PHY 寄存器里的值读出来或者写进去。协议本身不复杂,但时序细节一旦搞错,抓回来的波形就是一团乱麻。我刚调 PHY 那会儿就吃过这个亏——读回来的数据全是 0xFFFF,折腾了半天才发现是 TA 阶段释放总线的时间没算对。
这一节咱们就把读操作、写操作和总线仲裁掰开揉碎了讲。你想想看,搞清楚了这三个东西,MDIO 调试你就掌握了七八成。
读操作时序
读操作的目标是从 PHY 的某个寄存器里读出 16 位数据。整个过程由 STA 发起,PHY 负责响应。嗯,这里有个关键点:读操作中总线控制权会切换一次。
我记得第一次调试读时序时,波形上数据总是不对。后来发现是 TA 阶段 STA 释放总线晚了半个周期,PHY 拉低 MDIO 时两边怼上了。你想想看,一个是推挽输出,一个是开漏输出,不打架才怪。
标准读操作帧结构如下:
- 前导码(PRE):STA 连续驱动 32 个 1,让总线进入同步状态。
- 起始码(ST):发送 2'b01,标志一帧的开始。
- 操作码(OP):发送 2'b10,表示本次为读操作。
- PHY 地址:发送 5 位 PHY 地址,高位在前。
- 寄存器地址:发送 5 位寄存器地址,高位在前。
- 转向时间(TA):STA 释放 MDIO 线(高阻),PHY 接管并驱动 2 位。第一位 PHY 拉高,第二位 PHY 拉低。
- 数据(DATA):PHY 连续驱动 16 位数据,STA 在每个 MDC 上升沿采样。
- 结束:MDIO 回到高阻或空闲状态。
写操作时序
写操作比读操作简单一些,因为总线从头到尾都由 STA 控制,没有控制权切换。说白了,就是 STA 一个人把整帧数据推完。
标准写操作帧结构如下:
- 前导码(PRE):STA 连续驱动 32 个 1。
- 起始码(ST):发送 2'b01。
- 操作码(OP):发送 2'b01,表示本次为写操作。
- PHY 地址:发送 5 位 PHY 地址。
- 寄存器地址:发送 5 位寄存器地址。
- 转向时间(TA):STA 继续驱动 MDIO 线,发送 2'b10。第一位拉高,第二位拉低。
- 数据(DATA):STA 驱动 16 位数据,PHY 在每个 MDC 上升沿采样。
- 结束:MDIO 回到空闲状态。
我建议写操作完成后,紧接着读同一个寄存器做回读验证。这样能第一时间发现写入是否生效,避免后续调试时怀疑人生。
读操作与写操作的对比
| 对比项 | 读操作 | 写操作 |
|---|---|---|
| 操作码(OP) | 2'b10 | 2'b01 |
| TA 阶段驱动方 | STA 释放,PHY 驱动 | STA 全程驱动 |
| TA 阶段数据 | Z0(高阻 + 低电平) | 10(高电平 + 低电平) |
| 数据驱动方 | PHY | STA |
| 回读验证 | 天然回读 | 建议额外发起读操作 |
| 总线冲突风险 | 较高(控制权切换) | 较低(单方控制) |
总线仲裁
MDIO 总线可以挂多个 STA 设备。但问题来了——如果两个 STA 同时发起操作,谁说了算?
MDIO 协议没有像 I2C 那样复杂的仲裁逻辑,它靠的是电平冲突检测。具体机制是这样的:
- 每个 STA 在驱动 MDIO 之前,先检测总线是否空闲。
- 如果检测到 MDIO 为低,说明总线上有活动,退避等待。
- 如果两个 STA 同时开始驱动,通过监控 MDIO 实际电平来发现冲突。
- 一旦某个 STA 试图拉低 MDIO 但实际读到高电平,说明有其他 STA 在拉高,冲突发生,该 STA 立即释放总线。
我个人习惯在多 STA 共享总线的场景下,给每个 STA 分配不同的优先级,或者用软件握手来避免冲突。硬件仲裁虽然能用,但调试起来太痛苦了。
核心要点:总线仲裁的关键在于——先检测后驱动,冲突即退避。任何时候都不要假设总线是空闲的。
代码示例:MDIO 读写实现
下面是一个简化的 C 语言 MDIO 读写实现,重点展示了时序控制逻辑。实际项目中需要根据硬件平台调整 GPIO 操作和时序延迟。
// MDIO 读操作
uint16_t mdio_read(uint8_t phy_addr, uint8_t reg_addr) {
uint16_t data = 0;
int i;
// 前导码:32 个 1
for (i = 0; i < 32; i++) {
mdio_out(1);
mdc_pulse();
}
// 起始码 ST = 2'b01
mdio_out(0); mdc_pulse();
mdio_out(1); mdc_pulse();
// 操作码 OP = 2'b10(读)
mdio_out(1); mdc_pulse();
mdio_out(0); mdc_pulse();
// PHY 地址(5 位,高位在前)
for (i = 4; i >= 0; i--) {
mdio_out((phy_addr >> i) & 1);
mdc_pulse();
}
// 寄存器地址(5 位,高位在前)
for (i = 4; i >= 0; i--) {
mdio_out((reg_addr >> i) & 1);
mdc_pulse();
}
// TA 阶段:STA 释放总线,PHY 驱动
mdio_input(); // 切换为输入
mdc_pulse(); // 第一位:PHY 拉高
if (mdio_read_pin() != 1) {
return 0xFFFF; // PHY 未应答
}
mdc_pulse(); // 第二位:PHY 拉低
// 读取 16 位数据
for (i = 15; i >= 0; i--) {
mdc_pulse(); // MDC 上升沿前数据已稳定
data |= (mdio_read_pin() << i);
mdc_pulse(); // 完成一个完整的 MDC 周期
}
return data;
}
// MDIO 写操作
void mdio_write(uint8_t phy_addr, uint8_t reg_addr, uint16_t data) {
int i;
// 前导码:32 个 1
for (i = 0; i < 32; i++) {
mdio_out(1);
mdc_pulse();
}
// 起始码 ST = 2'b01
mdio_out(0); mdc_pulse();
mdio_out(1); mdc_pulse();
// 操作码 OP = 2'b01(写)
mdio_out(0); mdc_pulse();
mdio_out(1); mdc_pulse();
// PHY 地址(5 位)
for (i = 4; i >= 0; i--) {
mdio_out((phy_addr >> i) & 1);
mdc_pulse();
}
// 寄存器地址(5 位)
for (i = 4; i >= 0; i--) {
mdio_out((reg_addr >> i) & 1);
mdc_pulse();
}
// TA 阶段:STA 继续驱动,发送 2'b10
mdio_out(1); mdc_pulse();
mdio_out(0); mdc_pulse();
// 写入 16 位数据
for (i = 15; i >= 0; i--) {
mdio_out((data >> i) & 1);
mdc_pulse();
mdc_pulse();
}
}
代码里读操作和写操作的结构基本一致,区别就在 OP 码和 TA 阶段的行为。读操作多了一个总线释放和应答检查,写操作则全程由 STA 驱动。嗯,这里要注意的是,实际硬件中 mdc_pulse() 的时序一定要满足 MDC 的频率要求——标准 MDIO 最高 2.5 MHz,别跑太快了。