接手一块新板子,最怕的不是内核 panic,而是那种"看起来哪儿都对,但就是不通"的 Linux 网络问题。上周帮朋友调一块 AM335x 平台的板子,MAC 通过 RMII 接口外接了一颗 PHY 芯片,硬件连线确认过、参考手册里的寄存器配置也核对过,可内核起来之后 eth0 就是报找不到 PHY。dmesg 里没有"link down",ioctl 查接口状态显示 no carrier,ethtool 也没有任何速度信息。查设备树、查 MAC 驱动配置,折腾了半天,最后硬件同事一句话点醒我:这颗 PHY 的管理接口没有走标准 MDIO,而是被接到了 I2C 总线上。很多工程师一听到"Linux 网络"首先想到的是常用命令、防火墙、抓包这些偏应用层的东西,但真正把产品搞死的,往往是这种 PHY 管理通道层面的底层 bringup 问题。
这篇文章我想把这次排查过程完整梳理一遍:在不使用 MDIO、只靠 I2C 管理 PHY 的前提下,如何让内核的 phylib 正确识别并驱动这颗 PHY。不光给结论,还会讲清楚背后的原理、mii_bus 适配层的写法,再配上我实际调试中踩过的坑。最后,同一个思路还可以迁移到 DSA 交换机驱动上。内容偏底层,适合正在做嵌入式 Linux BSP、网络设备驱动,或者被"no PHY found"这类问题折磨过的嵌入式工程师参考。
1. 一块不走 MDIO 的 PHY:问题现场和硬件背景
1.1 被忽略的"管理通道"才是问题根源
先说现场现象。板子上电后,U-Boot 阶段网络是通的,能通过 TFTP 下载内核;但内核起来以后,网络就是不通的。这个对比特别有迷惑性,因为 U-Boot 里通常不做完整的 PHY 驱动管理,很多平台在 U-Boot 阶段是靠 bootloader 初始化 PHY 时直接写寄存器,或者干脆硬件 strap 引脚把 PHY 配置好,让它上电自协商出一个固定速率。所以 U-Boot 能 ping 通,不代表内核也能顺利接管 PHY。
进内核后,我首先怀疑设备树 Ethernet 节点配置不对。翻了一圈,发现 mdio 子节点下的 PHY 节点地址、phy-mode、reset-gpios 都没问题。再怀疑 MAC 驱动,但平台是常见型号,驱动按理说非常成熟。直到硬件同事指出"这颗片子的 PHY 管理接口用的是 I2C 接口",我才意识到从一开始就用错了排查方向。硬件上 MAC 和 PHY 之间的数据通路是 RMII,这没问题,但管理通路不是标准的 MDC/MDIO 两根线,而是两根 I2C 线。内核如果按常规思路去 MDIO 总线上扫描 PHY,自然什么都扫不到。
1.2 为什么硬件设计师会把 PHY 管理接口放到 I2C 上
很多做应用层的朋友可能不理解,PHY 管理接口不是固定的吗?其实不是。PHY 芯片通常提供一个可选的管理接口,标准是 MDIO,但不少型号支持配置成通过 I2C 寄存器访问。硬件上出现这种接法,通常有这几个原因:
- 主控 SoC 的 MDIO/SMI 控制器引脚已经被其他器件占用,比如同一个 MAC 要接多颗 PHY 或交换机芯片,引脚不够分,I2C 总线反而更容易扩展出多个从设备地址。
- 板级引脚复用紧张。MDIO 是专用的两根线,如果它们占用的引脚被别的外设复用了,硬件只能把 PHY 挂到现成的 I2C 总线上,节省引脚。
- 某些 PHY 芯片本身支持"管理数据通过 I2C/SPI 输入"的工作模式,这在多端口交换芯片、网关、串口转网口模块上非常常见。
不过这种接法在硬件上省事了,软件上的第一道坎就来了:内核的 phylib 默认是透过 MDIO 总线去扫描 PHY 的,如果管理通道是 I2C,就必须做一层适配,把 I2C 读写包装成内核认识的 mii_bus 读写接口。这层包装做得好,phylib 就能像操作普通 MDIO PHY 一样操作它。
1.3 MDIO 与 I2C 管理通道的差异对照
先看一下两种管理通道的本质差异,这样后面讲适配层的时候会更好理解。
| 对比维度 | MDIO/MDC 管理 | I2C 管理 |
|---|---|---|
| 物理线数 | 2 根(MDC + MDIO) | 2 根(SCL + SDA) |
| 通信方式 | 时钟同步、双向数据线,MAC 侧主动发起 | 标准 I2C 协议,设备地址 + 寄存器偏移 |
| 地址模型 | 5 位 PHY 地址(0~31),寄存器 0~31 | 7 位器件地址 + 内部寄存器偏移 |
| 寄存器宽度 | 标准 MII 寄存器是 16 位 | 由 PHY 的实现决定,常见 8 位或 16 位 |
| 访问速度 | 可到数 MHz 甚至更高 | 标准模式 100k、快速 400k,部分支持 1MHz |
| 内核抽象 | mdiobus 的 read/write 回调 | 需要适配为 mdiobus 的 read/write 回调 |
核心观点:不管底层是 MDIO 还是 I2C,对上层的 phylib 来说,本质需求只有一个——能读写 PHY 的寄存器。所以解决方向就很清晰:把 I2C 访问包装成 mii_bus 上的一次寄存器读写。
2. phylib 认的是寄存器而不是总线类型
2.1 理解 phylib 的运行机理
刚开始接触内核网络驱动的人往往会有一个误解,以为 phylib 只能配合 MDIO 工作,I2C 管理的 PHY 就"不支持"。实际上 phylib 的设计里,对底层的依赖被抽象成了 mii_bus 结构体。这个结构体提供了 read、write 两个函数指针,驱动通过注册 mii_bus,告诉内核"我可以从某个地址读寄存器、往某个地址写寄存器"。至于这两个函数内部是操作 GPIO 模拟 MDIO,还是操作 I2C 控制器,phylib 完全不关心。
用大白话说就是:phylib 是一个"只管约等于奔波"的调度器,它只负责把 PHY 状态机跑起来——上电、自协商、链路状态检测、速率/双工更新等。但"怎么去访问那颗 PHY"这件事,是在 mdio bus 层解决。只要你把寄存器读写接口交付给它,它就能工作。
在标准接法下,SoC 的 MDIO 控制器驱动会创建一个 mii_bus,read 回调里通过 MDIO 时序去读 PHY 的寄存器。换到 I2C 管理的场景,我们要做的事情是完全等价的:自己实现一个 mii_bus,让它的 read/write 回调通过 I2C 总线去读/写 PHY 的寄存器。只要寄存器内容符合 MII 标准布局,后面的 phylib 逻辑不会有任何变化。
2.2 内核"找到 PHY"的三种典型方式
在内核网络子系统里,"PHY 是怎么被发现"的其实有几种路径,搞清楚这些路径,才知道 I2C 方案改的是哪一环。
- 标准 MDIO 总线扫描。MAC 驱动或独立 MDIO 控制器驱动注册 mii_bus,mdiobus_register 的时候会遍历 PHY 地址 0~31,给每个地址读 PHY Identifier 寄存器,如果读到有效 ID,就创建一个 phy_device。
- 设备树显式描述。在 Ethernet 节点的 mdio 子节点里显式写 ethernet-phy@xx,地址和 PHY 绑定,内核就不再扫,而是直接到指定地址取 PHY 信息。
- fixed-link 方式。这个方法特殊,它不读任何 PHY 寄存器,而是直接在内核里固定链路参数,比如 speed 100、duplex full。它适用于链路不可协商或者管理通道完全不可用的场景,相当于"假装有一颗 PHY",把链路参数写死。
这里有一个容易走的误区:有人发现标准扫描不行,就直接上 fixed-link,把速率固定成 100Mbps,等于让内核绕开 PHY 管理。这能"骗通"网络,但代价很大——不会自动协商,不知道对端能力,链路掉线了内核也不知道。相比之下,如果管理通道只要是 I2C 上的寄存器可读,就不该用 fixed-link 糊弄,而是应该把 I2C 适配成 mii_bus,让内核正常管理 PHY。
2.3 关键结论:先确认寄存器能读,再谈 phylib 兼容
在实际调板过程中,我的建议是先别看太多代码,先用工具确认一件事:这颗 PHY 的寄存器,通过 I2C 到底能不能读出来。能读,后面的路就走得通;不能读,那问题就不在 phylib 而在硬件或最底层的 I2C 通信上。
具体可以用 I2C 工具先探测。假设 PHY 的 I2C 器件地址是 0x10,通过 i2cdetect 能看到这个地址上有设备。然后用 i2cget 去读 PHY 的 Vendor ID 寄存器。标准 MII 寄存器里,寄存器 2 是 PHY Identifier 高 16 位,寄存器 3 是低 16 位。比如常见的 RTL8211F,ID 是 0x001CC916,读出来应该有对应值。这一步验证通过,就说明 PHY 确实在 I2C 总线上活着,而且寄存器映射符合预期。
i2cdetect -y 2 // 扫描 I2C 总线 2,确认 PHY 地址 i2cget -y 2 0x10 0x02 // 读 PHY 寄存器 2,看 vendor ID 高 16 位 i2cget -y 2 0x10 0x03 // 读 PHY 寄存器 3,看 vendor ID 低 16 位这么做还有一个额外好处:顺手验证了寄存器地址偏移、字节序这些细节。很多 PHY 在 I2C 模式下的寄存器偏移不是简单的一一对应,而是有一个内部 page 机制,直接用 i2cget 验证比反复改代码高效得多。
3. 用 mii_bus 把 I2C 通道"伪装"成 MDIO 总线
3.1 适配层整体思路
既然确认了 I2C 通道能读 PHY 寄存器,下一步就是写适配层。整体思路不复杂:构造一个 mii_bus,把 read 和 write 函数指针指向我们基于 I2C 的读写函数。这个 mii_bus 注册成功后,内核再去扫描或绑定 PHY 时,会调用这两个回调函数,从而与 PHY 完成寄存器交互。
这里有一个关键点需要提前强调:mii_bus 的 read/write 回调,函数签名是固定的,其中一个参数是 phy_addr。但在 I2C 模式下,PHY 地址这个概念需要自己处理。你可以把 phy_addr 忽略掉,因为 I2C 器件地址是固定的;但为了兼容可能的多个 PHY 挂在同一 I2C 总线上,更合理的做法是根据 phy_addr 映射不同的 I2C 器件地址。比如设备树里把 phy@0x10 之类放进去,回调函数里再把 phy_addr 解释成 I2C 从地址。
3.2 核心代码框架
下面这段代码是典型的适配层框架,不同平台可能函数名和寄存器读写格式不同,但骨架是通用的。
#include <linux/mdio.h> #include <linux/i2c.h> struct i2c_phy_priv { struct i2c_client *client; u8 phy_i2c_addr; // 按实际硬件确定 }; static int i2c_phy_read(struct mii_bus *bus, int phy_addr, int regnum) { struct i2c_phy_priv *priv = bus->priv; struct i2c_client *client = priv->client; u8 cmd[2]; u8 val[2]; struct i2c_msg msgs[2]; int ret; cmd[0] = (regnum >> 8) & 0xff; cmd[1] = regnum & 0xff; msgs[0].addr = priv->phy_i2c_addr; msgs[0].flags = 0; msgs[0].len = 2; msgs[0].buf = cmd; msgs[1].addr = priv->phy_i2c_addr; msgs[1].flags = I2C_M_RD; msgs[1].len = 2; msgs[1].buf = val; ret = i2c_transfer(client->adapter, msgs, 2); if (ret != 2) return -EIO; return (val[0] << 8) | val[1]; } static int i2c_phy_write(struct mii_bus *bus, int phy_addr, int regnum, u16 value) { struct i2c_phy_priv *priv = bus->priv; struct i2c_client *client = priv->client; u8 buf[4]; int ret; buf[0] = (regnum >> 8) & 0xff; buf[1] = regnum & 0xff; buf[2] = (value >> 8) & 0xff; buf[3] = value & 0xff; ret = i2c_master_send(client, buf, 4); if (ret != 4) return -EIO; return 0; } static int i2c_phy_probe(struct i2c_client *client) { struct device *dev = &client->dev; struct i2c_phy_priv *priv; struct mii_bus *bus; priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; priv->client = client; priv->phy_i2c_addr = client->addr; bus = mdiobus_alloc(); if (!bus) return -ENOMEM; bus->name = "i2c-phy-mii-bus"; bus->read = i2c_phy_read; bus->write = i2c_phy_write; bus->priv = priv; snprintf(bus->id, MII_BUS_ID_SIZE, "i2c-%s", dev_name(dev)); return mdiobus_register(bus); }这段代码最重要的地方在 i2c_phy_read 和 i2c_phy_write。读的时候,先用一个 i2c_transfer 发送寄存器地址,再发一个读请求,拿到 16 位数据,组合返回;写的时候,把寄存器地址和 16 位数据一起打包成 4 字节发出去。有的 PHY 在 I2C 模式下支持"当前地址自增"功能,写数据时还能简化,但最稳妥的写法就是显式带寄存器地址。
3.3 设备树和驱动挂接
注册好 mii_bus 之后,还要让 Ethernet 驱动使用这个 bus。这里有两条路线,取决于你的平台和驱动结构。
如果 PHY 挂在 I2C 设备树节点下,常见做法是把 PHY 上电后由 I2C 驱动创建 mii_bus,然后在 Ethernet 节点的 phy-handle 里引用这颗 PHY 的 device node。如果不方便跨设备树引用,也可以让 MAC 驱动在 probe 阶段直接调用 i2c_phy_probe 产生的 mii_bus,再调用 phy_connect 去绑定 PHY。
设备树里典型描述大概是这样的:
&mac0 { status = "okay"; phy-mode = "rmii"; phy-handle = <&phy0>; mdio { #address-cells = <1>; #size-cells = <0>; /* 注意这里是 I2C 场景下的适配,实际 PHY 不是挂在 MDIO 下 */ }; }; &i2c2 { phy0: ethernet-phy@10 { compatible = "ethernet-phy-id001c.c916"; reg = <0x10>; /* reset-gpios 等属性按需添加 */ }; };需要说明的是,这种做法不算内核官方推荐的"标准姿势",因为 ethernet-phy 这类兼容字符串更多是在 mdio 总线子节点里用;但在实际产品 bringup 阶段,把 PHY 节点挂到 I2C 控制器下面,再由 I2C 驱动去注册 mii_bus,是很多厂商 BSP 都采用的做法。关键是保证 PHY probe 后,注册的 mii_bus 能被 Ethernet 驱动通过 phy-handle 找到。
3.4 为什么这套方案能成:mii_bus 的抽象边界
多说一句为什么这条路可行。内核网络子系统经过了大量迭代,从最早的固定 PHY 驱动,到 phylib,再到 phylink,抽象边界一直很稳定:MAC 驱动关心的是"我有个 PHY 设备,它能提供 link 状态、速率、双工信息",而不关心 PHY 的管理接口底层走的是什么协议。mii_bus 就是这条边界上的关键接口。
只要 read/write 回调语义正确,能给 phylib 返回符合 MII 规范的寄存器值,phylib 会认为这就是一颗普通的 MDIO PHY,自协商、自动介质转换、链路中断检测这些机制全部照常启动。这也是为什么我们不需要去改 phylib 核心代码,只需提供一个适配层的原因。
4. 调试实录:时序、地址映射和慢总线上的坑
4.1 复位和上电时序:PHY 还没醒,驱动就去读寄存器了
第一次把上面的适配层代码跑起来,发现 i2c_phy_read 返回的经常是 0xffff,偶尔又能读到正确的 PHY ID。这个现象很有代表性:PHY 没有完全上电复位完成,I2C 访问就会失败。
I2C 总线上如果设备没就绪,主控侧读到的数据往往是 0xff,或者 I2C 从设备没有 ACK,i2c_transfer 直接报错。很多 PHY 的上电复位时间在几十毫秒到一两百毫秒之间,有些带硬件复位引脚的 PHY,还需要在复位引脚拉低后等待规定的低电平时间。问题在于,内核 I2C 子系统的 probe 时机,和 PHY 芯片自身完成初始化之间,没有天然的同步关系。
解决思路有两个:一个是在 mdiobus_register 之前,给 PHY 的复位 GPIO 一个完整的复位序列,然后 msleep 足够的时间;另一个是在读 PHY ID 失败时,做一次重试。某些 PHY 的 ID 寄存器在第一次访问时甚至需要一个"唤醒读"来激活,第一次读返回 0xffff,第二次才有正确值。实战中我在 i2c_phy_read 里加了简单的重试逻辑,连续读两次,间隔 5ms,解决了一部分启动随机失败的问题。
4.2 寄存器地址映射:别拿 MDIO 的 offset 直接套 I2C
第二个大坑是寄存器映射。标准 MII 寄存器 0~31 是定义好的,但 PHY 切到 I2C 管理模式后,把 MII 寄存器映射到 I2C 内部寄存器空间的规则每一家都可能不同。最常见的两种设计是:
- 直接映射。I2C 偏移地址等于 MII 寄存器号,比如寄存器 0 在 I2C 内部偏移 0x00,寄存器 1 在 0x02。这种情况下字节序要注意,是 16 位高字节在前还是低字节在前,直接影响读回来的值。
- 带 page 的映射。PHY 内部寄存器空间很大,MII 寄存器只是其中的一部分窗口,需要通过 page 寄存器切换。这种情况要格外小心,访问寄存器 2/3 很可能要先把 page 设成 0,修改其他扩展寄存器时再切换 page,否则读出来的 ID 永远是同一个值。
我见过最坑的案例是,PHY 的 I2C 寄存器地址不是按 16 位对齐,而是按 8 位对齐。读 MII 寄存器 2 要发地址 0x02,读寄存器 3 要发地址 0x03,但你如果按标准 MDIO 的高低位打包 16 位,就会把两个 8 位寄存器拼成一个 16 位值,结果反转。这种细节只能靠 data sheet 和实测对比来验证,光看驱动代码很难发现。
4.3 链路能通,但驱动"半盲"不等于没问题
调试过程中还出现了一个很迷惑的现象:我不加任何 PHY 驱动,网口居然也能 ping 通对端。原因不复杂——很多 PHY 在上电后通过硬件 strap 引脚或内部默认配置,自动完成了自协商,数据通路已经建立,MAC 也能收发帧。但这个状态下,内核没有 phy_device,不知道链路速率是多少,也无法响应 ethtool 的查询,更没有链路断开检测。
这种"半盲"状态在测试部门看来就是"时好时坏"的网络。比如对端从一个 100M 交换机换成一个 1G 交换机,PHY 自动协商到千兆,但 MAC 侧的时钟配置还是 RMII 下默认的 100M 模式,就会出现丢包甚至完全不通。所以,千万不要以"能 ping 通"作为判断适配完成的标准,必须看到 ethtool 输出正确的速度和双工模式,并且状态寄存器能跟着链路变化。
4.4 用 i2c-tools 做对照实验
遇到寄存器值异常时,我最常用的手段是用 i2c-tools 做对照。先把 PHY 当作普通 I2C 器件来访问,读出几个关键寄存器的期望值,再对比适配层代码返回的值。这个对照实验能快速区分是"适配层写错了"还是"寄存器映射本身理解错了"。
比如在 shell 里执行:
i2cget -y 2 0x10 0x00 // control register,复位后通常为 0x1140 i2cget -y 2 0x10 0x01 // status register,link 建立后 bit 2 应为 1 i2cget -y 2 0x10 0x04 // auto-negotiation advertisement如果 i2cget 能读出合理值,而 sysfs 里 /sys/class/net/eth0/phy 的状态不对,那问题大概率在 mii_bus 适配代码的数据格式处理上。如果 i2cget 本身就读不出合理值,那就得回到硬件:I2C 地址对不对、PHY 是否被复位、总线有没有被其他设备占用。
4.5 I2C 慢带来的连锁反应
最后一个容易忽略的坑是性能。MDIO 的时钟通常在几十 kHz 到数 MHz,而 I2C 标准模式只有 100k,快速模式 400k。每次读 PHY 寄存器,I2C 要发起 STOP/START、传输设备地址、寄存器地址、数据,这些开销比纯 MDIO 大不少。
phylib 默认有一个状态机轮询机制,会周期性检查 PHY 状态寄存器。如果 I2C 总线特别慢,或者同一 I2C 总线上挂了多个设备,一次轮询周期可能被拖长,导致链路状态变化不能及时上报。针对这个问题,可以在 PHY 驱动里把轮询间隔调大,或者使用 PHY 的中断引脚。但要注意,中断方式需要 PHY 支持中断寄存器,且 GPIO 中断线要接入 SoC 的 GPIO 控制器,对硬件设计有要求。没有中断引脚时,宁可靠软件轮询,也不要用过短的轮询周期去抢 I2C 总线。
5. 从单颗 PHY 到 DSA 交换机:同款思路怎么迁移
5.1 DSA 驱动里的内部 PHY 也是走 mii_bus 的
前面解决的是单颗 PHY 不走 MDIO 走 I2C 的场景,但嵌入式 Linux 设备上还有一类常见的网络硬件——DSA 交换机芯片。交换机芯片往往自带多个内部 PHY,或者通过内嵌的 MDIO 控制器连接外部 PHY。DSA 框架对内部 PHY 的管理,最后同样落到 mii_bus 上。
看过 DSA 驱动的应该知道,struct dsa_switch_ops 里面有 phy_read 和 phy_write 这两个回调。DSA 框架注册内部 MDIO 总线时,会把这些回调包装给上层使用。只要实现这两个回调,DSA 就能像管理普通 PHY 一样管理交换机内部的 PHY。所以,如果一颗交换机芯片的控制接口恰好是 I2C,而不是标准 MDIO,那思路就变得非常清晰:把 dsa_switch_ops 里的 phy_read / phy_write 实现为通过 I2C 读写交换机内部寄存器。
5.2 交换机内部寄存器通过 I2C 转接的适配方法
具体实现上,很多交换芯片内部有一组 PHY 管理寄存器,比如基地址 0x100,偏移 0x20 一个端口,之后每个端口内部再按 MII 寄存器布局排布。这种情况下,适配函数要做的就是把"读第 3 个内部 PHY 的寄存器 4"翻译成"读偏移地址 base + port_index * offset + regnum * reg_stride"。
代码层面可以借助 regmap 简化,比如用 devm_regmap_init_i2c 把 I2C 寄存器访问封装成 regmap,然后 dsa_switch_ops 里的 phy_read 调用 regmap_read,phy_write 调用 regmap_write。这种做法的好处是 regmap 自带缓存和lock,多线程访问时不容易出乱,而且调试时可以通过 regmap.debugfs 直接看寄存器值,相当方便。
static int switch_phy_read(struct dsa_switch *ds, int port, int regnum) { unsigned int val = 0; int base = ds_phy_base(ds, port); regmap_read(ds->regmap, base + regnum * 2, &val); return val; } static int switch_phy_write(struct dsa_switch *ds, int port, int regnum, u16 val) { int base = ds_phy_base(ds, port); return regmap_write(ds->regmap, base + regnum * 2, val); }这里 regnum * 2 这个系数就是前面说的 8 位对齐还是 16 位对齐的问题,不同芯片不一样,务必以芯片手册为准。很多国产交换芯片对寄存器的访问宽度、位序都有自己一套规矩,不要默认和 README 里的例子完全一致。
5.3 一套可复用的排查顺案
单颗 PHY 和交换机 PHY 都适配完之后,我总结了一套固定的排查顺序,每次 bringup 网络设备都能少走弯路,在这里一起分享出来:
- 硬件确认。先确认 PHY/交换芯片的管理接口到底是 MDIO 还是 I2C,有没有 strap 引脚决定工作模式。用万用表量引脚电平,别只看原理图。
- I2C 探测。用 i2cdetect 确认器件地址,确认芯片在总线上存在。
- 裸读验证。用 i2cget 读 Vendor ID/Model 寄存器,验证寄存器映射和字节序。
- 适配层最小验证。先不接完整 phylib,直接在适配层代码里填一个 debugfs 或早打印,读出 PHY ID 并打印。
- 注册 mii_bus,观察内核扫描结果。如果 phy_device 成功创建,再继续。
- ethtool 验证。重点看 Speed、Duplex 和 Link detected,不要只看能不能通。
- 链路变化模拟。手动插拔网线,确认 PHY 状态寄存器能更新。
这套流程不管是单 PHY 还是 DSA 都适用,核心思想就是:一件一件事验证,把"内核的网络栈"和"底层寄存器通信"两个问题解耦。
5.4 同样思路能覆盖的产品场景
顺便说一句,不少做串口转网口服务器、工业以太网网关的朋友应该有体会,这类产品的主控往往是一颗小 ARM 或 MCU,资源和引脚都非常紧张。为了省掉一组 MDIO 控制器和引脚,硬件工程师经常会把 PHY 的管理接口设计成 I2C。这种情况下,软件上的本质工作和我上面写的完全一样:用 mii_bus 适配层把 I2C 寄存器访问接进 phylib。把这次调试经验抽象出来,就不只是解决一块板子的问题,而是给一类嵌入式网络设备提供了通用的底层适配方案。
我个人的体会是:碰到 Linux 下"网口不通"的问题,先不要急着改 phylib,也不要第一时间用 fixed-link 绕过去。先问清楚硬件上 PHY 的管理通道长什么样,然后顺着 mii_bus 这一层做适配,把控制权完整交给内核网络子系统。这套思路尤其在国产化平台、新设计板卡的 bringup 阶段特别实用。最后再补一个小建议:把 i2cget 验 PHY 寄存器的脚本保存下来,固化到团队的 bringup 环境里,下次新板卡回来,五分钟就能判断管理通道硬件正不正常,比打开示波器量 MDIO 波形快得多。