以太网调试现场最常听到的一句话就是"我明明读到了PHY寄存器,怎么值不对"。十次里有八次,问题出在Clause 22和Clause 45这两套管理接口帧格式被混用了。SMI(Station Management Interface)作为MAC与PHY之间的管理通道,底层跑的是MDIO(Management Data Input/Output)两线协议,但上层帧格式分成了两代:Clause 22管的是5位寄存器地址,Clause 45扩展到16位并引入了设备类型(Device Type/MMD)的概念。很多PHY芯片同时支持两套访问方式,寄存器地址空间却各算各的,你拿Clause 22的偏移去读Clause 45的寄存器,读回来的自然是一堆无意义的数。这篇内容就是把这层窗户纸捅破,从帧结构、时序、寄存器映射到实际驱动代码,把两种访问姿势彻底讲清楚,适合正在调PHY驱动、写MDIO工具或者排查链路问题的嵌入式工程师。
1. 先搞清楚SMI和MDIO到底谁管谁
1.1 从MAC和PHY的物理连接说起
一块以太网芯片,MAC侧和PHY侧之间除了数据通道(RGMII、RMII、SGMII这些),还有一条独立的管理通道。这条管理通道就是SMI,它由两根线组成:MDC(Management Data Clock)和MDIO(Management Data Input/Output)。MDC是时钟,由MAC侧(或者叫STA,Station Management Entity)驱动,频率通常在1MHz到12.5MHz之间,标准里规定最高不超过25MHz。MDIO是双向数据线,用来传输命令和读写的数据。
很多人把SMI和MDIO当成一回事,严格来说SMI是接口的整体名称,MDIO是其中数据线的名字,但在工程语境里大家经常混着用,说"MDIO读写"其实就是指通过SMI接口访问PHY寄存器。这个不用太纠结,理解成"MDIO协议"就行。
关键点在于:MDIO协议本身只定义了物理层的时序和帧的传输方式,但帧里每个bit代表什么意思,是由Clause 22和Clause 45分别规定的。这就好比同一条电话线,你可以用老式拨号的方式打电话,也可以用新的信令协议,线没变,但说的话的格式变了。
1.2 为什么会有两套Clause
Clause 22诞生于以太网早期,那时候PHY的寄存器数量很少,5个bit的地址(0到31)完全够用。标准寄存器就那么十几个:BMCR(0x00)、BMSR(0x01)、PHYID1(0x02)、PHYID2(0x03)、ADVERTISE(0x04)等等。这套东西简单直接,一个帧里塞进PHY地址(5bit)、寄存器地址(5bit)、操作码(2bit)和数据(16bit),一共32个bit,干净利落。
但后来以太网速率往上走,1000BASE-T、10GBASE-T出来了,PHY内部需要配置的东西暴增。光是各种均衡器参数、时钟校正、SerDes配置就几百个寄存器,5位地址根本不够用。于是Clause 45被提出来,把寄存器地址扩展到16位,同时引入了Device Type(也叫MMD,MDIO Manageable Device)的概念,用5位Device Type来区分不同的功能块,比如PMA/PMD、WIS、PCS、PHY XS等等。这样地址空间一下子从32个变成了32×65536个,彻底解决了地址不够的问题。
注意:Clause 45并不是要取代Clause 22,两者是并存的。很多支持Clause 45的PHY芯片,前32个寄存器依然用Clause 22的方式访问,因为BMCR、BMSR这些标准寄存器在Clause 45里也有对应的映射,但习惯上还是用Clause 22读。
1.3 两套帧格式的核心差异对比
先把最核心的差异用一张表列出来,后面再逐项拆解。
| 特性 | Clause 22 | Clause 45 |
|---|---|---|
| 寄存器地址位宽 | 5 bit(0-31) | 16 bit(0-65535) |
| Device Type/MMD | 无 | 5 bit(0-31) |
| 帧总长度 | 32 bit | 64 bit(分两帧) |
| 操作码 | 2 bit(10读/01写) | 2 bit(00地址/11读/01写) |
| 是否需要地址帧 | 否 | 是(先发地址帧再发数据帧) |
| 典型用途 | 标准寄存器、PHY ID | 扩展寄存器、SerDes、PCS配置 |
这张表里最关键的一行是"帧总长度"和"是否需要地址帧"。Clause 22一帧搞定,Clause 45要两帧:先发一个地址帧把Device Type和16位寄存器地址写进去,再发一个数据帧做读或写。这个"两帧"机制是很多驱动写错的根源。
2. Clause 22帧结构逐bit拆解
2.1 32个bit怎么分配
Clause 22的一帧是32个bit,从前往后依次是:
- Preamble(32bit):实际上标准里Clause 22的帧前面有32个连续的1作为前导码,但很多MAC控制器会自动处理,软件层面不用管。有些资料把前导码算进去说帧是64bit,这里我们只算有效载荷部分。
- ST(Start of Frame,2bit):固定为01,标志帧开始。
- OP(Operation Code,2bit):10表示读,01表示写。
- PHYAD(PHY Address,5bit):PHY的地址,由硬件引脚或者上拉下拉决定,总线上最多挂32个PHY。
- REGAD(Register Address,5bit):寄存器地址,0到31。
- TA(Turnaround,2bit):读操作时,前一个bit是Z(高阻),后一个bit是0,给PHY时间切换数据线方向;写操作时固定为10。
- DATA(16bit):读出来的或者要写进去的数据。
算一下:2+2+5+5+2+16 = 32,刚好。
2.2 读操作的时序细节
读操作时,MAC先发出ST、OP、PHYAD、REGAD,然后进入TA阶段。TA的第一个bit,MAC释放MDIO线(变成高阻态),PHY这边检测到之后,在第二个bit把线拉低(输出0),然后紧接着输出16位数据。整个过程MDC时钟由MAC持续提供,PHY在MDC的上升沿采样命令,在下降沿输出数据。
这里有个容易踩的坑:TA阶段的方向切换。如果MAC没有正确释放MDIO线,PHY就没法驱动数据线,读回来的全是1或者全是0。有些MAC控制器硬件会自动处理TA,但有些需要软件在发送完地址后把GPIO方向切过来。我在调一颗国产交换芯片的时候就遇到过,它的MDIO引脚是复用的,需要先写一个寄存器把引脚切到MDIO模式,否则读出来永远是0xFFFF。
2.3 写操作的时序细节
写操作简单一些,TA固定为10,MAC发完TA之后直接发16位数据,PHY在MDC上升沿采样。写操作不需要方向切换,因为MDIO线始终由MAC驱动。
写操作的一个常见问题是时序余量。MDC频率如果跑太高,比如超过12.5MHz,而PHY的MDIO建立/保持时间不够,就会写失败。标准里MDIO在MDC上升沿之前要有10ns的建立时间,之后要有10ns的保持时间。实际调试时如果发现写寄存器偶尔失败,先把MDC降频到2.5MHz试试,能稳定工作再往上加。
2.4 一个完整的Clause 22读时序示例
假设PHY地址是0x01,要读寄存器0x02(PHYID1),操作码读是10,那么帧的32个bit是:
ST: 01 OP: 10 PHYAD: 00001 REGAD: 00010 TA: Z0 DATA: 由PHY输出拼起来就是:01 10 00001 00010 Z0 xxxxxxxxxxxxxxxx
用逻辑分析仪抓的时候,你会看到MDC有32个时钟周期,MDIO线上前14个bit是MAC驱动的,然后一个bit高阻,一个bit被PHY拉低,最后16个bit是PHY驱动的数据。如果PHY ID读出来是0x0141之类的值,说明时序对了。
3. Clause 45为什么要发两帧
3.1 地址帧和数据帧的分工
Clause 45的帧是64个bit,但它不是一帧发完,而是分成两个独立的32bit帧:地址帧(Address Frame)和数据帧(Data Frame)。地址帧的作用是把Device Type和16位寄存器地址先"寄存"到PHY内部,数据帧再根据这个地址做读或写。
地址帧的结构是:
- ST:00(注意,Clause 45的ST是00,和Clause 22的01不同)
- OP:00表示地址帧
- PHYAD:5bit
- DEVAD(Device Address):5bit,就是Device Type
- TA:10
- DATA:16bit的寄存器地址
数据帧的结构是:
- ST:00
- OP:11表示读,01表示写
- PHYAD:5bit
- DEVAD:5bit
- TA:读时Z0,写时10
- DATA:16bit数据
关键点:地址帧和数据帧的PHYAD和DEVAD必须一致,否则PHY会忽略。而且地址帧发完之后,PHY内部会记住这个地址,直到下一个地址帧到来。所以如果你要连续读同一个寄存器的不同位置,理论上可以只发一次地址帧,后面跟多个数据帧。但实际驱动里为了简单,通常每次读写都重新发地址帧。
3.2 Device Type到底有哪些
Device Type是Clause 45引入的核心概念,5个bit,标准里定义了一部分:
| Device Type值 | 名称 | 用途 |
|---|---|---|
| 0x00 | PMA/PMD | 物理介质附加/相关 |
| 0x01 | WIS | WAN接口子层 |
| 0x02 | PCS | 物理编码子层 |
| 0x03 | PHY XS | XSBI相关 |
| 0x04 | DTE XS | DTE扩展 |
| 0x05 | TC | 测试相关 |
| 0x06 | Clause 22扩展 | 映射Clause 22寄存器 |
| 0x1E | 厂商自定义 | 各厂商自己用 |
注意0x06这个Device Type,它把Clause 22的寄存器映射到了Clause 45的地址空间里。也就是说,你可以用Clause 45的方式去读Clause 22的寄存器,Device Type填0x06,寄存器地址填0x00到0x1F。这个设计是为了兼容,但实际用起来容易混淆,因为同一个寄存器有两个地址。
3.3 两帧之间的间隔要求
Clause 45标准里规定,地址帧和数据帧之间不能有太长的间隔,否则PHY可能会把地址丢掉。具体来说,两帧之间的空闲时间不能超过MDC的若干个周期,不同PHY实现不一样。稳妥的做法是地址帧发完紧接着就发数据帧,中间不要插入其他操作。
我在调一颗Marvell的PHY时遇到过这个问题:驱动里在地址帧和数据帧之间加了一个延时函数,结果读出来的数据全是0。后来把延时去掉,改成连续发送,立刻就正常了。所以如果你发现Clause 45读出来的数据不对,先检查两帧之间有没有多余的延时或者调度。
4. 寄存器地址映射:同一个功能两套地址
4.1 标准寄存器在Clause 22和45里的对应关系
前面提到Device Type 0x06可以把Clause 22寄存器映射到Clause 45空间,但即使不用0x06,很多寄存器在Clause 45的各个Device Type里也有独立的定义。比如BMCR(Basic Mode Control Register),在Clause 22里是寄存器0x00,在Clause 45的PMA/PMD(Device Type 0x00)里是寄存器0x0000,在PCS(Device Type 0x02)里也有对应的控制寄存器。
这就导致一个现象:同一个功能,你可以通过不同的路径去配置。比如设置自协商,你可以写Clause 22的0x00寄存器bit12,也可以写Clause 45 PMA/PMD的0x0000寄存器bit12。两条路都能走通,但如果你只改了一边,另一边没同步,读状态的时候就会迷惑。
提示:调试时先确认PHY的数据手册里,某个功能到底映射在哪个Device Type的哪个寄存器。不要想当然地认为Clause 22的0x00就等于Clause 45的0x0000,有些PHY的映射关系是错位的。
4.2 厂商扩展寄存器的访问方式
厂商扩展寄存器是Clause 45的主战场。比如一颗支持2.5G/5G/10G的PHY,它的SerDes配置、均衡器系数、时钟校正参数都在厂商自定义的Device Type里,地址动辄0x8000以上。这些寄存器用Clause 22根本访问不到,必须用Clause 45。
访问厂商扩展寄存器时,Device Type通常填0x1E或者厂商指定的值。具体填多少,看数据手册。有些PHY的Device Type是硬件固定的,有些可以通过寄存器配置。我见过一颗PHY,它的厂商扩展Device Type默认是0x1E,但可以通过写某个Clause 22寄存器改成0x1F,这种设计就是为了避免总线上多个PHY的Device Type冲突。
4.3 双网口共用一个MDIO时的地址规划
双网口设备很常见,两个PHY挂同一条MDIO总线。这时候PHYAD就关键了,两个PHY必须有不同的PHYAD,否则地址冲突。PHYAD通常由硬件引脚决定,比如PHY0的PHYAD引脚接地,PHY1的PHYAD引脚上拉,这样PHY0地址是0,PHY1地址是1。
但有些板子设计的时候没注意,两个PHY的PHYAD引脚都接地了,结果两个PHY地址都是0。这时候怎么办?有两个办法:一是改硬件,把其中一个PHY的PHYAD引脚改掉;二是用软件,有些PHY支持通过写某个寄存器来改PHYAD,但这个方法不通用,而且改完之后如果掉电重启就丢了。
还有一种情况是PHY芯片背靠背(back-to-back)连接,两个PHY之间用SGMII直连,这时候MDIO总线可能只连到其中一个PHY,另一个PHY通过内部寄存器间接访问。这种场景下,Clause 45的Device Type就派上用场了,因为可以通过不同的Device Type来区分访问的是哪个内部模块。
5. 驱动代码里怎么区分两种访问
5.1 Linux内核的mdio接口
Linux内核里访问PHY寄存器用的是mdiobus_read和mdiobus_write这两个函数,它们最终会调用MDIO总线的读写。但这两个函数默认走的是Clause 22。如果要走Clause 45,需要用mdiobus_read_nested或者直接调用总线驱动的read_c45/write_c45回调。
在较新的内核里,PHY驱动框架提供了phy_read_mmd和phy_write_mmd这两个函数,专门用来访问Clause 45的MMD寄存器。函数原型大概是:
int phy_read_mmd(struct phy_device *phydev, int devad, u32 regnum); int phy_write_mmd(struct phy_device *phydev, int devad, u32 regnum, u16 val);devad就是Device Type,regnum是16位寄存器地址。这两个函数内部会判断PHY是否支持Clause 45,如果支持就走Clause 45帧格式,不支持就尝试用Clause 22的间接访问方式(有些PHY通过Clause 22的0x0D和0x0E寄存器来间接访问扩展寄存器)。
5.2 自己写MDIO工具时的注意事项
如果你要自己写一个MDIO读写工具,比如在FPGA或者单片机上模拟MDIO时序,有几个点必须注意:
第一,MDC频率不要超过PHY手册规定的最大值。大部分PHY支持到12.5MHz,但有些只支持到2.5MHz。保险起见,先用2.5MHz调通,再往上加。
第二,Clause 22和Clause 45的ST和OP编码不同,不要搞混。Clause 22的ST是01,Clause 45的ST是00。Clause 22的读OP是10,Clause 45的读OP是11。这些编码在标准里有明确定义,写错一个bit就读不出来。
第三,TA阶段的处理。Clause 22读的时候TA是Z0,Clause 45读的时候也是Z0,但Clause 45的地址帧TA是10。写操作时Clause 22和Clause 45的TA都是10。
第四,Clause 45的两帧之间不要插入其他MDIO操作,否则地址会丢。
5.3 一个实际的调试案例
之前调一颗10G PHY,用Clause 22读PHYID能读到,说明MDIO时序没问题。但读厂商扩展寄存器时,用Clause 45怎么读都是0。排查过程是这样的:
先确认Device Type对不对,查手册发现厂商扩展的Device Type是0x1E,没错。然后确认寄存器地址,手册上写的是0x8000,也没错。接着用逻辑分析仪抓MDIO波形,发现地址帧发完之后,数据帧的OP是11(读),但PHY返回的数据全是0。
后来仔细看波形,发现地址帧和数据帧之间的间隔太长了,有几十个MDC周期。原因是驱动里在发完地址帧后调用了一个打印函数,打印函数耗时太长。把打印去掉,改成连续发送,立刻就读到了正确的值。
这个案例说明,Clause 45对两帧之间的时序很敏感,调试时尽量不要在中间插入任何可能引入延时的操作。
6. 常见误区与排查清单
6.1 误区一:以为Clause 45是Clause 22的超集
很多人以为Clause 45兼容Clause 22,所以直接用Clause 45的方式去读Clause 22的寄存器就行。实际上不是这样。Clause 45的Device Type 0x06确实映射了Clause 22的寄存器,但并不是所有PHY都实现了这个映射。有些PHY只支持Clause 22,你发Clause 45的帧它根本不认。所以驱动里必须先读PHYID,判断PHY型号,再决定用哪种方式。
6.2 误区二:寄存器地址直接照搬
Clause 22的寄存器0x00和Clause 45 PMA/PMD的0x0000虽然功能相似,但bit定义可能不同。比如BMCR的bit12在Clause 22里是自协商使能,在Clause 45的PMA/PMD里可能对应的是别的功能。写之前一定要查手册,不要照搬。
6.3 误区三:忽略TA阶段的方向切换
前面说过,读操作时TA阶段MAC要释放MDIO线。如果用的是GPIO模拟MDIO,必须在TA的第一个bit把GPIO切为输入,否则PHY驱动不了数据线。这个坑在单片机或者FPGA上很常见,Linux内核的MDIO控制器一般硬件自动处理,但GPIO模拟的bitbang驱动需要软件处理。
6.4 排查清单
遇到MDIO读写问题时,按这个顺序排查:
- 确认MDC频率是否在PHY支持范围内,先降到2.5MHz试试。
- 确认PHYAD是否正确,总线上有没有地址冲突。
- 确认用的是Clause 22还是Clause 45,ST和OP编码对不对。
- 如果是Clause 45,确认Device Type和寄存器地址是否正确。
- 用逻辑分析仪抓波形,看TA阶段方向切换是否正常。
- 检查两帧之间有没有多余延时。
- 确认PHY是否支持Clause 45,有些低端PHY只支持Clause 22。
7. 几个实战中总结的小技巧
第一个技巧:读PHYID是判断MDIO通不通的最快方法。PHYID1和PHYID2是Clause 22的寄存器0x02和0x03,几乎所有PHY都支持。如果这两个读出来是0x0000或者0xFFFF,说明MDIO时序有问题,先别急着调Clause 45。
第二个技巧:用Clause 22的0x0D和0x0E寄存器间接访问扩展寄存器。有些PHY虽然不支持Clause 45,但提供了间接访问机制:先写0x0D寄存器设置地址,再写0x0E寄存器设置数据,然后读0x0E寄存器拿结果。这种方式速度慢,但兼容性好。
第三个技巧:MDC频率不要一开始就跑最高。先用1MHz或者2.5MHz调通,确认读写都正常了,再逐步提高频率。很多时序问题在低频下不会暴露,高频下才出现,所以低频调通不代表高频没问题,但低频都调不通肯定有问题。
第四个技巧:如果总线上挂了多个PHY,读PHYID的时候要逐个PHYAD去读,确认每个PHY都能正确响应。有时候两个PHY的PHYAD冲突了,读出来的ID会不对,这时候要检查硬件引脚。
第五个技巧:Clause 45的地址帧发完之后,如果紧接着发数据帧,中间不要有任何函数调用或者打印,最好用汇编或者直接操作寄存器的方式连续发送。我在FPGA上实现MDIO控制器时,地址帧和数据帧是在同一个状态机里连续发出的,中间没有间隙,这样最稳。
8. 从寄存器访问到链路调试的延伸
搞懂了Clause 22和Clause 45的区别,只是PHY调试的第一步。实际链路调试中,你还需要知道怎么读链路状态、怎么配置自协商、怎么查看误码率。这些功能分布在不同的寄存器里,有些用Clause 22读,有些用Clause 45读。
比如链路状态,Clause 22的BMSR(0x01)bit2是Link Status,读这个bit就能知道链路通没通。但自协商完成状态在BMSR的bit5,自协商能力在ADVERTISE寄存器(0x04)。如果要看更详细的链路参数,比如均衡器状态、信噪比,就得用Clause 45去读厂商扩展寄存器。
还有一个常见的需求是配置PHY的工作模式,比如强制1000M全双工或者自协商。这些配置在Clause 22的BMCR(0x00)里就能搞定。但如果要配置SerDes的预加重、均衡器系数,就必须用Clause 45。
所以实际调试时,通常是两种方式混着用:标准寄存器用Clause 22,扩展寄存器用Clause 45。驱动里要封装好两套读写函数,根据寄存器地址范围自动选择用哪种方式。Linux内核的phy_read和phy_write默认走Clause 22,phy_read_mmd和phy_write_mmd走Clause 45,用的时候注意区分就行。
最后说一个我踩过的坑:有些PHY的Clause 45寄存器读出来是16位的,但实际有效数据只有低8位或者低12位,高位是保留的。如果你直接把16位数据当成有效值用,可能会得到错误的结果。读之前先看手册里寄存器的位定义,确认哪些bit是有效的。这个坑在调SerDes参数时特别容易踩,因为SerDes寄存器的位定义往往很复杂,不同bit段代表不同的参数,读出来之后需要做位域提取才能用。