news 2026/10/4 5:32:36

ZYNQ下KSZ9031 MMD读取失败排查与解决

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ZYNQ下KSZ9031 MMD读取失败排查与解决

做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寄存器要分四步:

  1. 往Clause 22寄存器0x0D写入MMD设备地址,比如0x02表示PMA/PMD。
  2. 往Clause 22寄存器0x0E写入目标MMD寄存器的偏移地址。
  3. 再次往寄存器0x0D写入设备地址,并置位0x4000位,这个位的含义是“关闭自动递增”。如果不置这一位,某些PHY在读了第一个寄存器之后会自动把地址加1,后续再读可能就不在你的目标地址上了。
  4. 从寄存器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都返回0xFFFFPHY地址错、复位未释放、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调试,其实也就这么点事,理顺了之后,后续再改寄存器就顺手多了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/4 5:32:07

开题别再“一个对话框硬扛”:临床中药党写用药安全研究,可以这样搭 AI 工具 [特殊字符]

先把场景说具体:我是临床中药学专业,这次要完成的不是普通课程小论文,而是毕业论文的开题报告。题目暂定为:活血化瘀类中药注射剂与抗血小板药联用的出血风险研究——基于某院 HIS 数据的回顾性分析这个题目很有临床中药学的味道&…

作者头像 李华
网站建设 2026/10/4 5:31:37

JavaEE宠物领养网站毕设实战:JSP+Servlet+MySQL从设计到实现

简介:宠物领养网站是JavaEE方向经典的毕业设计选题,这份论文文档围绕“基于JavaEE下宠物领养网站的设计与实现”展开,定位为计算机专业学生完成毕设的参考范例。文档从课题背景与国内外现状切入,依次介绍需求分析、系统设计、数据…

作者头像 李华
网站建设 2026/10/4 5:30:57

Spring Boot + Lettuce 堆外内存溢出:OutOfDirectMemoryError 复现与分析

一、问题现象 异常信息 io.netty.util.internal.OutOfDirectMemoryError: failed to allocate 2096896 byte(s) of direct memory (used: 7406854, max: 8388608) 业务影响 Redis 操作全部失败接口返回 500应用可能崩溃 二、问题本质 Netty 向操作系统申请的堆外内存超过…

作者头像 李华
网站建设 2026/10/4 5:29:53

基于SpringBoot+Vue的护理知识学习咨询系统设计与实现

选题背景 随着信息技术的迅猛发展与医疗健康领域数字化转型的不断深入,传统护理教育模式正面临前所未有的挑战与变革。护理工作作为医疗体系中的重要组成部分,其专业性、实践性和持续学习需求尤为突出。然而,当前许多医疗机构和护理院校在护理…

作者头像 李华
网站建设 2026/10/4 5:28:07

LabVIEW多通道采集实战:热电偶与TTL转速信号同步方案

做发动机测试台架的时候,遇到最多的一件事就是用LabVIEW配合DAQ设备采集一堆传感器信号。温度要采,转速要采,有时候还得同时采好几个通道,模拟量和数字量混在一起。前阵子刚好把一套多通道采集程序从头到尾捋了一遍,采…

作者头像 李华
网站建设 2026/10/4 5:27:51

TCP/UDP调试工具实战:从连接建立到协议验证的完整指南

简介:TCP&UDPDebug 是一款面向网络编程开发者与运维调试人员的传输层协议测试工具,用于在开发 TCP 服务器、客户端或 UDP 服务时验证连接性能、排查通信异常。工具围绕连接建立与断开、数据收发、丢包检测、顺序校验、错误分析、吞吐量与延迟监控、端…

作者头像 李华