做ZYNQ网络核的时候,我最常被问的一句话就是:“ksz9031 mmd读取不了,到底是PHY坏了还是我写错了?” 这句话我听得耳朵起茧。KSZ9031RNX这颗PHY在ZYNQ板卡上实在太常见了,配合LwIP做千兆以太网,几乎人手一块。而MMD寄存器又偏偏是调RGMII延时、查Link状态、看协商结果的必经之路,读不到那一步,后面所有调试都像瞎子摸象。
这篇内容不是什么教科书,是我实际调板子过程中踩出来的结论。如果你也在ZYNQ裸机或Linux下配LwIP,面对KSZ9031RNX时发现MMD读出来全是0xFFFF,或者某些工具里直接报错,那么这篇文章大概率能帮你少走半天弯路。我会把MMD的间接访问原理、读不到的常见原因,以及一套可以直接抄的读取代码都写清楚。
1. 先把这套硬件链路认全:ZYNQ、LwIP和KSZ9031RNX是什么关系
1.1 为什么ZYNQ平台特别爱用KSZ9031RNX
ZYNQ的PS端自带GEM(Gigabit Ethernet MAC)控制器,配合外部PHY芯片就能出网口。KSZ9031RNX是Microchip(原Micrel)的10/100/1000M三速PHY,支持RGMII接口,芯片内部有灵活的时序调校寄存器,非常适合接在ZYNQ这样的FPGA+ARM平台后面。
典型的接法是:ZYNQ PS的GEM0或GEM1通过MIO或EMIO引出RGMII信号,包括TXD、RXD、TX_CTL、RX_CTL、GTX_CLK、RXC等;另外还有一对MDC/MDIO管理总线,专门用来读PHY的状态寄存器和配置PHY的工作模式。
LwIP是跑在ARM核上的协议栈,它本身不直接操作PHY,而是通过Xilinx提供的XEmacPs驱动访问GEM,GEM再通过MDIO总线去访问KSZ9031的寄存器。所以“ksz9031 mmd读取不了”这个问题,通常不是LwIP本身的锅,而是GEM的MDIO通路或者PHY的间接访问序列出了问题。
1.2 MMD寄存器是什么,为什么直接读不到
KSZ9031RNX遵循IEEE 802.3 Clause 22管理接口,普通的PHY寄存器一共32个,地址范围0x00到0x1F。像控制寄存器、状态寄存器、PHY ID、自协商能力寄存器这些,都在这32个寄存器里。
但现代千兆PHY还有很多高级功能,比如RGMII时钟延时、链路质量监控、中断管理、1000BASE-T状态等,这些寄存器数量远超32个。于是芯片引入了MMD(MDIO Manageable Device)的概念,也就是Clause 45定义的地址空间。MMD下每个Device地址对应一个大的寄存器数组,总共有65536个寄存器位,想靠Clause 22那一条总线直接访问是不可能的。
KSZ9031的做法是在Clause 22的寄存器空间里开两个窗口寄存器:一个写设备地址,一个写目标寄存器地址,然后再通过数据窗口读或写真正的MMD寄存器。这个机制本身不难,难就难在不少人把窗口地址记错了。
我见过最多的坑,是拿着别家PHY的习惯写0x1D/0x1E去访问。在Linux内核的micrel驱动里,KSZ9031走的是0x0D/0x0E这套间接窗口,而不是0x1D/0x1E。如果你用习惯性写法去读,结果当然是读不到,或者读出来全是对不上的值。
2. KSZ9031 MMD间接访问的正确姿势
2.1 理解四步读法与NOINCR位
在写代码之前,先把这个间接访问的时序逻辑讲透。内核里phy_read_mmd_indirect这个函数就是标准的课本答案,我建议你直接抄它的逻辑。
读MMD寄存器要分四步:
- 往Clause 22寄存器
0x0D写入MMD设备地址,比如0x02表示PMA/PMD。 - 往Clause 22寄存器
0x0E写入目标MMD寄存器的偏移地址。 - 再次往寄存器
0x0D写入设备地址,并置位0x4000位,这个位的含义是“关闭自动递增”。如果不置这一位,某些PHY在读了第一个寄存器之后会自动把地址加1,后续再读可能就不在你的目标地址上了。 - 从寄存器
0x0E读回16位数据。
写MMD寄存器也是类似的套路,前三步一样,最后一步是往0x0E写入要写的16位数据。
这里我多加一句:0x4000这个位虽然名字叫NOINCR,但它在整个访问流程里非常关键,尤其对KSZ9031这种“规矩比较多”的PHY。以前我在调一个i.MX平台的KSZ9031时,因为软件工程师觉得多写一次寄存器完全多余,就把第三步省略了,结果读出来的寄存器值时对时错。后来把这一步补上,问题立刻消失。所以在ZYNQ下面写代码,也别自作聪明省这一拍。
2.2 在ZYNQ裸机上用XEmacPs读写MMD
ZYNQ裸机环境下,最直接的方式就是用Xilinx SDK自带的XEmacPs_MdioWrite和XEmacPs_MdioRead接口。下面这段代码我在Zynq-7000上实测可用,PHY地址按你板子的实际strap配置填,常见的是0x07,也有一批板子用0x00或0x04,一定要看原理图。
#include "xemacps.h" #include "xemacps_hw.h" #include "xil_printf.h" static XEmacPs g_EmacPs; int mdio_mmd_read(u32 phy_addr, u32 dev_addr, u32 reg_addr, u16 *value) { u16 tmp; if (XEmacPs_MdioWrite(&g_EmacPs, phy_addr, 0x0D, (u16)dev_addr) != XST_SUCCESS) { return -1; } if (XEmacPs_MdioWrite(&g_EmacPs, phy_addr, 0x0E, (u16)reg_addr) != XST_SUCCESS) { return -1; } if (XEmacPs_MdioWrite(&g_EmacPs, phy_addr, 0x0D, (u16)(dev_addr | 0x4000)) != XST_SUCCESS) { return -1; } if (XEmacPs_MdioRead(&g_EmacPs, phy_addr, 0x0E, &tmp) != XST_SUCCESS) { return -1; } *value = tmp; return 0; } int mdio_mmd_write(u32 phy_addr, u32 dev_addr, u32 reg_addr, u16 data) { if (XEmacPs_MdioWrite(&g_EmacPs, phy_addr, 0x0D, (u16)dev_addr) != XST_SUCCESS) { return -1; } if (XEmacPs_MdioWrite(&g_EmacPs, phy_addr, 0x0E, (u16)reg_addr) != XST_SUCCESS) { return -1; } if (XEmacPs_MdioWrite(&g_EmacPs, phy_addr, 0x0D, (u16)(dev_addr | 0x4000)) != XST_SUCCESS) { return -1; } if (XEmacPs_MdioWrite(&g_EmacPs, phy_addr, 0x0E, data) != XST_SUCCESS) { return -1; } return 0; }调用前先确认g_EmacPs已经通过XEmacPs_CfgInitialize初始化,而且GEM时钟、MDIO引脚复用都已经配好。用的时候先读PHY ID验证总线:
u16 id1 = 0, id2 = 0; XEmacPs_MdioRead(&g_EmacPs, 0x07, 0x02, &id1); XEmacPs_MdioRead(&g_EmacPs, 0x07, 0x03, &id2); xil_printf("PHY ID1 = 0x%04X, ID2 = 0x%04X\r\n", id1, id2);KSZ9031RNX的正常值是0x0022和0x1622。如果你看到这两个值,起码说明MDIO管理总线通了。接下来再读MMD,比如读PMA/PMD设备下的控制寄存器:
u16 mmd_val = 0; if (mdio_mmd_read(0x07, 0x02, 0x0004, &mmd_val) == 0) { xil_printf("MMD 02:0004 = 0x%04X\r\n", mmd_val); } else { xil_printf("MMD read failed\r\n"); }如果能打出非0xFFFF的值,就说明这套间接访问流程完全没问题,剩下的就是寄存器偏移地址找得对不对。
2.3 在Linux下怎么间接验证
如果你在ZYNQ上跑的是Linux,调试MMD会更方便。内核自带的phy_read_mmd之类的接口已经帮你封装好了,驱动里直接调就行。用命令行工具的话,有的板子没有装mdio-tools,就得先确认工具的命令格式。
我对mdio-tools的版本印象是:不同版本参数顺序略有差异,但基本都会有一个mdio子命令用来读PHY寄存器,有mmd子命令用来读MMD寄存器。如果你手头的工具版本比较特殊,建议先用--help查一下,不要凭记忆敲。
不管用哪种方式,Linux下如果你要对寄存器做读写,最好在PHY驱动完成初始化但网口没有频繁访问PHY的时候操作,或者直接写一个小的内核模块调用phy_read_mmd。千万避免在用户态用GPIO模拟MDIO,同时内核驱动又在轮询PHY状态,两条管理通路同时访问同一颗PHY,很容易让别人以为MMD读不到。
3. 为什么你一步一步照着写还是读不到
3.1 先做基础寄存器排查,别一上来就怪MMD
我调试的时候有个习惯,叫“从简单问题往复杂问题推”。遇到MMD读不到,先不碰MMD,而是先读PHY的基本寄存器。如果0x02/0x03这两个PHY ID寄存器都读不到,那问题大概率不是MMD访问序列,而是MDIO总线根本没通。
可能的原因包括:
- PHY地址不对,MDIO总线上根本没有设备在回应这个地址。
- MDC/MDIO引脚复用没配好,或者被EMIO接到了FPGA侧但FPGA里没做约束。
- PHY的复位引脚一直被拉低,芯片还没启动。
- 上拉电阻缺失,MDIO数据线在空闲时无法维持高电平。
- MDC时钟频率太高,PHY跟不上,读回的数据不稳定。
解决顺序也很固定:先看原理图确认PHY地址,再量复位引脚和电源,接着用示波器抓MDC波形,最后再用软件一次一次减小MDC分频因子。我遇到过一块板子,MDC是从PL侧用户逻辑引出来的,频率跑到了几十兆,PHY基本处于“你说什么我听不清”的状态,基础寄存器偶尔能读对,偶尔全是0xFFFF,非常有迷惑性。
3.2 基础寄存器能读,MMD却读不到,检查窗口地址
如果基础寄存器能稳定读到0x0022/0x1622,那就说明MDC、MDIO、PHY全部在线。此时MMD读不到,最大的嫌疑就是间接访问窗口地址写错。
很多人在这一步掏出厂商提供的KSZ9031数据手册,看到里面写着0x1D和0x1E,就直接用这两个地址去操作。但你需要非常仔细地区分:这是Clause 22的物理寄存器地址,还是某种“用户接口”说明。Linux内核实际使用的间接访问窗口是0x0D和0x0E,这是大量发行版和BSP跑过的路线,优先相信内核的写法。
如果你自己写工具,把这个地址改对,问题通常立刻解决。
另外还有一种情况,是寄存器偏移根本没传对。MMD寄存器地址是16位的,比如0x0004、0x0008,这和Clause 22里的寄存器地址不一样。不少人偷懒,把偏移写成了0x04,代码里的类型又是u8,一进函数就被截断了,当然读不到。写MMD工具时,所有传给间接窗口的地址都得用16位无符号类型来存和传。
3.3 读出来是0xFFFF,到底算什么
MDIO读设备地址不存在的PHY时,数据线如果没有任何设备应答,读回来的数据线会在空闲位上被上拉电阻拉高,最终得到0xFFFF。所以看到0xFFFF,先不要急着怀疑是所有寄存器内容都是这个值,很可能就是“总线没应答”。
但反过来,如果MMD目标寄存器本身不允许读,或者PHY还在复位中,也可能返回异常值。我试过把PHY复位引脚接在GPIO上,软件复位释放得太快,上电后马上读MMD,前几十毫秒读到的全是0xFFFF。等把复位释放时间从10ms延长到100ms之后,一切正常。碰到0xFFFF,建议在代码里复位后加延时再读,不要指望PHY上电瞬间就能给你稳定数据。
4. 常见问题与排查技巧实录
4.1 我整理的一张排查速查表
为了方便现场调试,我把碰到过的问题整理成了一张表。每一种现象我都尽量写了最直接的判断和处理方式,不一定覆盖所有板子,但遇到类似情况可以先按这个方向试。
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| 读0x02/0x03都返回0xFFFF | PHY地址错、复位未释放、MDIO上拉缺失 | 查原理图PHYAD strap值;量复位脚;检查MDIO上拉电阻;抓MDC波形 |
| 基础寄存器能读,MMD读全是0xFFFF | 间接访问窗口地址写错,或用0x1D/0x1E替代了0x0D/0x0E | 改回0x0D/0x0E,按内核micrel驱动流程操作 |
| MMD读值偶发错位,时好时坏 | 漏写NOINCR位,或地址被自动递增 | 第二步之后补写0x0D = dev_addr |
| MDC时钟太高,读到乱码 | GEM的MDC分频配置过高 | 减小MDC时钟,最好降到2.5MHz附近再试 |
| 复位后立刻读MMD失败 | PHY启动未完成 | 复位拉高后延时50ms以上再操作 |
| 读回的值全为0 | 目标寄存器本身为0,或读写地址错误 | 数据手册核对MMD设备号和寄存器偏移;读一个已知非零寄存器验证 |
这张表看起来简单,但每一条背后都对应真实踩坑经历。尤其是第一条,很多新手拿到板子就写代码,结果PHY地址根本没对上,后面所有操作都白做。
4.2 为什么LwIP跑着跑着就“抢”了MDIO
在ZYNQ裸机上用LwIP时,XEmacPs驱动会周期性轮询PHY的Link状态。这个轮询本身也会通过MDIO总线去读PHY寄存器。如果此时你的调试代码也在读MMD,两边如果没有任何同步机制,就可能出现同一时刻两条逻辑都在操作MDIO。
有人可能会问:裸机不是单线程吗?但你的定时器中断里完全可能跑着LwIP内部函数,主循环里又跑着自己的MDO工具,两者一交错,时序就被打乱。我就在一个工程里遇到过:主程序循环里每100ms读一次MMD,LwIP每500ms查一次Link,看起来互不干扰,实际在某个特定相位点上,MMD读出来的值偶尔会缺一两位。
解决办法有两种:要么在读取MMD时暂时关掉PHY轮询中断,要么用同一个互斥锁保护所有MDIO访问。如果你只是自己在调试台上敲命令,那影响不大;但如果要把这段代码固化到产品里,必须做好临界区保护。
4.3 板子默认能出网,不代表MMD就没必要调
有一种很迷惑的情况:LwIP明明能Ping通,网口也能跑千兆,但MMD读起来还是不对。这时你会觉得,反正能跑,MMD读不到也没关系。但一旦你发现吞吐率上不去,或者长时间传输出现CRC错误,回头还是要来调MMD。
KSZ9031有个很常见的需求是调整RGMII的TX/RX时钟延时。一些板子通过外围电阻已经做了默认配置,软件不用改;但也有板子为了做兼容,把配置留给软件。这个时候如果你读不了MMD,就没法判断当前的时钟相位到底偏了多少,千兆稳定性就只能靠运气。
调试这类问题时,不要只盯着Link up没up,要看寄存器里自协商结果和各项状态标志。MMD读通了,你才有能力回答“为什么偶尔掉线”“为什么千兆过不了温循”这类问题。
5. 个人经验:我后来是怎么定位这个问题的
5.1 一次从现象到根因的完整复盘
我最早碰到“ksz9031 mmd读取不了”是在一块自研ZYNQ板卡上,用的是PS端GEM0,PHY地址拨码开关设成0x07。板子回来后,LwIP初始化正常,但我想读MMD的寄存器做量产测试,发现读0x02:0x0004时返回0xFFFF。
第一反应是板子焊接问题,量了MDC和MDIO波形,都在,PHY电源也正常。然后我用示波器观察MDIO数据线上的应答位,发现每次操作0x0D时,PHY都有应答,但操作0x0E时偶尔没有。这就很奇怪了。
后来我翻内核驱动代码,发现我没有写第三步的dev_addr | 0x4000。我原来的代码用了一个简化版,只写两次间接窗口寄存器,然后就急着去读数据。补上NOINCR这一拍之后,MMD读取稳定通过,问题消失。这个案例让我后来在所有平台调PHY时,都先以内核里的通用实现为对照,不自己发明花活。
5.2 给新人的一个小建议
如果你现在还没定位到问题,我建议先输出一遍“最小验证程序”:只初始化GEM,读PHY ID,读一个MMD已知寄存器,打印出来。不要在这段程序里启动LwIP,不要开任何PHY轮询定时器,也不要连接剩余功能。把问题范围缩到最小,才最好查。
如果你用的PHY地址不是0x07,可以试试手册里的通用PHY地址0x00。不过最靠谱的还是看原理图上PHYAD引脚接高接低,这部分代码不需要写得多复杂,但基础判断一定要扎实。
最后再分享一个小技巧:读MMD的时候,可以同时读两遍同一个寄存器,如果两次结果不一致,说明时序或总线有问题,别急着信其中某一次的数据。这个习惯帮我挡掉了很多回数据抖动造成的假问题。KSZ9031的MMD调试,其实也就这么点事,理顺了之后,后续再改寄存器就顺手多了。