以太网 PHY 芯片 MDIO 管理接口深入分析:寄存器读写、自协商状态机与 Link 检测完整实现
一、PHY 不是透明管道:自协商失败与 Link Flapping 的底层根因
在嵌入式 Linux BSP 开发中,以太网 PHY 芯片(如 RTL8211F、KSZ9031、DP83848)往往被视为 MAC 控制器和 RJ45 之间的"透明管道"。直到现场部署出现 Link 反复 Up/Down、对端交换机协商到 10Mbps 半双工、或特定网线长度下无法建链——开发才发现所有问题都指向 PHY 寄存器的配置与状态转换。
PHY 芯片通过 MDIO(Management Data Input/Output)串行总线与 MAC 控制器通信。IEEE 802.3 标准第 22 条定义了 16 个基本寄存器的地址空间,第 45 条扩展了更多寄存器。MCU/SOC 通过 MDC(时钟)和 MDIO(数据)两根引脚,以类似 I2C 的串行协议读写 PHY 内部寄存器,完成速率配置、自协商控制、Link 状态检测和错误统计读取等关键操作。
本文深入解析 MDIO 的寄存器操作时序、自协商状态机的全状态遍历,并给出在裸机 RTOS 环境下的完整驱动实现。
二、MDIO 帧格式与 PHY 自协商状态机
MDIO 协议定义了两种帧格式:Clause 22(基本,5bit PHY 地址 + 5bit 寄存器地址)和 Clause 45(扩展,支持更多寄存器和间接寻址)。最基本的 Clause 22 读/写帧格式如下:
读操作: PRE(32×1) | ST(01) | OP(10) | PHYAD(5bit) | REGAD(5bit) | TA(ZZ) | DATA(16bit) 写操作: PRE(32×1) | ST(01) | OP(01) | PHYAD(5bit) | REGAD(5bit) | TA(10) | DATA(16bit)自协商(Auto-Negotiation)是 PHY 最重要的完整状态机。IEEE 802.3 定义的 AN 状态机包含以下关键状态:
自协商的关键寄存器:
| 寄存器 | 地址 | 描述 |
|---|---|---|
| BMCR (Basic Mode Control) | 0x00 | Bit[12]=Auto-Neg Enable, Bit[9]=Restart AN |
| BMSR (Basic Mode Status) | 0x01 | Bit[2]=Link Status, Bit[5]=AN Complete |
| PHYID1/PHYID2 | 0x02/0x03 | PHY 芯片 OUI 标识 |
| ANAR (Advertisement) | 0x04 | 本端能力通告 |
| ANLPAR (Link Partner Ability) | 0x05 | 对端能力寄存器 |
自协商故障的核心诊断路径:读取 ANAR(0x04) → 确认本端通告能力完整 → 读取 ANLPAR(0x05) → 确认对端能力匹配 > 读取 BMSR[5] → 确认 AN 完成标志 > 读取 BMSR[2] → 确认 Link 状态。
三、MDIO 驱动的裸机实现与自协商状态监控
以下代码实现 MDIO 总线的底层 bit-bang 操作和 PHY 寄存器读写。适用于 STM32 或任何有 GPIO 控制能力的 MCU。
/* * mdio_driver.c — MDIO 总线 bit-bang 驱动 + PHY 自协商控制 * * 硬件连接: * MCU GPIO_A → MDC (时钟, 最大 2.5MHz) * MCU GPIO_B → MDIO (数据, 开漏, 需上拉电阻 1.5KΩ) * * 时序遵从 IEEE 802.3 Clause 22 * * 设计要点: * 1. MDIO 为开漏信号,读操作时需将 GPIO 设为输入模式 (Hi-Z) * 2. MDC 时钟频率上限 2.5MHz,本实现使用 1MHz 确保兼容性 * 3. 读取操作中 TA 阶段需在第一个时钟周期释放总线(Hi-Z), * PHY 在第二个时钟周期驱动数据线 */ /* * 平台相关的 GPIO 操作 —— 用户需根据实际平台实现 * 以下为 STM32 HAL 风格的伪接口 */ #define MDC_PIN GPIO_PIN_0 #define MDIO_PIN GPIO_PIN_1 #define MDIO_PORT GPIOA static void mdc_high(void) { /* GPIO_SetBits(MDIO_PORT, MDC_PIN) */ } static void mdc_low(void) { /* GPIO_ResetBits(MDIO_PORT, MDC_PIN) */ } static void mdio_high(void) { /* 开漏输出高 (释放总线) */ } static void mdio_low(void) { /* 开漏输出低 */ } static int mdio_read(void) { /* 返回 GPIO 输入电平 (0 或 1) */ } static void mdio_out(void) { /* 设置 GPIO 为开漏输出模式 */ } static void mdio_in(void) { /* 设置 GPIO 为输入 (浮空) 模式 */ } static void delay_us(int us) { /* 微秒级延时 */ } #define MDC_DELAY_US 1 /* 1MHz 时钟: 高低各 1us = 周期 2us */ /* MDIO Clause 22 帧结构常量 */ #define MDIO_PREAMBLE_LEN 32 /* 前导码: 32 个连续 1 */ #define MDIO_START_BITS 0x01 /* ST = 01 */ #define MDIO_OP_READ 0x02 /* 读操作: OP = 10 */ #define MDIO_OP_WRITE 0x01 /* 写操作: OP = 01 */ #define MDIO_TA_READ 0x02 /* TA 读: ZZ → PHY 驱动 → 0 */ /* * 发送指定数量的 bit * 数据从 MSB 开始逐位发送 */ static void mdio_send_bits(uint32_t data, int num_bits) { mdio_out(); /* 确保 GPIO 为输出模式 */ for (int i = num_bits - 1; i >= 0; i--) { mdc_low(); if (data & (1u << i)) { mdio_high(); } else { mdio_low(); } delay_us(MDC_DELAY_US); mdc_high(); delay_us(MDC_DELAY_US); } } /* * 发送前导码: 32 个连续的 1 * 用于同步 PHY 的 MDIO 状态机 */ static void mdio_send_preamble(void) { mdio_out(); for (int i = 0; i < MDIO_PREAMBLE_LEN; i++) { mdc_low(); mdio_high(); /* 数据 = 1 */ delay_us(MDC_DELAY_US); mdc_high(); delay_us(MDC_DELAY_US); } } /* * 读取 16 bit 数据 (MSB first) * * 读操作中,master 在 MDC 上升沿采样数据 * PHY 在 MDC 下降沿后驱动数据 */ static uint16_t mdio_recv_bits(int num_bits) { uint16_t data = 0; mdio_in(); /* GPIO 切换为输入 (Hi-Z) */ delay_us(MDC_DELAY_US); for (int i = 0; i < num_bits; i++) { mdc_low(); delay_us(MDC_DELAY_US); mdc_high(); delay_us(MDC_DELAY_US / 2); /* 等待数据稳定 */ if (mdio_read()) { data |= (1u << (num_bits - 1 - i)); } delay_us(MDC_DELAY_US / 2); } return data; } /* * Clause 22 MDIO 读操作 * * 帧格式: PRE(32) | ST(01) | OP(10) | PHYAD(5) | REGAD(5) | TA(ZZ) | DATA(16) * * 注意 TA (Turnaround) 阶段: * - 第 1 个 bit: master 释放总线 (Hi-Z), PHY 尚未驱动,总线为高(上拉电阻) * - 第 2 个 bit: PHY 驱动总线为 0,表示正常响应 * * 返回值: 16-bit 寄存器值,读取失败返回 0xFFFF */ uint16_t mdio_read_register(uint8_t phy_addr, uint8_t reg_addr) { /* 前导码 */ mdio_send_preamble(); /* ST (01) + OP (10) + PHYAD(5) + REGAD(5) = 14 bits */ uint32_t frame = ((uint32_t)MDIO_START_BITS << 12) | ((uint32_t)MDIO_OP_READ << 10) | ((uint32_t)(phy_addr & 0x1F) << 5) | ((uint32_t)(reg_addr & 0x1F)); mdio_send_bits(frame, 14); /* TA (Turnaround) 阶段 */ mdio_in(); /* master 释放总线 */ mdc_low(); delay_us(MDC_DELAY_US); mdc_high(); delay_us(MDC_DELAY_US); /* 接收 16-bit 数据 */ uint16_t data = mdio_recv_bits(16); /* 总线恢复为输出 (驱动为高,释放) */ mdio_out(); mdio_high(); return data; } /* * Clause 22 MDIO 写操作 * * 帧格式: PRE(32) | ST(01) | OP(01) | PHYAD(5) | REGAD(5) | TA(10) | DATA(16) */ void mdio_write_register(uint8_t phy_addr, uint8_t reg_addr, uint16_t data) { /* 前导码 */ mdio_send_preamble(); /* ST (01) + OP (01) + PHYAD(5) + REGAD(5) = 14 bits */ uint32_t frame = ((uint32_t)MDIO_START_BITS << 12) | ((uint32_t)MDIO_OP_WRITE << 10) | ((uint32_t)(phy_addr & 0x1F) << 5) | ((uint32_t)(reg_addr & 0x1F)); mdio_send_bits(frame, 14); /* TA (Turnaround) 阶段: master 发送 10 */ mdio_send_bits(0x02, 2); /* 发送 16-bit 数据 */ mdio_send_bits(data, 16); /* 总线恢复空闲 (高电平) */ mdio_out(); mdio_high(); } /* ================================================================ * PHY 寄存器定义 (IEEE 802.3 Clause 22 标准寄存器) * ================================================================ */ #define PHY_REG_BMCR 0x00 /* 基本模式控制 */ #define PHY_REG_BMSR 0x01 /* 基本模式状态 */ #define PHY_REG_PHYID1 0x02 /* PHY ID 高16位 */ #define PHY_REG_PHYID2 0x03 /* PHY ID 低16位 */ #define PHY_REG_ANAR 0x04 /* 自协商通告 */ #define PHY_REG_ANLPAR 0x05 /* 对端能力 */ #define PHY_REG_ANER 0x06 /* 自协商扩展 */ /* BMCR 位定义 */ #define BMCR_RESET (1 << 15) /* 软件复位 */ #define BMCR_ANE_ENABLE (1 << 12) /* 自协商使能 */ #define BMCR_RESTART_AN (1 << 9) /* 重启自协商 */ #define BMCR_DUPLEX_MODE (1 << 8) /* 全双工 */ #define BMCR_SPEED_100 (1 << 13) /* 100Mbps */ /* BMSR 位定义 */ #define BMSR_AUTO_NEG_COMPLETE (1 << 5) /* 自协商完成 */ #define BMSR_LINK_STATUS (1 << 2) /* Link 状态 */ /* * PHY 初始化与自协商控制 * * 返回值: 0 = 成功, -1 = PHY 无响应, -2 = 自协商超时 */ int phy_init_and_autoneg(uint8_t phy_addr) { /* 第一步: 读取 PHY ID,验证 MDIO 通信 */ uint16_t id1 = mdio_read_register(phy_addr, PHY_REG_PHYID1); uint16_t id2 = mdio_read_register(phy_addr, PHY_REG_PHYID2); if (id1 == 0xFFFF || id2 == 0xFFFF) { printf("[ERROR] PHY 无响应 (addr=%d, ID1=0x%04X, ID2=0x%04X)\n", phy_addr, id1, id2); return -1; } printf("[INFO] PHY 检测成功: OUI=0x%04X%04X\n", id1, id2); /* 第二步: 软件复位 PHY */ mdio_write_register(phy_addr, PHY_REG_BMCR, BMCR_RESET); /* 等待复位完成 (轮询 BMCR[15] 直到清零,最多 500ms) */ int retry = 500; while (retry-- > 0) { uint16_t bmcr = mdio_read_register(phy_addr, PHY_REG_BMCR); if (!(bmcr & BMCR_RESET)) { break; } delay_us(1000); /* 1ms */ } if (retry <= 0) { printf("[ERROR] PHY 复位超时\n"); return -2; } /* * 第三步: 配置自协商能力通告 * 通告: 100Base-TX Full/Half, 10Base-T Full/Half * ANAR[8:5] = 100Base-TX FD/HD, 10Base-T FD/HD */ uint16_t anar = (1 << 8) /* 100Base-TX Full Duplex */ | (1 << 7) /* 100Base-TX Half Duplex */ | (1 << 6) /* 10Base-T Full Duplex */ | (1 << 5) /* 10Base-T Half Duplex */ | (1 << 0); /* 802.3 选择器字段 */ mdio_write_register(phy_addr, PHY_REG_ANAR, anar); /* 第四步: 使能并重启自协商 */ mdio_write_register(phy_addr, PHY_REG_BMCR, BMCR_ANE_ENABLE | BMCR_RESTART_AN); printf("[INFO] 自协商已启动 (ANAR=0x%04X)\n", anar); /* 第五步: 等待自协商完成 (最多 5 秒) */ retry = 5000; while (retry-- > 0) { uint16_t bmsr = mdio_read_register(phy_addr, PHY_REG_BMSR); if (bmsr & BMSR_AUTO_NEG_COMPLETE) { /* 读取协商结果 */ uint16_t anlpar = mdio_read_register(phy_addr, PHY_REG_ANLPAR); printf("[INFO] 自协商完成! 对端能力: 0x%04X\n", anlpar); printf("[INFO] Link 状态: %s\n", (bmsr & BMSR_LINK_STATUS) ? "UP" : "DOWN"); /* 解析协商的最高速率 */ if (anlpar & (1 << 8)) { printf("[INFO] 协商速率: 100Mbps 全双工\n"); } else if (anlpar & (1 << 7)) { printf("[INFO] 协商速率: 100Mbps 半双工\n"); } else if (anlpar & (1 << 6)) { printf("[INFO] 协商速率: 10Mbps 全双工\n"); } else if (anlpar & (1 << 5)) { printf("[INFO] 协商速率: 10Mbps 半双工\n"); } return 0; } delay_us(1000); /* 1ms */ } printf("[ERROR] 自协商超时 (5 秒)\n"); return -2; } /* * Link 状态轮询任务 —— 建议在 RTOS 中以 100ms 周期调用 * 检测 Link 变化并上报事件 */ int phy_link_poll(uint8_t phy_addr, int *prev_link_up) { uint16_t bmsr = mdio_read_register(phy_addr, PHY_REG_BMSR); if (bmsr == 0xFFFF) { /* PHY 无响应 —— 可能处于复位状态 */ return -1; } int link_up = (bmsr & BMSR_LINK_STATUS) ? 1 : 0; if (link_up != *prev_link_up) { printf("[EVENT] Link %s → %s\n", *prev_link_up ? "UP" : "DOWN", link_up ? "UP" : "DOWN"); *prev_link_up = link_up; /* Link UP 后重新确认协商速率 */ if (link_up) { uint16_t anlpar = mdio_read_register(phy_addr, PHY_REG_ANLPAR); printf("[INFO] 协商速率能力: 0x%04X\n", anlpar); } } return link_up; }四、MDIO 的通信可靠性与硬件层面的边界条件
MDIO 总线的电气特性:MDIO 采用开漏输出 + 上拉电阻(典型值 1.5KΩ to 3.3V)的电气规范。最大容性负载为 470pF,超过此值会导致上升沿缓慢、时序违规。在一主多从的多 PHY 拓扑中,每个 PHY 的引脚电容叠加,需控制在 32 个器件以内。布线长度超过 20cm 时,建议在 MDC 上串联 22Ω 终端电阻。
PHY 地址冲突:MDIO 协议支持 5 位 PHY 地址(0~31)。多数 PHY 芯片通过硬件 strap pin 配置地址(如 RTL8211F 的 PHYAD[4:0] pins)。当 PCB 上存在多个 PHY 时,需确保地址不冲突。地址 0 有特殊含义(广播地址),部分 PHY 会将地址 0 的写入转发到所有 PHY——这是调试中容易忽略的陷阱。
寄存器写入的读回验证:IEEE 802.3 要求 MDIO 写入操作不提供 ACK 机制。为保证写入确实生效,必须对关键寄存器(如 BMCR 的 Auto-Neg 使能位)进行读回验证。示例代码中未包含此步骤,但在生产代码中对于 BMCR、ANAR 等控制寄存器的写入后必须读回确认。
禁用场景:
| 场景 | 原因 |
|---|---|
| 使用 RMII 内建 MDIO 控制器的平台 | 使用硬件 MDIO 控制器比 bit-bang 更可靠 |
| 需要访问 Clause 45 扩展寄存器 | bit-bang 实现需扩展为 Clause 45 帧 |
| PHY 间距 > 30cm 的多板互联 | 信号完整性不可保证 |
| 需要原子性的寄存器批量更新 | MDIO 不支持事务,需 read-modify-write |
五、总结
MDIO 是连接 MAC 和 PHY 的唯一管理通道,对其深入理解直接影响以太网链路的稳定性和可调试性:
寄存器是诊断的入口:Link flapping 的根因 90% 可通过 BMSR、ANLPAR 和 PHY 厂商的扩展状态寄存器定位。建议在 BSP 中内置寄存器 dump 命令。
自协商状态机:完整遍历 AN_ENABLE → ABILITY_DETECT → ACKNOWLEDGE_DETECT → COMPLETE_ACK → LINK_OK。任何阶段的超时或失败都能从 BMSR 中读取。
bit-bang 的可靠性:MDIO 的 bit-bang 实现需严格遵循 IEEE 802.3 的 32-bit 前导码和 TA(Turnaround)时序。1MHz MDC 频率是兼容性的安全选择。
寄存器读回验证:MDIO 写入操作无 ACK,关键控制寄存器写入后必须读回验证。这是生产代码和调试代码之间的关键质量差异。