MDIO/MDC接口概述:IEEE 802.3 Clause 22与Clause 45的区别
我们做硬件设计的,几乎天天要和MDIO/MDC打交道。但说实话,很多人对Clause 22和Clause 45的区别,也就是个模糊印象。我刚开始接触这接口时,也踩过坑,所以今天咱们好好捋一捋。
从两根线说起
MDIO(Management Data Input/Output)和MDC(Management Data Clock),说白了就是两根线。一个时钟,一个数据。用来干嘛?管理物理层(PHY)芯片。你想想看,以太网的PHY芯片内部有几十上百个寄存器,靠什么读写?就靠这两根线。
但IEEE 802.3标准里,对这个接口的定义有两个版本:
- Clause 22:1998年就定下来的老标准
- Clause 45:2000年之后才引入的新规范
这两者的差异,远比很多人想象的更大。我建议你在做新设计时,一定要先搞清楚该用哪个。
Clause 22:经典传承
Clause 22是最原始的MDIO规范。它的帧结构是这样的:
| 前导(32bit) | 起始(2bit) | 操作码(2bit) | PHY地址(5bit) | 寄存器地址(5bit) | 转向(2bit) | 数据(16bit) |
总共需要64个MDC时钟周期(32位前导 + 32位帧)。嗯,这里要注意,现在很多PHY支持“前导省略”,可以减少到32个时钟。
- PHY地址空间:5位,最多32个设备
- 寄存器地址空间:5位,每个设备最多32个寄存器
- 标准MDC频率:2.5 MHz(最大8 MHz)
- 支持设备类型:仅PHY
我在项目中遇到过,有些老设备也支持Clause 22的设备码扩展。但那不是标准做法。一般情况下,Clause 22就是简单、经典、够用——前提是你只做10/100/1000M的设计。
Clause 45:更广阔的世界
Clause 45是随着万兆以太网发展起来的。它解决了一个大问题:Clause 22的地址空间太小了。你想想看,一个万兆PHY内部可能有PMA、PCS、WIS等多个功能模块,每个模块都有自己的寄存器组。Clause 22那5位寄存器地址,完全不够用。
地址帧(数据帧不是同步的):
| 前导(32bit) | 起始(2bit) | 操作码(2bit) | 端口地址(5bit) | 设备类型(5bit) | 转向(2bit) | 寄存器地址(16bit) |
数据帧(读/写):
| 前导(32bit) | 起始(2bit) | 操作码(2bit) | 端口地址(5bit) | 设备类型(5bit) | 转向(2bit) | 数据(16bit) |
注意,Clause 45需要两个帧才能完成一次读写:先发地址帧设置寄存器地址,再发数据帧进行读写。所以操作时间比Clause 22翻了一倍。这是个重要的取舍。
| DEVAD值 | 设备类型 |
|---|---|
| 0 | PHY |
| 1 | PMA |
| 2 | PCS |
| 3 | WIS |
| 4 | DTE XS |
| 5 | TCI |
| 6 | Auto-Neg |
| 7-31 | 保留 |
快速对比表格
我把它们的核心区别整理成了一张表,方便你快速查阅:
| 属性 | Clause 22 | Clause 45 |
|---|---|---|
| 标准引入 | 1998年 | 2000年后 |
| 帧结构 | 单帧32位 | 双帧(地址+数据) |
| 寄存器地址宽度 | 5位(32个) | 16位(65536个) |
| 设备地址宽度 | 无(仅PHY) | 5位(32种设备类型) |
| 支持设备 | 仅PHY | PHY/PMA/PCS/WIS等 |
| MDC最高频率 | 8 MHz | 12 MHz(典型) |
| 兼容性 | 基本所有PHY都支持 | 万兆以上PHY必备 |
避坑指南:我的血泪教训
- 地址冲突:我曾经遇到过板上两个PHY用Clause 22通信,结果PHY地址都设成了0x1B,直接导致读写混乱。设计时一定要确保每个PHY地址唯一。
- 帧格式混淆:很多工程师写驱动时,Clause 45的“地址帧”和“数据帧”没区分开。我建议你在代码里加一个状态机,明确区分当前是地址阶段还是数据阶段。
- 时钟频率过高:有些国产PHY对MDC频率敏感。我用8MHz去读某款芯片就遇到过数据丢失。最后老老实实降到2.5MHz才稳定。
代码示例:读取PHY寄存器
这里我分别给一个Clause 22和Clause 45的读取示例。注意看帧结构的差异。
// Clause 22 读取PHY寄存器 0x10
// PHY地址 = 0x03, 寄存器地址 = 0x10
uint32_t clause22_read(uint8_t phy_addr, uint8_t reg_addr) {
uint32_t frame = 0;
// 前导 (32个1) — 很多PHY可以省略
// 起始码: 01
frame |= (0x01 << 30);
// 操作码: 10 (读)
frame |= (0x02 << 28);
// PHY地址: 5位
frame |= ((phy_addr & 0x1F) << 23);
// 寄存器地址: 5位
frame |= ((reg_addr & 0x1F) << 18);
// 转向: 2位 (Z+0)
frame |= (0x02 << 16);
// 发送32位帧...
// 接收16位数据...
}
// Clause 45 读取设备类型2 (PCS) 的寄存器0x1000
// 端口地址 = 0x03, 设备类型 = 0x02
void clause45_read(uint8_t port_addr, uint8_t dev_type) {
// 步骤1: 发送地址帧 (设置寄存器地址为0x1000)
uint32_t addr_frame = 0;
// 起始码: 00
// 操作码: 00 (地址)
addr_frame |= (0x00 << 28);
// 端口地址: 5位
addr_frame |= ((port_addr & 0x1F) << 23);
// 设备类型: 5位
addr_frame |= ((dev_type & 0x1F) << 18);
// 转向: 00
addr_frame |= (0x00 << 16);
// 寄存器地址: 16位
addr_frame |= (0x1000 & 0xFFFF);
// 发送地址帧...
// 步骤2: 发送数据帧 (读取)
uint32_t data_frame = 0;
// 起始码: 00
// 操作码: 11 (读, 帧结束后数据返回)
data_frame |= (0x03 << 28);
//... 其余字段类似
// 发送数据帧...
// 返回接收到的16位数据...
}
知识体系:Clause 22 vs Clause 45
这张图很直观:左边是Clause 22,右边是Clause 45。核心差异就两点:地址空间和设备类型支持。你在做系统设计时,根据PHY的规格书选择就行。
- 如果PHY规格书只写了“MDIO”,默认用Clause 22
- 如果规格书明确写“Clause 45”或“C45”,就用Clause 45
- 有些PHY同时支持两个标准,通过寄存器切换。我习惯在初始化时先读版本寄存器来判断