MDIO/MDC 总线:协议规范、操作时序、多设备访问
调试 Marvell PHY,绕不开 MDIO/MDC。这两根线就是你和 PHY 芯片之间的“对讲机”。我刚开始接触时,总觉得它很简单——两根线嘛,能有多复杂?结果有一次读寄存器死活返回 0xFFFF,查了半天才发现是时序没卡对。嗯,这期咱们就把这个“对讲机”彻底聊透。
1. 协议规范:帧结构长什么样
MDIO(Management Data Input/Output)和 MDC(Management Data Clock)一起构成管理接口。标准 IEEE 802.3 Clause 22 定义了它的帧格式。
每次通信是一帧,共 64 位。我习惯把它拆成五段:
- 前导码(Preamble):32 个连续的 "1"(0xFFFF_FFFF)。同步用。
- 起始码(Start of Frame):2 位,固定 "01"。
- 操作码(Opcode):2 位。"10" 表示读,"01" 表示写。
- 物理地址(PHY ADDR):5 位,最多 32 个 PHY。
- 寄存器地址(REG ADDR):5 位,每个 PHY 最多 32 个寄存器。
然后还有 2 个 Turnaround 位(读时由 PHY 驱动,写时由主机驱动)和 16 位数据。说白了,整个帧就是“握手+寻址+数据”。
| 字段 | 位宽 | 读操作值 | 写操作值 |
|---|---|---|---|
| Preamble | 32 | 全 1 | 全 1 |
| SoF | 2 | 01 | 01 |
| Opcode | 2 | 10 | 01 |
| PHY ADDR | 5 | 0~31 | 0~31 |
| REG ADDR | 5 | 0~31 | 0~31 |
| TA | 2 | Z0 | 10 |
| Data | 16 | PHY 输出 | 主机输入 |
2. 操作时序:怎么卡准边沿
MDC 是时钟,频率通常 2.5 MHz 或 12.5 MHz。MDIO 在 MDC 的上升沿采样,下降沿输出。我见过有人在 SPI 项目里反过来用,结果数据全部错位。
读写时序可以概括为:
- 读流程:主机发前导码 + 起始码 + 操作码 + 地址 + TA(第一个位高阻),然后释放 MDIO,PHY 在第二个 TA 位拉低后开始输出数据。
- 写流程:主机全程控制 MDIO,连续发完整帧,PHY 只接收。
关键时序参数以 2.5 MHz 为例:
| 参数 | 最小值 | 典型值 | 单位 |
|---|---|---|---|
| MDC 频率 | — | 2.5 | MHz |
| 高电平宽度 | 160 | 200 | ns |
| 低电平宽度 | 160 | 200 | ns |
| MDIO 数据建立时间 | 10 | — | ns |
| MDIO 数据保持时间 | 10 | — | ns |
为什么是 2.5 MHz?因为 MDC 周期 400 ns,主频 25 MHz 的 MCU 用软件模拟要忙 64×400 ns = 25.6 µs 读一次。如果你做实时控制,这个开销不小。我建议用硬件 MDC 模块或 DMA 方式,避免 CPU 空转。
3. 多设备访问:总线冲突怎么解
一条 MDIO 总线上最多挂 32 个 PHY。每个 PHY 有自己的 5 位地址(硬件拉高/拉低引脚决定)。常见冲突有两种:
- 地址冲突:两个 PHY 设成相同地址。数据线就会打架,读回的值是线与后的乱码。
- 电平冲突:主机的 MDIO 引脚在读操作时没切到高阻,强行拉低 PHY 的输出。
我遇到过最坑的一个问题:某款 PHY 默认地址是 0x00,而我板子上另一个设备也用了 0x00。结果写寄存器没问题(只有主机驱动),读的时候两片 PHY 同时拉 MDIO,读到的数据全是 0x0000。排查时一度怀疑时序,最后用示波器看到 MDIO 上有三态竞争才恍然大悟。
多设备访问的流程通常是轮询。主机先发一帧写入某个 PHY 的寄存器地址,再发读帧读取数据。由于 MDC 是共用的,所有 PHY 都能收到前导码和地址,但只有地址匹配的 PHY 才响应读写。你想想看,这有点像巴士——地址就是站台号,只有对号的车才开门。
// 伪代码:读取 PHY 地址 0x01 的寄存器 0x10
uint16_t phy_read(uint8_t phy_addr, uint8_t reg_addr) {
uint16_t data = 0;
// 1. 发送前导码:32个1
for (int i = 0; i < 32; i++) {
mdc_pulse(); // 每次脉冲输出 MDIO=1
}
// 2. 起始码 01
mdio_out(0); mdc_pulse();
mdio_out(1); mdc_pulse();
// 3. 操作码 10 (读)
mdio_out(1); mdc_pulse();
mdio_out(0); mdc_pulse();
// 4. PHY 地址 0x01 (00001)
for (int i = 4; i >= 0; i--) {
mdio_out((phy_addr >> i) & 0x01);
mdc_pulse();
}
// 5. 寄存器地址 0x10 (10000)
for (int i = 4; i >= 0; i--) {
mdio_out((reg_addr >> i) & 0x01);
mdc_pulse();
}
// 6. TA 位:第一个高阻,第二个读 0
mdio_high_z(); mdc_pulse(); // 主机释放
// 注意:有些 MCU 需要延时等待 PHY 拉低
if (mdio_in() != 0) {
return 0xFFFF; // 错误
}
mdc_pulse(); // 第二个 TA 位
// 7. 读取 16 位数据
for (int i = 15; i >= 0; i--) {
data |= (mdio_in() << i);
mdc_pulse();
}
return data;
}
4. 避坑指南:我踩过的坑
- 前导码不能省:有些 MCU 的 MDIO 模块会自动发前导码,但如果你用 GPIO 模拟,别偷懒不送 32 个 1。PHY 需要前导码来同步状态机。
- 读操作后别直接写:读帧的最后 16 位数据由 PHY 驱动,主机会在下降沿采样。如果你紧接着立刻写,MDIO 方向切换太快,容易把 PHY 的输出和主机的指令混在一起。我习惯在读写之间加 1 个 MDC 空周期。
- 地址不要从 0 开始:很多评估板默认 PHY 地址为 0,但你不知道板上有没有另一个设备也占用了 0。我的做法是让 PHY 至少跳到 1 或 2,并用 GPIO 读取 strap 确认。
好了,MDIO/MDC 这部分就这些。你只要把帧结构刻在脑子里,时序参数记牢,多设备访问时保持总线清洁 —— 后面所有 PHY 调试都会轻松不少。