1. MDIO协议是什么,为什么搞网络的人绕不开它
做嵌入式网络开发的人,天天跟PHY芯片打交道,就绕不开MDIO。MDIO全称Management Data Input/Output,也叫MII Management Interface,是IEEE 802.3标准里定义的一种管理接口。它负责让MAC侧(比如SoC里的GMAC控制器、交换机芯片、光模块的MCU)去读写PHY芯片的寄存器,从而完成速率协商、链路状态查询、环回测试、掉电管理、LED控制这些操作。
说通俗一点:PHY芯片就像一台设备的“网线感知器官”,它能感知网线有没有插上、对端设备支持什么速率,但这些感知结果不是凭空产生意义的,必须让MAC侧把它读出来,并根据结果去做配置。MDIO就是这条“大脑读取感知结果、下发调整指令”的神经通路。没有这条通路,PHY芯片连自适应协商都没法正常触发,网口就只能傻在那边。
MDIO物理上只有两根线:MDC(Management Data Clock,管理时钟)和MDIO(Management Data Input/Output,管理数据)。MDC由MAC侧提供,是单向时钟输出;MDIO是双向数据线,读写时分时复用。这个设计和I2C有点像,但比I2C简单,没有地址ACK应答机制,也没有设备热插拔检测,帧格式完全是IEEE 802.3自己定义的一套。
这篇文章我直接按实操路线来讲,适合三种人看:一是刚接触嵌入式网络、被PHY芯片驱动和寄存器搞得头疼的开发者;二是硬件工程师,想弄明白双网口共用一个MDIO该怎么设计、为什么有时候无法访问PHY;三是做Linux系统适配的工程师,遇到“PHY驱动加载正常但网口就是起不来”这类问题,需要从MDIO层面排查。后面会涉及Linux下phy驱动、设备树、mdio总线注册这些实际工程问题,也有纯硬件侧的参考设计经验和排查手法。
2. MDIO协议栈核心原理拆解
2.1 两根线是怎么把寄存器读出来的
先看MDC和MDIO的分工。
MDC是时钟源,由MAC侧主动产生。IEEE 802.3规定MDC的最高频率是25MHz,但实际工程里常见的是2.5MHz,因为这个频率是10/100M以太网时代就定的老频率,很多PHY芯片内部对这个频率做了同步逻辑,跑太高反而容易出问题。还有一些SoC的MDIO控制器本身是挂在AHB/APB总线上的,主频几百兆赫兹,内部做了分频配置,实际输出的MDC可以自由调,但建议从低往高试,不要上来就拉满。
MDIO是数据线,双向。在MDC的上升沿采样数据。PD(PHY Device)这边会把自己的寄存器数据在MDC下降沿之后、下一个上升沿之前准备好,MAC侧在MDC上升沿去读。数据方向切换的时刻是帧尾,这个后面讲帧格式时会细说。
MDIO线需要接上拉电阻,一般4.7kΩ到10kΩ都行。我见过不少板子因为MDIO上拉漏接,导致读出来的PHY寄存器全是0xFF,特别坑。正常工作时MDIO默认状态是高电平。如果拿示波器抓MDC波形,能看到帧与帧之间有明显的空闲高电平时间。
2.2 帧格式和读写时序细节
MDIO帧格式由一个前导码开始,整个帧结构是经典的五段式:
| 字段 | 位宽 | 说明 |
|---|---|---|
| PRE | 32位 | 前导码,全1,用于同步 |
| ST | 2位 | 起始码,固定01 |
| OP | 2位 | 操作码,读是10,写是01 |
| PHYAD | 5位 | PHY芯片地址,0~31 |
| REGAD | 5位 | 寄存器地址,0~31 |
| TA | 2位 | 状态转换位,读帧是Z0,写帧是10 |
| DATA | 16位 | 数据(读帧是PHY输出,写帧是MAC输出) |
有个细节很多资料不会强调:前导码真的是32位全1。而且MDIO规范里允许一种“省略前导码”的帧格式,叫“preamble suppression”或者“management frame without preamble”,但这个功能不能在常规通信里默认开启,否则控制器和PHY双方必须有同步机制。Linux内核里是有这个接口的,但是你一般用不到,也不建议用,老老实实带全前导码最稳。
读操作的时序是这样的:MAC先输出32个1,然后输出ST=01、OP=10、5位PHY地址、5位寄存器地址,然后TA是2个bit,第一个bit是高阻Z,第二个bit是一个0,紧跟着PHY侧接管数据总线,输出16位寄存器值。总线切换的瞬间如果上拉电阻没接对,或者PHY芯片的驱动能力太弱,抓波形会看到第一个数据bit被拉到个很怪的电平,后面那几位正常的现象。
写操作的时序就简单一些:OP是01,TA是10,然后MAC直接把16位数据发出去。PHY在MDC上升沿采样,不需要ACK。
所以读和写的核心区别在TA位和总线方向控制上。这也是初学者写FPGA的MDIO控制器时最容易翻车的地方,很多人忘了把MDIO引脚切输入方向,导致读出来的数据全是自己输出出去的旧值。
2.3 寄存器模型:标准寄存器与扩展寄存器
PHY芯片内部寄存器映射是分“标准寄存器”和“扩展寄存器”两层结构的。
标准寄存器就是IEEE 802.3规定的0~15号寄存器,0号寄存器是控制寄存器(BMCR),bit 0.15是软复位,bit 0.13是速率选择,bit 0.12是自动协商使能,bit 0.11是掉电,bit 0.8是全双工/半双工选择。1号寄存器是状态寄存器(BMSR),bit 0.15是100BASE-T4能力,bit 0.12是自动协商完成标志,bit 0.9是10BASE-T全双工,bit 0.8是10BASE-T半双工。2号和3号寄存器是PHY ID,由OUI加型号加版本组成。4号、5号是自动协商对端能力寄存器和本地能力寄存器。6号寄存器是自动协商扩展状态。
这些标准寄存器在IEEE 802.3的22.2.4章节里写得很细。做驱动开发的人必须把这些寄存器记得滚瓜烂熟,因为Linux phylib里读取链路状态用的就是BMSR寄存器的bit 0.12,触发自动协商就是改BMCR寄存器,如果这些地址和bit位搞错,驱动里的逻辑再好看也没用。
扩展寄存器就五花八门了。每个PHY厂商都会在寄存器3~31之间预留一些,然后在3~31之间塞自己的功能,比如LED控制、环回模式、测试模式、省电模式、EEE功能配置等。更大的寄存器空间是通过“page”(页)的方式访问的:先往寄存器0x1F写入一个page编号,再访问0x10~0x1E这些普通寄存器,访问到的就是对应page里的寄存器。这个机制是各厂商自定义的,所以换PHY芯片之后,扩展寄存器的驱动代码基本都要跟着改。
锐捷、瑞昱、博通、Marvell这些厂家的PHY芯片都有自己的一套page机制。最坑的是有些芯片默认上电后指向的page不是0,如果你驱动里没有显式切page就给扩展寄存器写值,写进去的门影子寄存器可能是错的。
2.4 MDIO时钟和时序容差
IEEE 802.3里对MDC和MDIO之间的时序关系有明确要求,但我实际测下来发现,PHY芯片能容忍的时序范围比规范宽得多。规范的核心就是把MDIO数据的建立时间和保持时间管好,数据变化必须跟MDC下降沿对齐,这样MAC侧在上升沿采样的窗口最大,PHY侧在下降沿后读数据的窗口也最宽裕。
如果你是自己写FPGA逻辑实现MDIO主站,最稳妥的实现方式就是把MDIO的bit数据在MDC下降沿前一点就准备好,然后等MDC上升沿被采样。换言之,数据动作描述为“下降沿驱动,上升沿采样”。按这个原则写出来的代码基本不会出时序问题。如果你上来就按“上升沿驱动,上升沿采样”去写,就会看到奇怪的现象:读回来的数据偶尔错一个bit,用示波器单次抓又抓不到。
PHY芯片地址的编号对应的是PHYAD引脚上拉/下拉的配置。RTL8211、KSZ9031这类常见芯片多是把PHYAD[4:0]引脚引出来,板子上通过上下拉电阻设定地址。这个地址不能跟同一条MDIO总线上其他PHY冲突,也不能跟MDIO控制器的“广播地址”冲突(广播地址是0,所有PHY都响应),所以实际板卡上PHY地址一般都会避开0。
3. Linux下MDIO总线和PHY驱动的实际工程玩法
3.1 mdio-bus是怎么注册出来的
在Linux内核里,MDIO控制器被抽象成mdiobus。一个mdiobus对应一组物理上的MDC/MDIO引脚,在这组引脚上可以挂多个PHY。内核里注册MDIO总线的路径一般有两种:一种是通过设备树里的mdio节点,另一种是SoC自带MDIO控制器的驱动直接调用mdiobus_register。
做设备树时,你会看到类似这样的结构:
&mdio0 { status = "okay"; phy0: ethernet-phy@4 { reg = <4>; device_type = "ethernet-phy"; }; };这个节点的用法就是告诉内核:mdio0总线上地址为4的位置有个PHY,叫phy0。之后以太网控制器(比如MAC节点)就可以通过phy-handle指向这个PHY:
&gmac0 { status = "okay"; phy-handle = <&phy0>; phy-mode = "rgmii-id"; };这套机制成熟到,你基本只需要保证设备树里的phy地址和硬件跳线一致,其他交给phylib去做。
但有个常见的坑:很多SoC的MDIO控制器和以太网MAC控制器是绑在一起的,比如你在设备树里disable了以太网MAC节点,它的MDIO控制器也会变成禁用状态。结果就是PHY照样有复位、有时钟,但MDIO总线根本不起作用,PHY读不到。这种情况需要确认设备树里MAC节点和MDIO节点的状态控制逻辑。
3.2 phylib驱动框架:从probe到read_status
Linux的PHY驱动框架分两层:一层是PHY lib自身的通用逻辑,另一层是具体PHY芯片的驱动。
前者处理的是所有PHY共性的东西,比如自动协商的软件状态机、暂停帧能力协商、速率匹配等。后者处理的是厂商特有的功能,比如Marvell的m88e1510驱动、瑞昱的RTL8211系列驱动。
内核里PHY驱动注册的结构体大致长这样:
static struct phy_driver ks9131_driver = { .phy_id = 0x00221610, .phy_id_mask = 0xfffffff0, .name = "Micrel KSZ9131", .config_init = ksz9131_config_init, .config_aneg = ksz9131_config_aneg, .read_status = ksz9131_read_status, .suspend = ksz9131_suspend, .resume = ksz9131_resume, };phy_id匹配规则是:MDIO读出的PHY ID寄存器值跟phy_id做掩码比对,匹配成功就绑定这个PHY驱动。如果你换了PHY芯片但设备树里没有配置正确的compatible,内核从MDIO读取的PHY ID会和驱动里的ID不匹配,最终PHY就会回退到“通用PHY驱动”(genphy)。这种情况下十有八九链路能通,但芯片特性功能全没启用。
实际调试中我见过最典型的例子:板子上用的PHY是千兆芯片,但内核识别成genphy之后只协商出百兆速率,抓破脑袋不知道原因。答案就是PHY驱动没匹配上。
有一种做法可以验证是不是这个问题:在驱动加载后,手动读PHY ID寄存器,看看读出来的值和PHY芯片手册上写的ID一不一致。内核提供了一堆调试手段,最基础的是通过mdio-tools工具或者debugfs直接操作MDIO总线。
3.3 不支持标准MDIO总线的SoC怎么访问PHY
有些方案或SoC出来的时候MDIO控制器驱动不成熟,或者MDIO控制器在某个平台上根本没有对应引脚,这时候就得换路子访问PHY寄存器。
热词里有个“linux phy 不使用mdio,使用i2c”,这就是典型的替代方案。不少PHY芯片在有MDIO接口的同时,还留了I2C从接口。比如RTL8211E没有I2C,但BCM5461、KSZ9031、YT8010这些芯片内部逻辑就把MDIO的一部分寄存器也映射到了I2C接口上,你完全可以通过I2C地址去读写PHY寄存器。
在Linux下用I2C访问PHY的路径一般是:设备树里把PHY挂到i2c总线上,然后驱动里用i2c_master_recv / i2c_master_send这类接口去读写。
&i2c2 { status = "okay"; phy0: ethernet-phy@18 { reg = <0x18>; compatible = "microchip,ksz9031"; }; };I2C访问PHY有一层天然的缺点:I2C速率本身就比MDIO慢很多,I2C标准速度100kbps,高性能模式才400kbps,而MDIO跑2.5MHz很轻松,所以在要求频繁轮询PHY状态的交换机场景里,用I2C代替MDIO会明显增加CPU开销。另外一个缺点是I2C没有MDIO那种“一次帧直接完成读16位寄存器”的巴适操作,必须先写寄存器地址再读数据,或者用I2C的重复起始位来一次性完成,代码层面多绕一层。
但这个方案也有正经的应用场景——有些光模块的DDM(数字诊断监控)信息就是通过I2C访问的,SFP的管理接口本身就是I2C。如果设计一个模块同时要读光模块信息和PHY信息,板子上就可以直接复用一条I2C总线,硬件布线上省一组引脚,这在空间受限的板卡上是个不小的优势。
3.4 双网口共用一个MDIO的配置实例
热词里专门有个“双网口共用一个mdio”,这是一个很典型的硬件设计提法。现在很多SoC只有一组MDC/MDIO引脚,但板卡要做两个网口,两个PHY芯片就得串在这两条线上。
先理清一个原则:MDIO总线上可以挂多个PHY,靠PHYAD区分。所以共用一个MDIO的核心问题就是:两个PHY的PHYAD必须不同。
硬件上怎么保证?两个PHY芯片的PHYAD引脚一共5根,分别接上下拉电阻,做成不同的地址。比如PHY0地址设4,PHY1地址设6。然后把两个PHY的MDC、MDIO引脚并联到SoC的MDC、MDIO引脚上,每个PHY两边各自加上拉电阻。
这样在Linux下,设备树MDIO节点下就能挂两个PHY:
&mdio0 { status = "okay"; phy0: ethernet-phy@4 { reg = <4>; }; phy1: ethernet-phy@6 { reg = <6>; }; }; &gmac0 { status = "okay"; phy-handle = <&phy0>; phy-mode = "rgmii-id"; }; &gmac1 { status = "okay"; phy-handle = <&phy1>; phy-mode = "rgmii-id"; };这里有个坑必须强调:两个PHY的PHYAD如果撞了,则后注册的那个PHY会把先注册的覆盖掉,或者产生“PHY地址冲突”的告警。在内核日志里你会看到类似“PHY 0x04 not found”或者识别出来的PHY ID对不上,这时不要先怀疑驱动,先用硬件测量确认PHYAD引脚的上下拉到底有没有生效。
另外一个硬件坑是:MDIO上拉电阻只能有一份,不要两个PHY各放一个上拉,这两个上拉并联之后电阻减半,对驱动能力不足的MDIO主站是致命的。辅助的做法是在每个PHY的MDIO引脚上串联一个几十欧姆的电阻,起到隔离和限流的作用,防止某个PHY的故障把整条总线拖死。
4. MDIO读写流程和寄存器配置实操记录
4.1 从SoC寄存器操作到MDIO帧的调用链
平时写Linux驱动,你很少直接去操作MDIO控制器的寄存器,但总有人问这样一个链路:用户态想读一个PHY寄存器,要经过哪几层?
先看一张通用调用链:
用户态(mdio-tools / ethtool) → Socket / ioctl → 网卡驱动 ethtool_ops → phylib mdiobus_read/mdiobus_write → MDIO控制器驱动(如 stmmac_mdio_read / fsl_pq_mdio_read) → 硬件寄存器(如 MII_DATA、MII_ADDR) → MDC/MDIO波形这里面最贴近硬件的是最后两层。以常见的Synopsys DesignWare MAC为例,MDIO控制器有MII_ADDR和MII_DATA两个寄存器。发起一次读操作时,先把PHYAD、REGAD、操作码写进MII_ADDR,同时置busy位,然后轮询MII_DATA里的busy位,等待硬件把结果放进数据位。
#define MII_ADDR_CR_REG_ADDR 0x00 #define MII_ADDR_PHY_ADDR (0x00 << 8) #define MII_ADDR_CR (0x01 << 30) #define MII_DATA_TA (0x02 << 2) #define MII_DATA_RA (0x01 << 30) #define MII_BUSY (0x01 << 0) static int stmmac_mdio_read(struct mii_bus *bus, int phyaddr, int phyreg) { unsigned int addr = phyaddr << 8 | phyreg; unsigned int data = MII_DATA_TA | MII_BUSY | (phyreg << 6); // 写入地址寄存器 writel(addr, priv->ioaddr + GMII_ADDRESS); // 写入数据寄存器 writel(data, priv->ioaddr + GMII_DATA); // 等待busy清零 while (readl(priv->ioaddr + GMII_DATA) & MII_BUSY) { udelay(10); timeout--; } return readl(priv->ioaddr + GMII_DATA) & 0xffff; }这段代码虽然简化过,但核心流程是真实的:写地址、写命令、轮询完成、读数据。要注意这里的MDC时钟频率是通过寄存器配置分频的,不设置的话硬件可能直接按默认跑,有些MAC默认分频是62.5MHz,这时必须把MDC时钟配置到2.5MHz左右,否则PHY芯片会不稳定。通常SoC的MDIO控制器时钟参考来自系统AHB时钟,需要配置clk_div得到合适的MDC。
4.2 用mdio-tools直接读写PHY寄存器
Linux下有一款调试神器叫mdio-tools,包含mdio和phytool两个工具。它的用法核心就是通过MII总线访问PHY的寄存器,尤其适合在没有网卡驱动依赖的情况下,直接把PHY寄存器读出来看看。
典型用法:
# 列出当前系统所有MII总线 mdio list # 在bus 0上读取地址为4的PHY的寄存器1 mdio read 0 4 1 # 往地址为4的PHY的寄存器0写入0x1000(比如关闭自动协商、设置千兆全双工) mdio write 0 4 0 0x1000第一次用这个工具的人,很容易犯一个错:把“bus编号”和“PHY地址”搞混。mdio list列出来的“0”是内核注册的bus序号(比如stmmac-0),后面的4才是PHY地址。不是所有的bus编号都以0开始,具体得看 dmesg 里注册的bus id。
如果你打算在项目里广泛使用mdio-tools,最好先把每个PHY芯片的手册里常用寄存器列一个速查表,比如爱用的0x00控制寄存器、0x01状态寄存器、0x1E扩展状态寄存器,这样调试效率会高很多。
4.3 从波形的角度确认读写是否正常
就算你有驱动,有工具,读出来的值是错的,也没法快速定位是链路时序问题还是寄存器配置问题。这时候就得上示波器抓MDC和MDIO波形。
标准操作:
- 示波器两个通道,CH1接MDC,CH2接MDIO,保证两个探头的地都接在同一个地参考上。
- 触发模式设置成上升沿触发,触发电平设在1.5V左右,因为MDIO是3.3V电平。
- 时间档设在20us/div左右,这样能覆盖整个读帧和写帧。
- 运行一次读寄存器操作,比如mdio read 0 4 1,抓一拍波形。
正常读波形你会看到:32个高电平脉冲组成的PRE段,然后ST、OP、PHYAD、REGAD、TA这些字段在MDC上升沿被采样,MDIO从高阻切换到驱动输出最后一个16位数据。注意在TA段,第二个bit之后,MDIO引脚的主人从MAC变成PHY,波形上会有一个明显的方向切换痕迹。如果这里方向切换正常,后续数据基本都对,如果整个数据段都是固定的错误值且没有方向切换痕迹,大概率是驱动代码忘了把引脚从输出切到输入。
级联示波器的技巧是,把MDC频率先降到最低可配置值来抓波形,比如SoC MDIO控制器如果支持配置分频,直接把MDC降到1MHz,波形会更干净清晰,方便数bit位。等逻辑对了再提上去确认高速稳定性。
4.4 寄存器页切换的坑:扩展寄存器为什么老是写不进去
前面提过page机制,这里具体展开一个实例。
继续用Marvell的88E1512举例子,它内部有page的概念。要访问一些扩展功能寄存器,比如LED控制、基于寄存器的环回、EEE配置,必须先把page切过去。
访问流程:
# 从page 0切到page 2 mdio write bus phy 0x1F 0x0002 # 现在读0x10寄存器,实际读的是page 2的0x10 mdio read bus phy 0x10写完之后要记得切回page 0,否则后面所有normal寄存器的访问都会落在错误的页上,表现就是链路时好时坏。
很多人在驱动代码里写扩展寄存器,只写一遍控制器就以为生效了,结果换成板上电后自动协商结果跟配置不一致。这类问题排查思路就是:先用mdio-tools手动切页、读写、切回,复现问题,然后跟驱动代码对比看缺了哪一步。大多时候是缺了切回page 0这一行。
5. 双网口共享MDIO时踩过的那些坑
5.1 地址冲突导致寄存器读写串扰
双网口共用一个MDIO总线,最大的坑就是两个PHY地址冲突。我做过一块双口网卡,PCB上两个PHY的PHYAD配置为相同的地址,逻辑上只有一条MDIO总线,结果两个PHY都能识别,但读出来的PHY ID和寄存器值明显相互干扰。你从MAC0的口读寄存器,得到一个数据;再从MAC1的口读,同样地址读出来的ID变成另一个PHY的ID。原因是MDIO总线上的设备通过地址匹配响应,MAC0发起的帧也会被两个PHY同时收到,两个PHY地址一样就都试图输出数据,MDIO线上的驱动互相打架。
解决办法有两个方向:硬件上改跳线地址,或者软件上只挂一个PHY到MDIO总线、另一个改用其他管理通道。硬件改地址是首选,成本最低、逻辑最清晰。
5.2 PCBA上PHY芯片的复位时序对MDIO的影响
在不少板卡上,两个PHY芯片的复位引脚可能接到了同一个GPIO或者同一条复位信号线上。如果这些复位信号在上电时波形混乱,比如毛刺比较多,或复位完成时间长,会出现一种情况:MDIO控制器已经注册了总线,但PHY芯片还处于复位状态,MDIO读回来的寄存器全是0x0000或者0xFFFF。这个现象的迷惑性在于不是每次上电都复现,而是有时能读到有效ID、有时不能。
我的排查经验是:先不接复位信号,手动固定复位引脚电平,强制PHY暂停复位,然后用mdio读ID,如果这时能稳定读到ID,十有八九是复位时序的问题。再量一下复位引脚和MDC/MDIO上电的相对时序,确认复位释放是否有足够的稳定时间。
PHY芯片的datasheet里一般会给出“reset完成之后至少等待X毫秒才能进行MDIO访问”的说明。不过有的芯片要求短则10ms、长则150ms,范围很大,板子上如果RC延时不够或者复位信号控制逻辑设计粗糙,就会导致上电后第一次MDIO访问总是失败。解决办法是驱动里对PHY做初始化时,加一个固定延时,或者在MDIO总线注册成功后主动对目标PHY做一次reset检测。
5.3 总线电容和上拉电阻的搭配
双PHY并联在同一条MDC/MDIO线上,总线的电容负载会成倍增加。如果PCB走线太长,线宽太细,再叠加两个PHY芯片的引脚电容,MDC的上升沿会被拉得特别缓。上升沿如果太缓,超过PHY芯片手册里的最大上升时间要求,采样时序就不对了,表现出来的现象是:低速(比如把MDC配到1MHz)能正常,把MDC频率调到标准值之后就出现间歇性误码。
解决办法有几个方向:
- 减小MDIO和MDC上拉电阻的阻值,比如从10kΩ改成4.7kΩ,能略微改善上升沿。
- 缩短总线走线长度,尽量做到等长,MDC和MDIO不要相差太远。
- 在PCB上给每个PHY的MDIO和MDC引脚各串一个33Ω的小电阻,隔离负载,减少反射。
- 在SoC的MDIO控制器配置里调低MDC频率,这是最后的兜底方案。
我见过一辆量产设备,PCB上双网口的MDIO走线绕了大半个板子,导致两个PHY只能把MDC压到1.2MHz才能稳定读寄存器。这种问题后期特别难改板,所以在硬件设计评审阶段就要把MDIO走线当高速信号来对待,不要因为频率低就随便拉。频率低归低,信号完整性照样有要求。
5.4 MDIO总线复用器:一种软件层的解决思路
如果硬件上PHY地址确实冲突了,但板子已经量产改不了硬件,又想通过软件解决,市面上有一种MDIO总线复用器的方案。这种芯片本身一端接主MDIO总线,另一端扩展出多路MDIO子总线,每一路可以单独接PHY,从而用软件控制选择切换到哪一路。常见的如TI的SMI总线开关、NXP的MDIO mux、立锜这类厂商的“MDIO switch”芯片。
Linux内核里对应的是mdio-mux框架。它的原理是把“父MDIO总线”抽象成一个mux总线,每个子总线上挂的PHY地址可以一样,因为访问子总线时通过mux芯片的寄存器先选择通道。设备树配置类似这样:
mdio-mux { compatible = "mdio-mux-gpio"; pinctrl-names = "default"; pinctrl-0 = <&mdio_mux_pins>; mux-gpios = <&gpio1 6 GPIO_ACTIVE_LOW>; mdio-parent-bus = <&mdio0>; mdio_mux_1: mdio@0 { reg = <0x0>; #address-cells = <1>; #size-cells = <0>; phy_a: ethernet-phy@1 { reg = <1>; }; }; };这个方案的问题是多了一次寄存器访问开销,每次读写PHY寄存器都要先切通道,性能有一定损耗。但在部分量产设备里,这是唯一不改硬件就能解决地址冲突的办法。所以mdio-mux这个方向值得知道,至少排查问题的时候你要能想到这个路子。
6. MDIO无法正常工作时的排查手册
6.1 八种典型故障现象与定位方法
做MDIO调试多了,很多问题都是重复的。我列一份故障排查速查表,每一类问题都给出最直接的确认手段。
| 故障现象 | 可能原因 | 确认方法 |
|---|---|---|
| 读PHY ID全FF | MDIO上拉缺失 / PHY未上电 / 复位未释放 | 量MDC有无波形,量PHY电源和复位引脚 |
| 读PHY ID全00 | PHY地址总线短路到GND / 寄存器读时序方向不对 | 量PHYAD引脚上下拉状态,检查驱动方向切换逻辑 |
| 偶尔能读通,偶尔不能 | 复位时序太短 / MDC过快 | 给复位释放加延时,降低MDC频率测试 |
| 读回数据整体左移/右移一位 | MDIO方向切换时刻不准确 / 帧格式字段位错 | 用示波器抓帧数bit位,对照IEEE帧结构 |
| 双网口两个PHY识别串扰 | 两个PHY地址冲突 | 分别断开一个PHY再看,或用mux方案 |
| Linux下PHY驱动绑定失败 | PHY ID匹配不上 / 驱动没有注册 | 查看dmesg里mdio bus枚举结果,比对phy_id_mask |
| ethtool能看到PHY信息但没有link | 自动协商失败 / 对端未插线 / PHY模式配置错 | 读BMSR寄存器看link bit,检查phy-mode配置 |
| 网口能协商千兆但吞吐量低 | MDIO访问异常导致EEE或节能功能异常 | 用mdio-tools空闲读寄存器观察是否存在超时 |
这些里最容易误导人的是第二种:读全00。很多是PHY地址引脚被错误地全部下拉到地导致地址为0。IEEE规范里地址0是“广播地址”,如果PHY地址设成0,它会对所有不匹配的访问也产生响应,导致总线上特别多的响应冲突。
7. 关于MDIO调试工具链的个人经验建议
7.1 示波器触发的进阶用法
如果你手头有一台带宽足够的示波器,用“总线解码”功能直接解码MDIO帧是很省力的。但先别高兴,MDIO解码不是所有示波器都默认支持,很多中低端示波器你只能靠手动数bit。手动数bit其实也不难,关键是先定位帧的起始位置。
MDIO帧的最前面是32个高电平的PRE,所以你在屏幕上先找到一段很长的高电平脉冲段,这段的末端就是PRE结束的地方,后面紧跟着ST和OP的编码。ST固定是01,OP读是10、写是01。数到ST之后的每一位,按PHYAD、REGAD、TA、DATA的位宽切割,就能把寄存器地址和值解出来。
有次我调一块板子,PHY读出来的ID总是不对,示波器上数出MDIO帧的DATA字段是这样:PHY输出的值和手调寄存器里的值完全一致但顺序颠倒,才发现是FPGA里MDIO位序做了MSB/LSB反转,硬件工程师又没在逻辑里统一。手动数bit在这种场景比任何工具都好用。
7.2 逻辑分析仪更适合长序列分析
示波器适合看单帧波形,但如果你要在嵌入式Linux里持续监控MDIO总线上跑了哪些读、哪些写,逻辑分析仪比示波器强太多。尤其是一台带协议解码的Saleae逻辑分析仪,直接把MDC接CH0、MDIO接CH1,打开MDIO协议解码器,就能看到完整的帧序列:哪个PHY地址、哪个寄存器、读写方向、数据内容。
这个工具对排查驱动异常特别有效。比如怀疑驱动在启动阶段乱写PHY寄存器,你可以上电抓一次完整启动流程,把每次MDIO帧都解码出来,跟代码里的调用点对照,一下子就能定位是哪一行的操作导致PHY进入奇怪状态。
逻辑分析仪的另一个好处是能长时间采集。有些软件问题不是固定复现的,比如跑一小时之后PHY掉线,你用示波器根本等不起,逻辑分析仪开着让它录,事后慢慢翻。
7.3 ethtool和普通网络设备调试的关系
大部分做网络开发的人,排查网口问题第一步就是ethtool,但很多人不知道,ethtool底层也会访问MDIO。比如ethtool eth0这个命令,它把MAC和PHY的协商状态全打出来,底层就是读取BMSR寄存器的link状态位、BMCR寄存器的自动协商设置。如果你静态查看半天,发现问题锁定在“网口link没起来”,就该用mdio-tools直接读寄存器确认是PHY没协商成功还是MAC侧没有上报。
这里有个特别的坑:ethtool读到的link状态的是驱动缓存的,不是实时的。ethtool eth0显示的“Link detected: yes/no”通常是phylib链路状态机轮询的结果,轮询周期一般是2秒左右。如果你想要实时看到PHY寄存器的原始状态,还得靠mdio直读。别在排查时盯着ethtool那行缓存不放,容易误判时序问题。
8. 写在最后的几条实际经验
我自己这几年跟MDIO打交道的体会是:MDIO这个协议本身简单到让人想当然,但越是简单的接口,在实际工程里越容易在细节上翻车。PHY地址跳线、上拉电阻、MDC频率、复位时序、Linux驱动匹配、设备树配置,每一环都像多米诺骨牌,倒一张全塌。
给各位几个我个人亲测有效的建议:
第一,拿到一块全新板子的第一件事,先用示波器抓MDC有没有波形。没有波形一切都白搭,别急着调驱动。
第二,在调试阶段别怕麻烦,直接把mdio-tools编进你的根文件系统。它会帮你省掉大量“改驱动重新编译烧写”的重复劳动。
第三,多花半小时把要用的PHY芯片手册里的寄存器描述翻一翻,重点记0号、1号、2号、3号这几组标准寄存器。很多看似诡异的现象,读一读寄存器原始值,答案自己就浮出来了。
最后,如果你准备把双PHY共用一个MDIO总线的方案定为量产设计,强烈建议在原理图评审阶段就把PHYAD不同、上拉电阻唯一、复位时序可独立控制这三点列为强制检查项。这些在后期全是“改一次板子浪费时间”的痛点来源。