ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

嵌入式网络调试:Ethernet PHY软件复位失效根因与解决实践

2026/8/30 15:32:58 拓冰建站 浏览量
嵌入式网络调试:Ethernet PHY软件复位失效根因与解决实践 搞嵌入式网络开发的朋友应该都撞过这个场景板子上电代码初始化 PHY执行了软件复位但寄存器读出来还是老样子或者链路死活起不来。Ethernet PHY 的软件复位不生效看起来是个很小的点但牵扯到 MDIO 时序、PHY 地址、strap 管脚、甚至驱动初始化顺序问题藏得深的时候能折腾好几天。我调过不少带 PHY 的板卡从 RTL8211、KSZ9031 到 DP83848 都踩过坑这篇文章就专门聊聊 Software reset 失效这件事哪些原因会导致它失效、怎么一步步定位、以及驱动侧怎么把软复位写得足够稳。1. 软复位的第一课寄存器行为与正常时序1.1 软件复位到底在复位什么以太网 PHY 通过 MDIOManagement Data Input/Output接口与 MAC 或 CPU 通信这个接口专门用于读写 PHY 内部寄存器。软件复位一般指往 BMCRBasic Mode Control Register寄存器地址 0x00的 bit15 写 1 来触发 PHY 内部复位。这个复位位是自清零的PHY 复位完成后硬件会自动把 bit15 清 0所以“复位是否完成”可以通过轮询 bit15 判断。PHY 软复位会做几件事把大部分 MII 寄存器恢复成默认值、重新初始化 PHY 内部状态机、中断当前自协商并重新启动。这也就意味着如果复位后你原本配置的自协商使能、速度、双工等参数没有重新写一遍PHY 会回到默认配置链路可能和你的预期不一致。很多“软复位不工作”的现象表面上是 PHY 没有反应其实是复位之后配置被清掉了但又被误读成“没复位成功”。1.2 写 1 不等于执行读 0 才算数不少人在代码里写了一个mdio_write(phy_addr, 0, 0x8000)就以为复位完成了。这个想法在多数 PHY 上会出问题。因为复位需要时间不同 PHY 的复位时间差别很大快的几十微秒慢的能到几百毫秒甚至更久。如果你写完立刻读 0x00很有可能会读到 0x8000这并不代表复位失败而是复位还没执行完。正确的做法是写 0x8000 后循环读取 BMCR直到 bit15 变为 0同时设置一个超时值防止卡死。比如int phy_soft_reset(int phy_addr) { int ret; uint16_t val; int timeout 100; ret mdio_write(phy_addr, 0x00, 0x8000); if (ret 0) return ret; while (timeout--) { ret mdio_read(phy_addr, 0x00, val); if (ret 0) return ret; if ((val 0x8000) 0) return 0; usleep(1000); } return -1; }1.3 怎么证明“软复位真的执行了”调试时不要只盯着 BMCR 的 bit15还要从多个角度确认复位确实发生过。首先是读 PHY ID 寄存器地址 0x02 和 0x03正常复位后 ID 应该保持不变。其次是观察链路状态复位期间 PHY 的 link 会掉线自协商重新开始所以如果你用示波器或 ethtool 持续观察会看到 link down 又 up 的过程。最后是核对配置寄存器比如 BMCR 的 bit15 清零后bit12自协商使能、bit13速度选择等会恢复到默认值。如果复位前后这些值完全没变化那基本可以确定软复位没有真正执行。2. 根因分类为什么软复位会失效2.1 MDIO 总线层面数据根本没写进去最常见的软复位失效原因不是 PHY 本身的问题而是 MDIO 上的写操作根本没成功。MDIO 的通信时序有规范要求MDC 时钟频率在 IEEE 802.3 标准里最大是 2.5 MHz部分 PHY 支持更高的 25 MHz 模式但需要特定寄存器开启而且不是所有 PHY 都保证在高时钟下稳定工作。排查时先看 MDC 频率。有的系统为了赶速度把 MDC 配到了 10 MHz 甚至更高结果 PHY 对数据采样出错寄存器写入不生效。我遇到过一块板子读 PHY ID 一直正常但写 BMCR 复位位后读回还是 0x0000最后用示波器抓波形才发现写帧的数据段完全乱了把 MDC 降到 2.5 MHz 之后立刻恢复正常。MDIO 帧格式也值得了解。一个完整的 MDIO 写帧包含 32 位 preamble、起始码、操作码01 表示写、5 位 PHY 地址、5 位寄存器地址、2 位 turnaround 和 16 位数据。如果你用逻辑分析仪或者示波器抓帧可以按这个格式手动解码确认 CPU 发出的 PHY 地址、寄存器地址和实际想要访问的是否一致。上拉电阻也是常见问题点。MDIO 数据线是双向的读操作时由 PHY 驱动如果 MDIO 缺少上拉电阻读数据时高电平可能不够导致读到全 0 或随机值写操作倒是正常。判断方法很简单只读 PHY ID如果每次读出来都不一样优先检查上拉电阻和 IO 电平。2.2 你操作的不是你想要的 PHYPHY 地址由 PHYAD strap 管脚在上电复位时锁存范围是 0 到 31。MDIO 写帧里的 PHY 地址必须和 PHY 实际锁存的地址一致否则 PHY 根本不应答。很多板卡的 PHY 地址在硬件设计阶段已经确定比如通过上下拉电阻把 PHYAD[4:0] 配成 0x01但如果你在代码里写死了 0x00软复位自然无效。在多 PHY 场景下更容易踩坑。比如带交换芯片的板卡CPU 通过 MDIO 总线接 switchswitch 内部又管理多个 PHY这种时候 PHY 地址往往不是简单的 0-4可能包含页选择或者内部端口映射。最稳妥的做法是上电后先遍历地址 0-31读取每个地址的 PHY ID和 datasheet 上的期望值比对确认哪个地址对应哪颗 PHY再做复位操作。2.3 寄存器访问路径或间接访问陷阱有些 PHY 芯片的寄存器访问不是简单的直接访问。比如扩展寄存器需要先写页寄存器再访问目标地址或者 PHY 挂在 MDIO mux 后面需要先切换通道。如果驱动把地址配置错软复位命令写到了错误的寄存器PHY 当然不会有反应。还有一个隐蔽的坑有些 PHY 在低功耗或者隔离模式下MDIO 接口仍然能够响应但寄存器写入可能被忽略或者只有特定命令才能唤醒。如果你把一个 PHY 配置成 power down 之后再去写软复位会发现 bit15 写进去了但 PHY 状态不对链路也起不来。这种时候先检查 BMCR 的 bit11power down和 bit10isolate是否被意外置位。2.4 被后面的代码覆盖最常见也最隐蔽这是我见过最多的“软复位失效”原因现象非常具有迷惑性单独执行软复位是好的但接入完整驱动流程后复位好像没发生过。问题出在初始化顺序上。比如驱动流程是写软复位、延时固定时间、配置自协商、等待自协商完成。如果延时时间不够PHY 的复位还没走完后面的自协商配置写入就可能被复位过程覆盖掉。最终效果就是 BMCR 里的自协商使能位不正确link 建立不起来看起来像是“复位把配置清了”实际上是复位还没完成配置写得过早。排查方法在复位完成后延时几毫秒把 BMCR 寄存器读出来再对比你后面代码写入的预期值。不一致就说明写入顺序有时序问题解决方法是把软复位改成轮询等待自清零完成再继续配置。2.5 strap 管脚在复位释放时锁定严格来说软件复位不会重新采样 strap 管脚strap 只在硬件复位释放或者上电过程中锁存。但很多“软复位不生效”的板子根因反而在硬件复位和 strap 上。如果硬件复位释放瞬间 strap 管脚电平没有稳定PHY 会锁存到错误的地址或者模式后续软复位即使执行成功PHY 的工作模式也是错的。比如有的电路用 RC 延时给 strap 管脚做默认电平如果 RC 时间常数太大复位释放时电平还没稳定PHY 就把不确定的电平锁进去了。这种问题在低功耗设计、IO 电压切换的板子上尤其常见。排查时需要示波器看复位释放时刻 strap 管脚的电平是否符合设计预期。2.6 时钟没起来状态机不转PHY 需要一个参考时钟才能维持内部状态机运行一般是外接晶振或由 MAC 提供 25 MHz 时钟。如果时钟没起振或者频率不对MDIO 接口可能还能读 ID因为读 ID 不需要完整的状态机但软复位后的自协商完全不会进行链路也就起不来。我调试时有个习惯碰到复位不生效先测 PHY 时钟输出或者晶振波形。用示波器探一下 XI 或 CLK_OUT 管脚确认频率和幅度符合 datasheet 要求。很多时候软复位本身没问题问题在于 PHY 的数据通路根本没工作时钟就是第一嫌疑。下面是失效现象和优先排查方向的速查表方便现场对照定位失效现象优先排查项常见原因写复位后 BMCR 仍为 0x8000MDC 频率、MDIO 波形总线时序不满足 PHY 要求读 PHY ID 全 0 或全 1PHY 地址、上拉电阻地址不匹配或总线无应答软复位单测正常加上配置代码后失效初始化顺序配置写入早于复位完成复位后 link 始终起不来时钟、strap、硬件复位PHY 数据通路未工作PHY 地址与实际硬件不符PHYAD strap 管脚strap 锁存错误3. 现场排查把问题“逼”出来3.1 先读 PHY ID 建立基线排查任何 PHY 问题前先把 PHY ID 读出来。PHY ID 由寄存器 0x02OUI 高 16 位和 0x03OUI 低 6 位 型号 6 位 版本 4 位组成。每个 PHY 型号有固定 ID你可以对照 datasheet 确认读取结果是否正确。如果读到 0x0000 或者 0xffff说明 MDIO 总线上根本没有 PHY 响应或者地址不对。建议写一个扫描脚本遍历 PHY 地址 0-31把所有能读到合法 ID 的地址打印出来。以 mdio-tools 为例# 遍历地址0-31读寄存器2和3 for i in $(seq 0 31); do id1$(mdio read phy 0 $i 2 2/dev/null | awk {print $NF}) id2$(mdio read phy 0 $i 3 2/dev/null | awk {print $NF}) if [ $id1 ! 0xffff ] [ $id1 ! 0x0000 ]; then echo addr$i id$id1$id2 fi done脚本的目的是找到所有存活的 PHY 地址。正常板卡上结果里应该能看到 datasheet 对应的 ID如果找不到先别纠结软复位把 MDIO 总线基本问题解决再说。3.2 示波器抓 MDIO/MDC观察帧波形软件层面的寄存器读写最终要通过 IO 波形体现。用示波器同时测 MDC 和 MDIO触发方式选 MDC 上升沿抓一次写 BMCR 0x8000 的完整帧。观察几个关键点MDC 频率是否在 PHY 允许范围内MDIO 数据是否在 MDC 下降沿后变化、上升沿前稳定帧里的 PHY 地址和寄存器地址是否和代码一致。如果发现数据线在 MDC 上升沿附近还在跳变说明时序余量不够。这时候可以降低 MDC 频率或者调整 MDIO 驱动能力优先保证通信稳定。还有一种情况MDIO 线上有电平竞争。比如多个 MDIO 主机同时操作同一根总线或者 PHY 的 MDIO 引脚被复用为其他功能读写数据就会错乱。用示波器看波形时如果发现 MDIO 高电平被拉低大概率是总线冲突。3.3 读回 BMCR确认写操作落地写软复位后立即读 BMCR有两种典型情况需要区分第一种读到 0x8000说明复位还在进行中继续轮询等待自清零。第二种读到 0x0000或者其他值但 bit15 已经为 0说明 PHY 接受了复位并完成。如果此时你看到 bit12 自协商使能、bit13 速度选择等配置寄存器都恢复默认了证明复位执行过。如果写完 0x8000 后无论怎么轮询 bit15 都一直为 1而且 PHY ID 能正常读到那问题基本在 PHY 内部状态。建议检查 PHY 的电源、时钟、复位管脚是否正常。3.4 检查硬件复位和 strap 信号软件复位不工作很多人忽略了硬件复位管脚。PHY 的 RST# 管脚如果在软件复位过程中被外部拉低会导致 PHY 一直在硬件复位状态任何 MDIO 写操作都不会被真正执行。检查 RST# 电平是否稳定在高电平如果有周期性拉低要追踪是谁在操作这个管脚。strap 信号检查要在复位释放瞬间做。示波器触发在 RST# 上升沿看 PHYAD、ANEG、MODE 等 strap 管脚的电平是否已经稳定。如果 strap 管脚在复位释放后才慢慢爬升PHY 锁存的就不是你想要的配置这对后续软复位的“无效”判断会产生误导。3.5 最小化复现脚本把问题从完整驱动里剥离出来用最小化脚本复现。在嵌入式 Linux 下可以直接在 shell 里读写 MDIO 寄存器在裸机环境写一个简单的测试函数只做三件事读 PHY ID、写软复位、轮询 BMCR。下面是一个最小化的 C 示例#include stdio.h #include unistd.h int main(int argc, char *argv[]) { int addr argc 1 ? atoi(argv[1]) : 1; uint16_t id1, id2, bmcr; mdio_read(addr, 2, id1); mdio_read(addr, 3, id2); printf(PHY ID: 0x%04x%04x\n, id1, id2); mdio_write(addr, 0, 0x8000); for (int i 0; i 100; i) { mdio_read(addr, 0, bmcr); if (!(bmcr 0x8000)) { printf(reset done after %d ms\n, i); break; } usleep(1000); } printf(BMCR after reset: 0x%04x\n, bmcr); return 0; }如果这个最小化脚本能正常复位说明硬件和 MDIO 总线没问题问题在完整驱动的代码逻辑。如果脚本都复现不了软复位失效那就回到硬件和总线排查。4. 三个真实案例复盘4.1 案例 AMDC 频率太高导致写操作丢失现象板卡上调试读取 PHY ID 正常但写软复位寄存器后再读 BMCR 一直是 0x0000bit15 始终没有置位PHY 也没有任何 link 变化。当时第一反应是 PHY 坏了换了芯片还是一样。排查用示波器抓 MDC 波形发现 MDC 频率被一个共用驱动的代码配置成了 12.5 MHz。再抓 MDIO 写帧发现数据段在 MDC 上升沿采样时不稳定PHY 把数据采样错了相当于写进去的数据不是 0x8000而是随机值。解决把 MDC 频率降到 2.5 MHz重新测试软复位正常生效。后面我养成了一个习惯只要能调低 MDC 频率就先用低频率跑通功能再去考虑性能优化。4.2 案例 B多 PHY 板卡地址认错对象现象一块带交换芯片的板卡交换芯片内部有 5 个 PHY。代码里固定往 PHY 地址 0 执行软复位结果交换机某个端口的 link 总是异常另一个端口倒是正常。排查用扫描脚本遍历 0-31发现 PHY 地址分布和代码预期完全不同。交换芯片把内部 PHY 映射到了以 0x04 起始的地址区间地址 0 上根本没有 PHY 响应。也就是说软复位命令一直被发到了总线上一个不存在的 PHY那“不工作”就是必然的。解决修改驱动中的 PHY 地址表按扫描到的实际地址配置软复位和后续链路配置都正常。4.3 案例 C复位成功但配置被覆盖现象最小化脚本测试软复位是好的但跑完整驱动后PHY 的自协商配置总是不对link 起不来。当时排查了很久一度以为 PHY 有问题。排查在软复位函数返回后加延时读取 BMCR发现自协商使能位为 0而代码里明明在后面写了配置。再细看代码发现驱动里软复位后有一个usleep(1000)的固定延时但实际 PHY 复位需要 5 ms 左右。后面的自协商配置写入太早被复位过程清掉了。解决把固定延时改成轮询 bit15 自清零清零后再写自协商配置问题消失。同时加了超时保护防止 PHY 异常时卡死初始化。案例复盘汇总案例现象定位手段修复方式MDC 频率过高写复位后 BMCR 不置位示波器抓 MDC/MDIO 波形MDC 降到 2.5 MHzPHY 地址错误复位命令发到不存在的 PHY扫描 0-31 地址读 PHY ID改用实际 PHY 地址配置写入过早复位单测正常驱动整体异常复位后读回 BMCR 比对轮询自清零后再配置5. 驱动侧稳妥实现一份可以直接落地的软复位代码5.1 封装 MDIO 读写原语不同平台的 MDIO 读写实现差别很大但接口可以统一。建议封装两个函数返回 0 表示成功负值表示错误。代码里不直接操作硬件寄存器方便后续适配或用 mock 测试int mdio_read(uint8_t phy_addr, uint8_t reg_addr, uint16_t *val); int mdio_write(uint8_t phy_addr, uint8_t reg_addr, uint16_t val);底层实现里注意 MDC 频率必须控制在 PHY 支持范围内MDIO 读写帧要按照 IEEE 802.3 格式构造帧间隔最好留几个 MDC 时钟周期避免连续操作时 PHY 来不及处理。5.2 软复位实现的四个关键点一个可靠的phy_soft_reset函数要有四个关键点写 0x8000 到 BMCR、轮询 bit15 自清零、超时保护、复位完成后延迟让内部状态机稳定。参考实现#define PHY_REG_BMCR 0x00 #define BMCR_RESET 0x8000 #define BMCR_ANENABLE 0x1000 #define BMCR_SPEED100 0x2000 #define BMCR_DUPLEX_FULL 0x0100 int phy_soft_reset(uint8_t phy_addr) { uint16_t val; int ret; int timeout 200; ret mdio_write(phy_addr, PHY_REG_BMCR, BMCR_RESET); if (ret 0) return ret; while (timeout--) { ret mdio_read(phy_addr, PHY_REG_BMCR, val); if (ret 0) return ret; if ((val BMCR_RESET) 0) break; usleep(1000); } if (timeout 0) return -1; /* 给PHY内部状态机一点时间 */ usleep(1000); return 0; }注意这里的usleep(1000)是给状态机启动用的不同 PHY 可能有差异如果后面紧接着要配置自协商建议再加一个 10 ms 级别的延时或者轮询 PHY 状态寄存器确认自协商已经启动。5.3 复位 自协商 链路检测的编排软复位之后不能直接等 link需要重新配置自协商参数。推荐的初始化顺序是读取 PHY ID确认访问对象正确。执行软复位等待自清零。写 ANAR寄存器 0x04配置本端支持的速率和工作模式。写 BMCR使能自协商并重启自协商bit12 1bit9 1。轮询基本状态寄存器寄存器 0x01的 bit5自协商完成和 bit2link 状态。很多驱动把第 2 步和第 4 步混在一起先写 BMCR 使能自协商再写软复位结果复位把自协商使能又清掉了。顺序上一定要先复位复位完成后再写配置。5.4 多 PHY 场景的复位策略一块板子上有多个 PHY 时尽量逐个复位、逐个确认。同时对所有 PHY 发软复位虽然省时间但多个 PHY 同时重启自协商可能带来电源冲击也可能让 MDIO 总线出现短时间异常。逐个复位的代价是初始化时间变长但如果把每个复位之间的等待时间优化好实际影响很小。另外多 PHY 场景下一定要维护一张 PHY 地址映射表把“逻辑端口号”和“MDIO PHY 地址”对应起来不要把 PHY 地址硬编码散落在各处。调试时打印复位前后每个 PHY 的状态能省非常多时间。6. 调试软复位的三条硬件经验6.1 新板卡先测时钟和复位释放拿到新板卡的第一件事不是烧代码而是用示波器测三样东西PHY 参考时钟是否起振、频率是否准确、RST# 复位释放时间是否满足 datasheet 要求。这三样如果没问题软件层面的排查才有意义。我见过因为晶振虚焊导致 PHY 软复位不生效的也见过 RST# 上拉电阻没贴导致 PHY 一直处于复位状态的。这些问题单纯靠软件是发现不了的。6.2 寄存器快照改动前先留证据调试 PHY 问题时建议在每次改动寄存器前把当前 BMCR、ANAR、状态寄存器的值打印出来。后面如果改了配置发现链路异常可以对比快照判断是哪个配置被覆盖了。软复位失效场景下快照尤其有用能确认复位前后寄存器变化是否符合预期。6.3 把软复位做成单独调试接口不要把软复位逻辑藏在完整初始化函数深处。把它做成一个独立接口比如调试串口命令phy_reset addr这样无论什么时候怀疑软复位有问题都能单独触发观察结果。完整的初始化流程里调用这个接口最小化脚本里也调用这个接口调试一致性和复现路径都会清晰很多。在实际项目里我最后都是靠这种分离式设计快速定位问题的。一旦怀疑软复位不生效先通过调试接口单独复位如果单独复位正常再回头查驱动初始化的上下文逻辑。6.4 软复位失效但读 ID 正常优先查逻辑如果软复位失败但 PHY ID 能正常读到那可以确定 MDIO 总线基本通路是好的问题大概率在逻辑层地址配错、时序不够、配置覆盖。如果 PHY ID 都读不到先别急着查软复位总线电气和硬件复位才是重点。这是我调试以来总结得最实用的一条排查顺序ID 能读查逻辑ID 不能读查硬件。