网络驱动核心:PHY芯片初始化与MDIO总线调优

PHY芯片,说白了就是物理层的翻译官。它把MAC发过来的数字信号转成能在网线上跑的模拟信号,反过来也一样。很多人在U-Boot里调网络,上来就怼MAC驱动,结果死活ping不通——其实问题往往出在PHY上。

我个人习惯,拿到一块新板子,先看PHY芯片的手册。不是通读,是抓重点:芯片ID、寄存器映射、自协商流程、中断状态。这几个搞清楚了,初始化就成功了一半。

PHY芯片的核心寄存器

IEEE 802.3规定了PHY的通用寄存器,前16个是标准化的。我列个表,这几个你必须要背下来:

寄存器地址 名称 关键位 说明
0x00 BMCR (控制寄存器) Bit 15: Reset
Bit 12: Auto-Neg
Bit 13: Speed
Bit 8: Duplex
复位、自协商、速度、双工模式
0x01 BMSR (状态寄存器) Bit 5: Auto-Neg Complete
Bit 2: Link Status
自协商完成、链路状态
0x02 PHY IDR1 高16位芯片ID 用于识别PHY型号
0x03 PHY IDR2 低16位芯片ID 配合IDR1唯一确定PHY
0x04 ANAR (自协商通告) 技术能力位 告诉对端我能跑多快
0x05 ANLPAR (对端能力) 对端能力位 看对端能跑多快
我的小技巧:调试时先读BMSR的Bit 2,看Link是否起来。如果Link都没亮,后面全是白费功夫。我曾经在一块板子上折腾了两天,最后发现是网口变压器虚焊。

MDIO总线:PHY的通信命脉

MDIO总线只有两根线:MDC(时钟)和MDIO(数据)。协议很简单,但坑不少。U-Boot里MDIO的时序通常由GPIO模拟或MAC内置控制器实现。

先看一个典型的MDIO读操作时序:

/* MDIO读操作帧格式 */
/* 前导码 + 起始码 + 操作码 + PHY地址 + 寄存器地址 + 转向 + 数据 */
/* 32个1 + 01 + 10 + AAAAA + RRRRR + Z0 + 16位数据 */

/* U-Boot中MDIO读的典型实现 */
static int mdio_read(struct mii_dev *bus, int addr, int devad, int reg)
{
    unsigned int val;
    
    /* 发送前导码:32个连续的1 */
    mdio_preamble();
    
    /* 发送起始码 + 读操作码 */
    mdio_send_bits(0x06, 4);  /* 01 + 10 = 0x6 */
    
    /* 发送PHY地址(5位)和寄存器地址(5位) */
    mdio_send_bits(addr, 5);
    mdio_send_bits(reg, 5);
    
    /* 转向周期:MAC释放总线,PHY驱动 */
    mdio_turnaround();
    
    /* 读取16位数据 */
    val = mdio_recv_bits(16);
    
    return val;
}
注意:MDIO的时序对延迟非常敏感。如果你用GPIO模拟,一定要确保MDC的高低电平保持时间足够。我见过有人把udelay(1)写成udelay(10),结果PHY直接不响应。建议用示波器抓一下,MDC频率不要超过2.5MHz。

PHY初始化流程

U-Boot启动时,PHY初始化一般走这几步。我画了个流程图,你跟着走一遍就清楚了:

PHY初始化流程图 1. 上电/硬件复位 2. 读取PHY IDR1/IDR2 3. 匹配PHY驱动 4. BMCR写复位(bit15=1) 5. 配置ANAR/BMCR 6. 等待自协商完成 7. 读取BMSR/ANLPAR 关键检查点 • ID匹配失败:PHY未上电 • 复位超时:MDIO时序错 • 自协商失败:对端不匹配 超时处理 建议超时设为3秒 超时后强制设置100M全双工

嗯,这里要注意。第4步的软件复位,写完BMCR的bit15后,要轮询等待bit15自动清零。我见过有人忘了这个等待,直接往下配置,结果PHY还在复位状态,写啥都不生效。

U-Boot中的PHY驱动框架

U-Boot的PHY驱动采用分层设计。顶层是通用接口,底层是具体芯片的实现。核心结构体就两个:

/* phy_device结构体:描述一个PHY设备 */
struct phy_device {
    unsigned int addr;          /* MDIO地址,0-31 */
    unsigned int phy_id;        /* 芯片ID,由IDR1+IDR2组成 */
    unsigned int supported;     /* 支持的能力位掩码 */
    unsigned int advertising;   /* 通告的能力位掩码 */
    unsigned int speed;         /* 协商后的速度 */
    unsigned int duplex;        /* 协商后的双工模式 */
    int link;                   /* 链路状态 */
    struct phy_driver *drv;     /* 指向具体驱动 */
};

/* phy_driver结构体:描述一个PHY驱动 */
struct phy_driver {
    unsigned int uid;           /* 芯片ID掩码匹配值 */
    unsigned int mask;          /* 掩码,用于模糊匹配 */
    int (*probe)(struct phy_device *phy);
    int (*config)(struct phy_device *phy);
    int (*startup)(struct phy_device *phy);
    int (*shutdown)(struct phy_device *phy);
};
避坑指南:我曾经遇到一个情况,PHY ID读出来是0xffffffff。查了半天,发现是MDIO的上拉电阻没焊。PHY地址引脚的电平不对,导致地址匹配不上。所以,如果ID读不出来,先量一下PHY地址引脚的电压。

MDIO总线调优实战

MDIO总线的调优,核心就三个参数:时钟频率、时序延迟、重试策略。我直接给出一套经过验证的配置:

参数 推荐值 说明
MDC频率 1.25 MHz - 2.5 MHz 标准上限2.5MHz,降频可提高稳定性
MDC高电平保持 ≥ 200 ns GPIO模拟时用udelay(1)即可
MDC低电平保持 ≥ 200 ns 对称占空比,避免时序偏移
读操作重试次数 3次 第一次失败后等待100us再试
写操作重试次数 2次 写操作一般不会失败
自协商超时 3秒 超时后强制设置固定模式

为什么重试次数要这么设?因为MDIO总线在噪声环境下,偶尔会出bit错误。读操作更容易受影响,所以多试一次。我建议在重试逻辑里加个打印,方便调试:

/* 带重试的MDIO读操作 */
int mdio_read_retry(struct mii_dev *bus, int addr, int reg)
{
    int ret, retry;
    
    for (retry = 0; retry < 3; retry++) {
        ret = bus->read(bus, addr, MDIO_DEVAD_NONE, reg);
        if (ret >= 0)
            return ret;
        
        if (retry == 0) {
            printf("MDIO read failed (addr=%d, reg=0x%02x), retrying...\n",
                   addr, reg);
            udelay(100);  /* 等待100us再试 */
        }
    }
    
    printf("MDIO read failed after 3 retries (addr=%d, reg=0x%02x)\n",
           addr, reg);
    return -ETIMEDOUT;
}
警告:不要在中断上下文里调用MDIO操作。MDIO的时序要求精确延迟,中断会打乱节奏。U-Boot虽然是单线程,但如果你在定时器回调里调MDIO,一样会出问题。我踩过这个坑,那次调试花了整整一个下午。

PHY芯片的电源管理

很多PHY芯片支持省电模式。U-Boot里一般不需要省电,但要注意:有些PHY上电后默认进入休眠状态,需要先唤醒。唤醒方式通常是写一个特定的寄存器序列。

以常见的RTL8211为例:

/* RTL8211唤醒序列 */
static int rtl8211_wakeup(struct phy_device *phy)
{
    /* 读中断状态寄存器,清除pending中断 */
    phy_read(phy, MDIO_DEVAD_NONE, 0x1a);
    
    /* 写PHYCR寄存器,退出省电模式 */
    phy_write(phy, MDIO_DEVAD_NONE, 0x1f, 0x0001);
    udelay(1000);
    
    /* 写BMCR,触发软复位 */
    phy_write(phy, MDIO_DEVAD_NONE, MII_BMCR, BMCR_RESET);
    udelay(1000);
    
    return 0;
}

嗯,这里有个细节。唤醒后一定要等PHY稳定,我一般等1ms。有些PHY需要更长时间,具体看手册。如果你发现PHY唤醒后读ID还是0,多半是等待时间不够。

调试技巧汇总

最后,我把自己多年积累的PHY调试技巧列出来,你直接拿去用:

  • 先看Link灯:硬件问题先排除。灯不亮,查网口、变压器、PHY供电。
  • 读ID确认通信:能读到ID,说明MDIO总线通了。读不到,查MDIO上拉、PHY地址。
  • 抓BMSR看状态:Link状态、自协商完成位,这两个位能告诉你PHY当前在干嘛。
  • 强制模式做对比:自协商搞不定时,强制设置100M全双工。如果强制模式能通,问题在自协商。
  • 加打印看流程:每个关键步骤加printf,看执行到哪一步卡住了。别怕打印多,调试完再删。

核心总结:PHY初始化就三件事——确认通信、配置模式、等待协商。MDIO调优就三个参数——频率、延迟、重试。把这六个字记住,90%的PHY问题都能搞定。