2. MDIO 管理接口:时钟、帧格式与读写时序
MDIO 管理接口,很多人也叫它 SMI 或者 MIIM,说白了就是 MAC 侧用来读写 PHY 内部寄存器的通道。你想想看,PHY 的链路状态、协商结果、中断标志,全都得靠这条串行总线才能拿到或修改。我个人觉得,搞明白 MDIO 的时序和帧格式,是写底层驱动、排 PHY 疑难杂症的基本功。
2.1 协议基础与时钟规范
MDIO 采用两条线:MDC(时钟)和 MDIO(数据)。总线是一个共享结构,允许多个 PHY 挂在同一条总线上,通过 PHY 地址来区分器件。嗯,这里要注意:MDC 必须由 MAC 持续产生,PHY 只有时钟同步功能。
| 参数 | 规范 |
|---|---|
| 最大 MDC 频率 | 2.5 MHz(标准模式),某些 PHY 支持 10 MHz(快速模式) |
| MDC 高/低电平持续时间 | ≥ 200 ns(2.5 MHz 时) |
| MDIO 数据变化沿 | 在 MDC 下降沿改变,在上升沿被采样 |
| 总线空闲状态 | MDIO 为高阻,MDC 保持低 |
我踩过的坑: 刚开始做项目时,我把 MDC 频率设到了 10 MHz,结果调试了好几天发现某些 PHY 读出来的寄存器值总是不对。后来查手册才发现那款 PHY 不支持快速模式。记住:低频率更可靠,2.5 MHz 兼容性最好。
2.2 帧格式深度拆解
MDIO 传输的基本单位是一帧 64 个 MDC 周期(读操作)或 64 个周期(写操作)。帧结构是固定的,我习惯把它拆成五个阶段来理解:
- 前导码(Preamble):32 个连续 1,用来同步 PHY 的接收逻辑。
- 起始码(Start of Frame):固定 2 位
01,帧开始的标志。 - 操作码(Op Code):读操作为
10,写操作为01。 - 地址段:5 位 PHY 地址 + 5 位寄存器地址。
- 数据段:16 位数据 + 2 位翻转周期(Turnaround)。读操作时需要 PHY 在 TA 阶段释放总线、切换方向。
具体每个阶段的位域分配,我整理成了一张表,方便你查:
| 阶段 | 位数 | 读帧内容 | 写帧内容 |
|---|---|---|---|
| 前导码 | 32 | 111...111(32个1) |
111...111(32个1) |
| 起始码 | 2 | 01 |
01 |
| 操作码 | 2 | 10 |
01 |
| PHY 地址 | 5 | PHY_ADDR[4:0] | PHY_ADDR[4:0] |
| 寄存器地址 | 5 | REG_ADDR[4:0] | REG_ADDR[4:0] |
| 翻转 (TA) | 2 | 由 PHY 驱动:先 Z,再 0 | 由 MAC 驱动:先 1,再 0 |
| 数据 | 16 | PHY 输出 DATA[15:0] | MAC 输出 DATA[15:0] |
注意: 标准规范要求前导码至少 32 位。有些 PHY 允许跳过前导码(省略模式),但我建议你不要省略,兼容性会差很多。我遇到过一台交换机,省略前导码后其中一个 PHY 始终不响应,折腾了半天。
2.3 读写操作时序
读写时序的核心差异在于 TA 阶段的方向控制。下面我用一段 C 代码演示最普通的软件模拟 MDIO 读写(以 STM32 的 GPIO 为例),你可以在没有硬件 MDIO 的 MCU 上直接跑。
// 模拟 MDC 脉冲
void MDC_pulse(void) {
MDC = 1;
delay_ns(200); // 等待上升沿采样
MDC = 0;
delay_ns(200);
}
// 读寄存器(32 位前导码版本)
uint16_t MDIO_read(uint8_t phy_addr, uint8_t reg_addr) {
uint16_t data = 0;
int i;
// 1. 前导码:32 个 1
for (i = 0; i < 32; i++) {
MDIO_out(1);
MDC_pulse();
}
// 2. 起始码 01
MDIO_out(0); MDC_pulse();
MDIO_out(1); MDC_pulse();
// 3. 操作码:读 10
MDIO_out(1); MDC_pulse(); // SMI:读操作码的 MSB 先发
MDIO_out(0); MDC_pulse();
// 4. PHY 地址 (5 bit)
for (i = 4; i >= 0; i--) {
MDIO_out((phy_addr >> i) & 0x01);
MDC_pulse();
}
// 5. 寄存器地址 (5 bit)
for (i = 4; i >= 0; i--) {
MDIO_out((reg_addr >> i) & 0x01);
MDC_pulse();
}
// 6. TA:MAC 释放总线,第1个周期高阻,第2个周期 PHY 将数据线拉为0
MDIO_set_input(); // 释放 MDIO 引脚
MDC_pulse(); // 采样到高阻(忽略)
MDC_pulse(); // 采样到 0(PHY 准备完成)
// 7. 读取 16 位数据(MSB 在先)
for (i = 15; i >= 0; i--) {
MDC_pulse(); // 上升沿采样
data |= (MDIO_read_pin() << i);
}
// 8. 最后再给一个 MDC 时钟让 PHY 恢复空闲
MDC_pulse();
return data;
}
我曾经就在这里栽过: 读取数据时,很多工程师容易忘记 MDIO 的方向切换是在 TA 阶段完成的。如果 TA 的第1个周期你还没释放引脚,PHY 会始终处于高阻,你只能读到 0xFFFF 或者乱码。所以一定要确保
MDIO_set_input() 的执行时机精确无误。
写操作的时序类似,但 TA 阶段由 MAC 主动驱动 10,随后连续输出 16 位数据。标准帧结束后还有一个额外时钟周期用于恢复总线。如果你用硬件 MDIO 控制器(比如 RTL8211 系列),这些时序逻辑会自动完成,不过我还是建议你先用 GPIO 模拟一版,方便调试。
2.4 本章核心流程总图
2.5 调试经验总结
- 时钟极性: 规范要求数据在上升沿采样,但有些 FPGA 的 MDIO 控制器实现用了下降沿。挂示波器确认一下,别上来就怀疑 PHY 坏了。
- 高阻处理: TA 阶段如果 MDIO 线没有被外部上拉或下拉,悬空状态可能导致读数据全是 0xFFFF。我建议在设计板上给 MDIO 加一个 10kΩ 上拉电阻。
- 地址冲突: 如果总线上挂了两颗相同地址的 PHY,读操作会冲突。商用交换机一般通过硬件 strapping 引脚保证地址唯一。
- 默认值: 多数 PHY 上电后寄存器 0(BMCR)默认 0x3100(复位=1,loopback=0)。我第一次调试时读出来不对,还以为通信有问题,后来才发现是 PHY 还在复位状态。
我自己的习惯: 开始调试一个新板子时,先写一个简单的读 ID 寄存器(寄存器 2 和 3)的程序。如果能读出 PHY ID(如 RTL8211 的 ID 是 0x001CC916),说明 MDIO 时序和地址都对了。如果读到 0x0000 或 0xFFFF,大概率是时序有问题,优先检查 MDC 频率和 TA 切换时机。