ZYNQ上KSZ9031 PHY调试:MDIO与MMD读取0xFFFF的排查实践
最近在一块 ZYNQ-7000 板卡上调千兆以太网PS 端 GEM0 外接的是 Microchip 的 KSZ9031RNX软件层面用裸机 LwIP 跑 TCP/IP 协议栈。原本觉得这套组合很常见结果调试日志直接停在PHY init failed用XEmacPs_PhyRead读 PHY ID 寄存器返回的全是0xFFFF想进一步通过 MMD 方式读 KSZ9031 的扩展寄存器也是一样读不到。前前后后折腾了两天多最后把 MDIO 通路、PHY 复位时序、MMD 访问序列拆开排查才确认问题根源。这篇文章就把这趟排障过程完整记录下来。内容包括 KSZ9031RNX 的 MMD 读取原理、ZYNQ GEM 在裸机和 Linux 下的 MDIO 配置、实用的驱动代码以及几个常见但容易被忽视的坑。适合正在用 ZYNQ 跑以太网、或者遇到 PHY 寄存器访问异常的人不一定能直接抄作业但至少能帮你把排查范围缩小一大半。1. 问题现场LwIP 起不来MMD 读什么都是 0xFFFF1.1 最典型的失败现象先说现象。裸机工程用的是 Xilinx SDK/Vitis 自带的 LwIP 模板启动日志走到以太网驱动初始化时串口打印ERROR: PHY init failed或者PHY reset timeout。这时候如果直接在代码里加一个读 PHY 寄存器的动作比如读0x02和0x03来获取 PHY ID读回来的值通常是0xFFFF或者0x0000。如果是0xFFFF基本可以判定 PHY 没有应答 MDIO 请求如果是0x0000要么是 PHY 地址不对要么是驱动读到了错误的总线状态。我当时还专门写了一个 MMD 读取函数严格按照 KSZ9031 数据手册里说的通过0x0D和0x0E这两个 Clause 22 寄存器来访问 MMD 扩展寄存器。逻辑上自认为没问题结果读出来的数据依然是0xFFFF。一开始我还怀疑是不是 KSZ9031RNX 这个后缀的芯片 MMD 映射和普通版本不一样后来又查资料又抓波形最后才意识到MMD 读不到往往是上层表现真正的问题在 MDIO 底层通路没打通。1.2 为什么一定要读 MMD很多人问Clause 22 的基础寄存器不是够用了吗为什么非要折腾 MMD这里要解释一下。传统的快速以太网 PHY比如常见的 10/100M PHY寄存器空间只需要 32 个 16 位寄存器就够了IEEE 802.3 的 Clause 22 管理帧可以直接覆盖。但是到了千兆 PHY尤其是支持 1000BASE-T 的 PHY协商逻辑、主从时钟配置、线缆诊断、信号质量、温度检测这些功能都放在扩展寄存器空间里。这个扩展空间使用 Clause 45 定义的 MMD 机制来管理MMD 全称是 MDIO Manageable Device每个 MMD 设备有独立的设备地址和最多 65536 个寄存器。KSZ9031RNX 是 AutoMDIX 千兆 PHY它的很多关键信息比如 1000BASE-T 协商状态、主从模式、链路质量、RGMII 输入输出时钟偏移调整等都不在 Clause 22 的基础 0x00-0x1F 寄存器里。软件要精确控制 PHY 行为必须通过 MMD 读取或者写入。这也是我坚持要打通 MMD 读取的原因不把这条通路搞定后面做 RGMII 时序补偿和链路调试都无从谈起。2. 先把基础打牢MDIO 通路、PHY 地址和复位时序2.1 上电第一步确认 PHY 地址和复位很多人读不到 PHY 寄存器第一反应是去改软件里的 PHY 地址但其实PHY 地址是硬件 strapping 决定的。KSZ9031 的 PHYAD 引脚在芯片复位时会锁定电平常见地址是0x07、0x00、0x04等。我用过几个不同开发板有的用0x07有的用0x04没有统一标准所以第一件事永远是看原理图确认 PHYAD[2:0] 接的上拉还是下拉。除了地址还有一个特别容易被忽略的问题PHY 复位释放时间。KSZ9031 的硬件复位引脚需要保持低电平一段时间释放后还需要等待内部初始化完成。有的板子用 ZYNQ 的 MIO 或者 GPIO 控制 PHY 复位如果软件在初始化以太网时没有先释放复位或者释放后没有延时足够长PHY 根本还没准备好MDIO 读回去自然全是0xFFFF。我当时甚至遇到过更隐蔽的情况PHY 的复位引脚和 GEM 的复位共用了同一个信号GEM 复位释放后立即可用但 PHY 还在起振导致驱动在初始化前几毫秒去读 PHY ID读不到就报错。后来在初始化流程最开始加了至少 10ms 的延时问题立刻缓解。如果把延时拉到 50ms 到 100ms基本可以覆盖绝大多数 PHY 的上电时序。2.2 MDC/MDIO 时钟分频读不到的先查这里ZYNQ GEM 的 MDIO 接口由两个信号组成MDC管理时钟和 MDIO管理数据。MDC 的频率不能太离谱IEEE 802.3 标准建议最大不超过 2.5MHz 左右。ZYNQ 的 GEM 控制器内部有一个时钟分频配置项通过XEmacPs_SetMdioDivider设置常见取值有XEMACPS_MDIO_DIV_16、XEMACPS_MDIO_DIV_32、XEMACPS_MDIO_DIV_64等。这个分频值选错表现非常奇怪。分频太小MDC 频率过高PHY 识别不过来寄存器读出来乱跳分频太大MDIO 事务太慢驱动容易超时。我当时的板卡 GEM 参考时钟是 125MHz试过用 16 分频MDC 大概 7.8MHzPHY 基本不响应换到 64 分频MDC 降到 1.95MHzPHY 才正常。这个分频值不能只看 CPU 主频一定要结合 GEM 的实际输入时钟来计算或者直接用示波器测 MDC 引脚确认波形频率符合 PHY 手册要求。另外要注意如果 MDIO 是通过 EMIO 引出到 PL 再接 PHY 的MDC 和 MDIO 在约束文件里必须有正确的管脚分配和 I/O 标准配置。很多人在 MIO 上跑没问题切到 EMIO 就死活读不到原因就是 PL 侧的引脚约束没写对信号根本没到 PHY。2.3 RGMII/SGMII 的接口模式别选错ZYNQ GEM 支持多种外部接口模式MII、GMII、RGMII、SGMII 等。KSZ9031RNX 是 RGMII 接口的 PHY所以 GEM 需要配置成 RGMII 模式并且确认连接的是 GEM0 还是 GEM1以及对应的 MIO/EMIO bank 电压。这里想多说一句和热词相关的话题有人用 ZYNQ PL 侧的 SGMII IP 核去接外部 PHY 芯片结果以太网起不来。这种情况十有八九是SGMII IP 核和 PHY 芯片的角色配置对不上。SGMII IP 核如果作为 MAC 侧使用需要配置成 MAC 模式如果作为 PHY 侧使用需要配置成 PHY 模式。很多人拿到 IP 核默认配置直接烧PHY 芯片也在按 PHY 模式工作两边都以为对方是 MAC协商永远不可能成功。KSZ9031 本身没有 SGMII 接口但同样的道理适用于所有带 SGMII 的 PHY比如 Marvell 88E1512 这类。所以遇到链路起不来先冷静确认接口模式再动软件。回到 RGMII还有一个常见的时序问题RGMII 要求时钟边沿对齐但不同 PHY 对内部延迟的默认值不一样。KSZ9031 支持通过 MMD 寄存器调整 RXD 和 RX_CLK 的相位关系这就是调试千兆链路时经常用到的 RX delay 和 TX delay 配置。如果这部分没配好可能表现为能协商上千兆但 Ping 不通或者数据包 CRC 错误频繁。而这个配置就必须访问 MMD 寄存器所以 MMD 读取能力是绕不过去的基础设施。3. MMD 访问原理与 ZYNQ 实操实现3.1 0x0D/0x0E 窗口用两个影子寄存器访问整个 MMD 空间KSZ9031 支持通过 MDIO 管理接口进行 MMD 访问。如果你仔细看芯片手册会发现它提供了一套间接访问机制利用 Clause 22 的两个保留寄存器0x0D和0x0E做窗口。0x0D被称为 MMD 访问控制寄存器低 5 位用来选择 MMD 设备地址最高位bit15用来切换工作模式bit15 为 0 时0x0E是地址寄存器bit15 为 1 时0x0E是数据寄存器。整个流程可以总结成四步写 PHY 寄存器0x0D写入 MMD 设备地址此时 bit15 保持 0表示接下来要写地址。写 PHY 寄存器0x0E写入目标寄存器地址16 位偏移。再写 PHY 寄存器0x0D写入0x4000 | MMD 设备地址也就是把 bit15 置 1切换到数据模式。对 PHY 寄存器0x0E进行读或写拿到的就是对应 MMD 寄存器里的数据。这四步的本质就是把一个原本需要 Clause 45 管理帧才能访问的寄存器空间映射到了 Clause 22 的两个通用寄存器上。ZYNQ GEM 的 MDIO 控制器虽然没有直接暴露 Clause 45 的无缝操作接口但通过这种窗口方式一样能访问到扩展寄存器。而且这种窗口方式在业界非常通用KSZ9031、RTL8211F、Marvell 88E1518 等都大同小异只是个别芯片把0x0D叫 MMD Control把0x0E叫 MMD Address/Data操作逻辑完全相同。写个小表格方便对照步骤操作寄存器写入值含义1写0x0Dmmd_dev_addr选择 MMD 设备地址模式2写0x0Ereg_addr写入 MMD 寄存器地址3写0x0D0x4000 | mmd_dev_addr切换为数据模式4读/写0x0Edata读取或写入数据注意第 4 步如果是写操作写入的是0x0E寄存器如果是读操作返回的就是目标 MMD 寄存器的值。整个事务完成后最好把0x0D写回 0把窗口关掉否则后续如果驱动里有人直接读0x0E拿到的内容会和预期不符。3.2 ZYNQ 裸机下的 MMD 读取代码在 Xilinx SDK 或者 Vitis 的裸机环境里MDIO 底层读写一般通过XEmacPs_PhyWrite和XEmacPs_PhyRead完成。这两个函数封装了 GEM 的 PHY 维护寄存器操作。基于这个封装可以很轻松实现 KSZ9031 的 MMD 读写函数。下面是一个可以直接拿去用的示例#include xemacps.h int ksz9031_mmd_read(XEmacPs *mac, u32 phy_addr, u32 mmd_dev, u32 reg_addr, u16 *value) { int status; /* Step 1: 选择 MMD 设备进入地址模式 */ status XEmacPs_PhyWrite(mac, phy_addr, 0x0D, mmd_dev 0x1F); if (status ! XST_SUCCESS) return status; /* Step 2: 写入要访问的寄存器地址 */ status XEmacPs_PhyWrite(mac, phy_addr, 0x0E, reg_addr 0xFFFF); if (status ! XST_SUCCESS) return status; /* Step 3: 切换为数据模式bit15置1 */ status XEmacPs_PhyWrite(mac, phy_addr, 0x0D, 0x4000 | (mmd_dev 0x1F)); if (status ! XST_SUCCESS) return status; /* Step 4: 读取数据 */ status XEmacPs_PhyRead(mac, phy_addr, 0x0E, value); if (status ! XST_SUCCESS) return status; /* 关闭窗口避免影响后续访问 */ XEmacPs_PhyWrite(mac, phy_addr, 0x0D, 0x0000); return XST_SUCCESS; }写操作的函数类似区别只是在第 4 步改成XEmacPs_PhyWrite(mac, phy_addr, 0x0E, data)int ksz9031_mmd_write(XEmacPs *mac, u32 phy_addr, u32 mmd_dev, u32 reg_addr, u16 data) { int status; status XEmacPs_PhyWrite(mac, phy_addr, 0x0D, mmd_dev 0x1F); if (status ! XST_SUCCESS) return status; status XEmacPs_PhyWrite(mac, phy_addr, 0x0E, reg_addr 0xFFFF); if (status ! XST_SUCCESS) return status; status XEmacPs_PhyWrite(mac, phy_addr, 0x0D, 0x4000 | (mmd_dev 0x1F)); if (status ! XST_SUCCESS) return status; status XEmacPs_PhyWrite(mac, phy_addr, 0x0E, data); if (status ! XST_SUCCESS) return status; XEmacPs_PhyWrite(mac, phy_addr, 0x0D, 0x0000); return XST_SUCCESS; }使用的时候可以先用基础寄存器确认 PHY ID。KSZ9031RNX 的典型 PHY ID 是0x0022和0x1631也就是寄存器0x02读到0x0022寄存器0x03读到0x1631。如果这个都读不对就别急着调 MMD回到第二章节查 MDIO 通路。确认基础寄存器没问题后可以随便读一个 MMD 寄存器验证函数是否正确。比如读 MMD 设备 3 下面的 1000BASE-T 状态寄存器正常能返回一个非0xFFFF的值。如果依然读到0xFFFF就继续往下查问题。3.3 Linux/PetaLinux 下怎么验证 MMD如果你用的是 PetaLinux 或者主线 Linux情况会比裸机好一些因为 Linux PHY 驱动框架已经封装了phy_read_mmd和phy_write_mmd。但前提是 PHY 驱动里正确绑定了芯片型号而且 MDIO 控制器驱动支持对应操作。在调试阶段我最常用的办法是直接改内核设备树让 mdio 总线先正常工作然后通过用户态工具验证。新版 Linux 里有一个叫 mdio-tools 的工具集提供了mdio命令可以直接对 MDIO 总线发起读操作甚至发起 Clause 45 访问。比如mdio mdio-bus read 0x07 0x0d这里0x07是 PHY 地址0x0d是寄存器地址。通过组合读多个寄存器可以验证 MMD 窗口是否工作。需要注意的是老版本内核里某些驱动对 Clause 45 原生操作支持不完整但通过0x0D/0x0E窗口读取通常都有效因为窗口底层用的还是标准 Clause 22 帧。如果你不想额外装工具也可以先用 devmem2 直接访问 GEM 的 PHY 维护寄存器但那样要手动组帧太麻烦。我在调试时一般优先用 mdio-tools它不仅支持 Clause 22还支持 Clause 45省掉了自己写驱动的麻烦。这点对于快速排除是 PHY 问题还是驱动问题非常关键。4. 常见问题与排查技巧实录4.1 读基础寄存器正常MMD 读回来全是 0xFFFF这是最让人抓狂的情况。Clause 22 基础寄存器一切正常PHY ID 能读出来Vendor 信息也正常但按照标准 MMD 窗口流程读取返回永远是0xFFFF。我排查这种问题时按优先级列了三个怀疑对象第一MMD 设备地址是不是写错了。不同寄存器组对应不同 MMD 设备地址比如 IEEE 标准里常见的0x1是 PMA/PMD 寄存器组0x2和0x3是 1000BASE-T 相关控制状态寄存器组但厂商特有功能通常在更高地址的厂商定义区域。如果你拿厂商功能寄存器的设备地址去套标准流程读回来自然不对。这种问题的特点是读某些 MMD 寄存器正常读另一些全是 FFFF。第二第 4 步是否真的进入了数据模式。有些芯片对0x0D的 bit15 判断不是立刻生效需要在写入0x4000 | mmd_dev之后稍等几个 MDC 周期。软件里可以在第 3 步和第 4 步之间加一个非常短的 busy wait或者至少保证驱动实现里没有把两次写操作合并优化掉。第三PHY 当前状态不允许读取。如果 PHY 还在启动或者复位阶段部分 MMD 寄存器会返回0xFFFF。等链路状态稳定之后再读数据立刻恢复正常。4.2 PHY init failed 和 LwIP 起不来如果你用的是 SDK/Vitis 的 LwIP 模板初始化出错一般就卡在xemacpsif_phyinit这个函数里。它做的事情很简单从某个起始 PHY 地址开始循环读0x02和0x03寄存器判断有没有读到合法 PHY ID。如果循环到最后都找不到就报ERROR: PHY init failed。这种问题除了前面提到的硬件通路问题还有一个很容易被忽视的点模板里默认的 PHY 地址可能跟你板子上的硬件地址不一致。Vitis 模板往往默认PHY_ADDR是某个固定值比如 7而你的板子上 PHY strap 出来是 4。这个时候改一个宏定义就能过但要意识到这个问题必须和硬件原理图对应起来不能凭空猜。另外如果你修改过 FSBL 里 MIO 的配置比如把 MDIO 引脚从 MIO 改到了 EMIO那么 FSBL 里必须同步更新 GEM 的引脚配置。当时我遇到一个烧写 flash 时报valid FSBL file is required的提示一度以为跟 PHY 有关后来发现只是烧写工具要求指定 FSBL 文件而工程里没有输出有效 FSBL。这种问题跟 PHY 和 LwIP 没有关系不要混在一起排查。4.3 读完 MMD 后普通寄存器也跟着变奇怪这是一个非常常见的“二次伤害”。很多人在调试时直接在调试器里手动写0x0D、0x0E读到了想要的数据但之后就忘了恢复窗口状态。等到驱动再读0x0E这个保留或者半保留寄存器时拿到的数据完全不能看。所以我在实现 MMD 读写时强制在函数尾部执行XEmacPs_PhyWrite(mac, phy_addr, 0x0D, 0x0000)把窗口状态复位。这个动作对后续驱动行为影响很大尤其是在跑 LwIP 时驱动会周期性读取 PHY 的链接状态寄存器如果你把 MMD 窗口停留在数据模式驱动读0x01其实读的是0x0E窗口寄存器链路状态会变得不可预期。4.4 问题速查表现象可能原因检查与处理所有 PHY 寄存器读回 0xFFFFMDIO 通路不通、PHY 未上电、PHY 地址错误用示波器量 MDC/MDIO查原理图 PHYAD strap能读基础寄存器MMD 读回 0xFFFFMMD 设备地址错误、窗口时序不满足、PHY 状态未就绪选标准设备地址重试加延时后重读查手册LwIP 初始化报 PHY init failedPHY 地址宏定义与硬件不一致、复位未释放、MDC 分频不对确认宏定义释放复位调整分频值MMD 读完后普通寄存器乱掉0x0D 窗口没有复位在 MMD 读写函数尾部写 0x0D 为 0x0000千兆协商正常但 PING 不通或 CRC 错误RGMII 时钟偏移未调整通过 MMD 寄存器调整 RX/TX delaySGMII 接 PHY 起不来IP 核 MAC/PHY 角色配置错误核对 SGMII IP 配置改成 MAC 模式烧写 flash 提示 valid FSBL requiredFSBL 文件缺失或无效先构建有效 FSBL再执行 flash 操作5. 调试工具与实战技巧别靠猜靠波形和日志5.1 示波器是排查 MDIO 问题的第一工具说实话这类 PHY 调试问题里超过一半都是硬件或者引脚配置问题再强的软件理论分析也抵不过示波器看一眼。MDIO 总线空闲时是高电平MDC 时钟稳定输出。用示波器探头量 PHY 侧的 MDC 引脚如果看不到时钟软件写得再对也没用。再看 MDIO 数据线读操作时 MAC 发起地址和寄存器号然后释放总线PHY 在特定的时钟窗口驱动数据回传。如果能看到 MDIO 上有数据变化但 PHY 一直没有驱动总线大概率是 PHY 地址不对或者 PHY 芯片根本没进入工作状态。这种时候再去翻数据手册也好改软件也好都有明确方向了。5.2 mdio-tools 这类命令行工具能省一半时间现在的 Linux 环境里我强烈建议每个做 PHY 调试的人都备一套 mdio-tools。它的用法很简单不需要写内核模块直接用户态操作/dev/mdio设备节点就能够读写 Clause 22 和 Clause 45 寄存器。最新版本在 gitcode 上就有镜像仓库编译也比较简单。在 PetaLinux 或者嵌入式 Linux 里把这个工具放到 rootfs 中调试时直接在串口终端敲命令mdio mdio-bus read 0x04 0x02 mdio mdio-bus read 0x04 0x03先确认基础 ID再通过组合读写完成 MMD 窗口的验证。如果命令能读到0x0022和0x1631基本可以确定 PHY 管理通路没问题。这个工具比裸机下打印日志高效得多尤其是当你需要连续读大量寄存器分析链路协商状态时简直像开了外挂。5.3 把 MMD 操作封装成统一接口后面会非常省心无论是裸机还是 Linux我都建议把 MMD 读写封装成独立接口而不是在业务代码里散落各种XEmacPs_PhyWrite调用。封装之后调试阶段可以非常方便地加打印、加延时、加错误计数。比如int phy_debug_read_reg(u32 phy_addr, u32 reg) { u16 val 0; int status ksz9031_mmd_read(g_emac, phy_addr, 0x3, reg, val); if (status XST_SUCCESS) { xil_printf(PHY %02x MMD%d reg %04x %04x\n\r, phy_addr, 0x3, reg, val); } return status; }有了这种接口你就可以在初始化流程里加一个“PHY 自检模式”逐段打印基础寄存器、MMD 寄存器、协商状态。等整个系统跑起来之后这套调试代码可以留着以后遇到链路问题随时能用。实测下来这种调试代码的价值远远超过了它占用的一点 Flash 空间。另外提醒一句不同平台上的 MMD 实现有一点差异。STM32 的 ETH 驱动、NXP 的 MDIO 驱动在寄存器窗口实现细节上跟 ZYNQ GEM 不完全一样但0x0D/0x0E窗口思路是通用的。如果换一颗国产百兆 PHY 芯片也一定先查它的寄存器手册确认 MMD 设备地址和访问时序别想当然认为和 KSZ9031 完全一样。把这一步做好后面调 LwIP 也好调裸机协议栈也好都会顺畅很多。