网络驱动核心: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 (对端能力) | 对端能力位 | 看对端能跑多快 |
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;
}
PHY初始化流程
U-Boot启动时,PHY初始化一般走这几步。我画了个流程图,你跟着走一遍就清楚了:
嗯,这里要注意。第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);
};
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;
}
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问题都能搞定。