U-Boot网络链路深度解析:从net到MAC到PHY的调试与移植
发布时间:2026/10/3 12:32:16 作者:尧图编辑部 阅读量:1,286

1. 为什么值得花时间把U-Boot网络这条链路吃透搞嵌入式Linux的人早晚会撞上U-Boot网络这道坎。板子刚上电内核还没起来你要么得靠TFTP把内核镜像拽进去要么得靠NFS挂根文件系统再不然就是产线批量烧录时用网络下发固件。这些场景全都压在U-Boot的网络栈上。一旦网络不通你连个像样的调试手段都没有串口里敲破键盘也只能看到Retrying...无限循环。我见过太多人在这条链路上卡住有人换了PHY芯片之后网口死活不亮有人MDIO读出来全是0xFFFF有人ping通了自己却ping不通网关还有人明明PHY自协商显示link up但就是收不到包。这些问题的根子往往不在某一个点而在于对整条链路——从U-Boot的net子系统到MAC控制器再到PHY芯片最后到MDIO管理总线——缺乏一个完整的认知框架。这篇东西就是想把这条链路从头到尾捋一遍。我会按U-Boot网络子系统怎么组织→MAC驱动怎么接进来→PHY怎么被识别和管理→MDIO总线怎么读写寄存器→实际调试时怎么一步步定位这个顺序展开。适合正在做板级bringup的BSP工程师、需要移植U-Boot网络驱动的开发者以及那些被网口问题折磨过想彻底搞明白原理的人。读完你应该能自己判断问题出在net层、MAC层、PHY层还是MDIO层而不是盲目地改设备树或者换PHY地址。2. U-Boot网络子系统的整体设计与分层思路2.1 从net到mac到phy的三层结构U-Boot的网络代码在drivers/net/和net/两个目录下但真正理解它需要建立一个清晰的分层模型。我习惯把它分成三层来看最上面是协议层对应net/目录负责ARP、IP、ICMP、UDP、TFTP、NFS这些协议的处理。你敲的ping、tftp、dhcp命令最终都落到这里。这一层不关心你用的是哪个MAC、哪个PHY它只跟一个叫struct eth_device的东西打交道。中间是MAC层对应具体的以太网控制器驱动比如drivers/net/designware.c、drivers/net/fec_mxc.c、drivers/net/ti/cpsw.c这些。MAC控制器是SoC内部的东西负责组帧、CRC校验、DMA搬运。它通过MII/RMII/RGMII这些接口跟外部的PHY芯片相连。最下面是PHY层对应drivers/net/phy/目录。PHY是那颗通常放在网口变压器旁边的小芯片负责把MAC过来的数字信号变成差分模拟信号发到网线上。U-Boot里有一个通用的PHY驱动框架phy.c加上各家PHY厂商的具体驱动realtek.c、micrel.c、marvell.c等。这三层之间通过两个关键结构体串联struct eth_device把MAC驱动注册到协议层struct phy_device把PHY芯片挂到MAC驱动上。理解这两个结构体的字段和生命周期是读懂整条链路的关键。2.2 为什么U-Boot不直接操作PHY而要搞个通用框架有人可能会问PHY寄存器就那么几个直接在MAC驱动里写死地址读写不就行了为什么要搞一套phy_device、phy_driver、phy_connect的框架这个问题我当年也想过。答案在于可移植性和可维护性。一块板子换个PHY芯片是常有的事Realtek的RTL8211换Micrel的KSZ9031寄存器定义、自协商流程、延时参数都不一样。如果每个MAC驱动都自己实现一遍PHY操作代码会烂成一锅粥。U-Boot的PHY框架做了几件事第一通过MDIO总线扫描PHY地址自动识别PHY的IDphy_id然后匹配对应的phy_driver第二把自协商、link状态读取、速度双工配置这些通用流程抽象成genphy_*系列函数第三允许具体PHY驱动覆盖这些通用函数处理厂商特有的寄存器。这个设计的好处是MAC驱动只需要调用phy_connect()拿到一个phy_device指针然后调phy_startup()、phy_read_status()就行完全不用管底下是哪家PHY。换PHY的时候只要新PHY有对应的驱动MAC驱动一行都不用改。2.3 设备树在其中的角色现代U-Boot2016年之后基本都走设备树了。网络相关的设备树节点通常长这样ethernetfe300000 { compatible rockchip,rk3399-gmac; reg 0x0 0xfe300000 0x0 0x10000; clocks cru SCLK_MAC; phy-mode rgmii; phy-handle phy0; mdio { #address-cells 1; #size-cells 0; phy0: ethernet-phy1 { reg 0x1; }; }; };这里有几个关键点phy-mode告诉MAC用哪种接口MII/RMII/RGMII/SGMIIphy-handle指向具体的PHY节点PHY节点里的reg就是PHY的MDIO地址。U-Boot在初始化MAC驱动时会解析这些属性然后调用phy_connect()把MAC和PHY绑在一起。我踩过的一个坑是有些老代码用phy-addr属性直接写在MAC节点里新代码用phy-handle指向子节点。如果你混用了可能PHY根本扫不到。移植的时候一定要确认U-Boot版本对应的绑定文档。3. MAC驱动与PHY的连接细节从probe到link up3.1 MAC驱动probe时到底做了什么以DesignWare GMAC很多国产SoC和Rockchip、STAR的五代芯片都用它为例dwmac_probe()大致做了这些事第一步从设备树读取寄存器基地址、时钟、复位引脚做devm_ioremap和clk_prepare_enable。这一步失败通常表现为访问MAC寄存器直接挂死或者读出来全0。第二步配置MAC的工作模式。设置phy-mode对应的接口类型配置DMA突发长度、FIFO阈值这些。这里有个容易忽略的点RGMII模式下需要配置TX/RX延时。很多板子网口能link up但丢包严重就是RGMII延时没配对。延时可以配在MAC侧通过tx_delay/rx_delay参数也可以配在PHY侧通过PHY的扩展寄存器两边只能配一边配重了会采样错误。第三步注册MDIO总线。调用mdiobus_register()把设备树里mdio节点的子节点注册成MDIO设备。这一步完成后MDIO总线上就能扫描PHY了。第四步调用phy_connect()。这个函数会遍历MDIO总线上的地址0到31读每个地址的PHY ID寄存器寄存器2和3找到匹配的PHY后用phy_driver里的config_init做初始化。第五步注册struct eth_device到U-Boot的网络层填充iobase、init、send、recv、write_hwaddr这些回调。3.2 phy_connect的匹配逻辑与常见失败原因phy_connect()的匹配逻辑其实不复杂它先看设备树里有没有指定phy-handle有的话直接读那个节点的reg作为PHY地址没有的话就从0到31逐个扫描。扫描时读寄存器2PHY ID1和寄存器3PHY ID2拼成一个32位的ID然后跟phy_driver数组里的phy_id和phy_id_mask做匹配。常见的失败原因我整理成表格现象可能原因排查方法扫描不到任何PHYMDIO时钟太快/太慢降低MDIO时钟频率通常2.5MHz以下读到ID为0xFFFFPHY没供电或复位没释放量PHY的VDD和复位引脚电平读到ID为0x0000MDIO数据线接反或上拉缺失检查MDIO/MDC走线和上拉电阻匹配到错误驱动PHY ID掩码设置不对确认phy_id_mask覆盖了正确的位PHY找到了但link不上自协商没启动或延时不对读BMSR和BMSR扩展寄存器我印象最深的一次是某块板子MDIO死活读不到PHY最后发现是MDC时钟分频系数设成了最小值MDC频率跑到25MHzPHY根本反应不过来。把分频调大之后立刻就正常了。所以MDIO时钟频率是个高频坑点尤其是SoC主频很高的时候。3.3 link up之后U-Boot做了什么PHY link up之后phy_startup()会调用phy_read_status()读取BMSR寄存器判断当前速度和双工模式。如果是千兆还会读MII_STAT1000寄存器确认是1000BASE-T还是1000BASE-X。然后根据结果配置MAC侧的速率和双工。这里有个细节U-Boot的PHY框架默认会启动自协商phy_config_aneg但有些场景下自协商会很慢比如对端是强制模式的交换机导致U-Boot启动时网络初始化超时。这时候可以在设备树里加phy-connection-type或者直接在PHY驱动里强制速度和双工。另外link up之后建议加一个小延时再开始发包。有些PHY芯片link状态位翻转和实际能收发包之间有几毫秒的窗口U-Boot如果立刻发TFTP请求可能前几个包会丢。我在几个项目里都遇到过加个10ms延时就能稳定。4. PHY芯片的识别、配置与MDIO寄存器操作4.1 PHY ID的组成与识别流程每个符合IEEE 802.3规范的PHY都有两个ID寄存器寄存器2是PHY ID1OUI的高16位寄存器3是PHY ID2OUI的低6位加上厂商自定义的4位型号。拼起来是一个32位数高22位是OUI低10位是厂商自定义。以Realtek RTL8211F为例它的PHY ID是0x001cc916。在drivers/net/phy/realtek.c里驱动定义是这样的static struct phy_driver rtl8211f_driver { .name Realtek RTL8211F, .uid 0x001cc916, .mask 0x001fffff, .features PHY_GBIT_FEATURES, .config rtl8211f_config, .startup rtl8211f_startup, .shutdown rtl8211f_shutdown, };uid是完整的IDmask是匹配掩码。phy_connect()扫描时会把读到的ID和uid做(id mask) (uid mask)的比较。掩码的作用是忽略某些不重要的位比如有些PHY的版本号在低几位不同批次可能不一样掩码就能让同一个驱动匹配多个版本。4.2 MDIO读写寄存器的底层实现MDIO总线的读写时序是固定的先发32个1作为前导码然后发01读或10写的操作码接着5位PHY地址、5位寄存器地址最后2位TAturnaround。读操作时TA是Z0写操作时TA是10。U-Boot里MAC驱动需要实现mdio_read和mdio_write两个回调或者用mdiobus_register注册一个struct mii_bus。以DesignWare为例它通过MAC的GMAC_MDIO_ADDRESS和GMAC_MDIO_DATA寄存器来间接操作MDIOstatic int dwmac_mdio_read(struct mii_bus *bus, int phy, int reg) { struct dwmac_priv *priv bus-priv; u32 addr; int ret; addr (phy 11) | (reg 6) | MII_BUSY | MII_READ; writel(addr, priv-base GMAC_MDIO_ADDRESS); ret dwmac_mdio_wait_busy(priv); if (ret) return ret; return readl(priv-base GMAC_MDIO_DATA) 0xffff; }这里的关键是MII_BUSY位写完地址后要轮询这个位直到清零表示MDIO操作完成。轮询超时时间要设够太短了会误判失败太长了会拖慢扫描速度。我一般设1000次循环每次读寄存器加一点延时。4.3 常用PHY寄存器速查与调试技巧调试PHY的时候有几个寄存器是必看的寄存器地址名称作用典型值0x00BMCR基本控制自协商使能、复位、速度选择0x11400x01BMSR基本状态link状态、自协商完成0x796d0x02/0x03PHYID1/2PHY ID视芯片而定0x04ANAR自协商通告0x01e10x05ANLPAR链路伙伴自协商能力0x45e10x09MII_STAT1000千兆状态0x03000x0fMII_ESTATUS扩展状态0x3000在U-Boot命令行里可以用mii命令直接读写PHY寄存器# 读PHY地址1的寄存器0 mii read 1 0 # 写PHY地址1的寄存器0值为0x1140 mii write 1 0 0x1140 # 扫描MDIO总线上所有PHY mii dump 1mii dump会打印PHY的0到5号寄存器的解析结果非常直观。我调试的时候第一步就是mii dump看PHY ID对不对、link状态有没有、自协商完成没有。如果mii dump都读不出来那问题就在MDIO层或者PHY供电不用往下查了。注意有些PHY的扩展寄存器需要通过寄存器0x1e页面选择切换页面才能访问。比如Realtek的PHY要读扩展寄存器得先写0x1e选择页面再读目标寄存器最后切回页面0。忘了切回去会导致后续标准寄存器读写异常。5. 实操从零调试一块新板子的U-Boot网络5.1 硬件检查清单在动软件之前先确认硬件没问题。我一般按这个顺序查PHY供电量PHY的VDDIO和VDDA通常是1.8V或3.3V。有些PHY还有独立的1.2V内核电压。复位信号PHY的复位引脚在上电后应该有一个低脉冲。如果一直拉低PHY不工作如果一直高可能没复位成功。时钟PHY需要25MHz或50MHz的参考时钟。用示波器量一下没有时钟PHY就是死的。MDIO/MDC走线这两根线需要上拉电阻通常4.7k到10k。走线太长或者没上拉会导致通信失败。RGMII走线RGMII的TX/RX共12根线要等长延时匹配。如果走线差异太大高速下会采样错误。5.2 软件调试的五个阶段阶段一确认MDIO能通在U-Boot里执行mii read 0 2如果返回0xFFFF或0x0000说明MDIO不通。检查MDC时钟分频、GPIO复用有些SoC的MDIO引脚默认不是MDIO功能、上拉电阻。阶段二确认PHY ID正确mii dump 1看PHY ID。如果ID跟手册对不上可能是PHY地址不对试试0到31或者PHY处于某种特殊模式比如有些PHY上电后默认在isolate模式。阶段三确认link upmii read 1 1读BMSRbit 2是link statusbit 5是自协商完成。如果link没up检查网线、对端设备、自协商配置。可以试着手动mii write 1 0 0x1140强制千兆全双工。阶段四确认能收发包ping网关。如果ping不通但link是up的检查MAC的DMA配置、FIFO阈值、RGMII延时。可以用mii read 1 0x0f读扩展状态看有没有CRC错误、符号错误。阶段五确认TFTP/NFS能用ping通了但TFTP超时通常是U-Boot的IP地址、网关、子网掩码配错了或者TFTP服务器没开。用setenv检查环境变量printenv看ipaddr、serverip、netmask、gatewayip。5.3 一个真实的RGMII延时调试案例某块RK3399的板子PHY是RTL8211Flink up正常ping通但TFTP下载大文件时丢包严重速度只有几百KB/s。用mii dump看PHY状态一切正常没有CRC错误。问题出在RGMII延时。RK3399的GMAC支持在MAC侧配置TX/RX延时设备树里默认没配。RTL8211F的默认延时是TX 0ns、RX 0ns而RGMII规范要求TX和RX各加约2ns延时具体取决于走线长度。解决方案是在设备树里加gmac { tx_delay 0x28; rx_delay 0x1a; };tx_delay和rx_delay的值是SoC特定的RK3399的GMAC用的是0x28和0x1a对应约2ns和1.5ns。改完之后TFTP速度直接跑到满速。这个案例说明link up不代表链路质量好。RGMII延时不对的时候低速包可能能通高速包就会大量出错。调试时一定要用大文件传输来验证链路质量不能只靠ping。6. 常见问题与排查技巧实录6.1 问题速查表问题现象优先排查方向具体操作U-Boot启动时网络初始化超时PHY自协商太慢设备树加phy-connection-type或强制速度mii命令读不到PHYMDIO时钟/供电/复位量时钟、供电降低MDC频率PHY ID读出来是0xFFFFPHY没上电或复位未释放量VDD和复位引脚link up但ping不通MAC配置或RGMII延时检查DMA、FIFO、延时参数ping通但TFTP丢包RGMII延时或FIFO阈值调延时增大FIFO阈值换PHY后驱动不匹配PHY ID掩码不对确认phy_id_mask覆盖正确位网络时通时不通时钟不稳定或电源纹波示波器量时钟和电源6.2 几个容易被忽略的坑坑一PHY地址冲突。有些板子设计时两个PHY共用一个MDIO总线地址配重了。扫描时只能找到一个另一个永远找不到。解决方法是改硬件地址或者用不同的MDIO总线。坑二GPIO复用没配对。很多SoC的MDIO/MDC引脚跟GPIO复用U-Boot的pinctrl配置如果没把这两个引脚设成MDIO功能mii命令就会超时。检查设备树的pinctrl节点。坑三PHY的compatible字符串。有些PHY驱动要求设备树里PHY节点的compatible跟驱动里的of_match匹配。如果只写了ethernet-phy1没写具体型号可能匹配到通用驱动功能不全。坑四U-Boot的PHY驱动没编译进去。CONFIG_PHY_REALTEK、CONFIG_PHY_MICREL这些配置项如果没开PHY扫描到了也匹配不到驱动。检查.config。坑五MDIO总线注册失败但没报错。有些MAC驱动在mdiobus_register失败后不返回错误继续往下走导致后面phy_connect找不到PHY。加日志确认MDIO总线注册成功。6.3 调试工具与命令速查U-Boot里网络相关的命令不多但够用# 查看网络设备 net list # 设置IP setenv ipaddr 192.168.1.100 setenv serverip 192.168.1.1 setenv netmask 255.255.255.0 setenv gatewayip 192.168.1.1 # ping测试 ping 192.168.1.1 # TFTP下载 tftp 0x80000000 zImage # MII寄存器操作 mii read phyaddr reg mii write phyaddr reg value mii dump phyaddr # 查看环境变量 printenv如果U-Boot里没有mii命令可能是CONFIG_CMD_MII没开。这个命令在调试阶段非常有用建议默认打开。7. 移植新PHY驱动的关键步骤7.1 从零添加一个PHY驱动假设你拿到一颗新PHY厂商没提供U-Boot驱动你需要自己加。步骤大致如下第一步在drivers/net/phy/下新建一个C文件比如myphy.c。定义phy_driver结构体填上name、uid、mask、features。第二步实现必要的回调。最少要实现config和startup。config里做PHY特有的初始化比如配置LED、关闭节能模式、设置RGMII延时。startup里可以覆盖默认的自协商流程。第三步在drivers/net/phy/Makefile里加一行obj-$(CONFIG_PHY_MYPHY) myphy.o。第四步在drivers/net/phy/Kconfig里加配置项config PHY_MYPHY bool My PHY support depends on DM_ETH help Support for My PHY.第五步在板子的defconfig里加CONFIG_PHY_MYPHYy。第六步在设备树的PHY节点里加compatible myphy,myphy-1并在驱动里加of_match表。7.2 PHY驱动里必须处理的几个寄存器不管哪家PHY有几个寄存器是必须正确处理的BMCR0x00bit 15是复位bit 12是自协商使能bit 13是速度选择1100M010Mbit 8是双工。写这个寄存器可以强制速度双工。BMSR0x01bit 2是link statusbit 5是自协商完成。读这个寄存器判断链路状态。注意BMSR是锁存寄存器读两次才能拿到最新值。PHYID1/20x02/0x03PHY ID用于匹配驱动。ANAR0x04自协商通告配置本端支持的速度和双工。MII_STAT10000x09千兆状态bit 10是1000BASE-T halfbit 11是1000BASE-T full。MII_ESTATUS0x0f扩展状态bit 15是1000BASE-X fullbit 14是1000BASE-X halfbit 13是1000BASE-T fullbit 12是1000BASE-T half。7.3 一个最小可用的PHY驱动模板#include common.h #include phy.h static int myphy_config(struct phy_device *phydev) { int val; /* 关闭节能模式 */ val phy_read(phydev, 0x10); val ~(1 8); phy_write(phydev, 0x10, val); /* 配置RGMII延时 */ phy_write(phydev, 0x1e, 0x0002); val phy_read(phydev, 0x1c); val | (1 4) | (1 5); phy_write(phydev, 0x1c, val); phy_write(phydev, 0x1e, 0x0000); return 0; } static int myphy_startup(struct phy_device *phydev) { int ret; ret genphy_update_link(phydev); if (ret) return ret; return genphy_parse_link(phydev); } static struct phy_driver myphy_driver { .name My PHY, .uid 0x12345678, .mask 0xffffffff, .features PHY_GBIT_FEATURES, .config myphy_config, .startup myphy_startup, }; U_BOOT_PHY_DRIVER(myphy_driver);这个模板里config做了两件事关节能、配RGMII延时。startup直接调用了通用函数。U_BOOT_PHY_DRIVER宏负责把驱动注册到全局链表。提示phy_read和phy_write是U-Boot PHY框架提供的封装内部会调用MDIO总线的读写回调。不要直接操作MAC寄存器。8. 从net到mac到phy的完整数据流回顾8.1 发送路径当你敲下tftp命令数据流是这样的net/层的TFTP代码构造UDP包调用eth_send()。eth_send()找到当前eth_device调用它的send回调。MAC驱动的send函数把数据包写入DMA描述符启动发送。MAC控制器组帧加上前导码、CRC通过RGMII接口发给PHY。PHY把数字信号转成差分模拟信号发到网线上。8.2 接收路径PHY从网线收到差分信号转成数字信号通过RGMII发给MAC。MAC控制器做CRC校验把数据包写入DMA缓冲区触发中断。MAC驱动的中断处理函数调用net_process_received_packet()。net/层解析以太网头如果是ARP就回复如果是IP就往上送。最终TFTP代码收到数据写入内存。8.3 关键数据结构的关系struct eth_device ├── name ├── iobase ├── priv (指向MAC驱动的私有数据) ├── init / send / recv / write_hwaddr └── phydev (指向struct phy_device) struct phy_device ├── bus (指向struct mii_bus) ├── addr (PHY地址) ├── phy_id ├── drv (指向struct phy_driver) └── speed / duplex / link struct mii_bus ├── name ├── read / write (MDIO读写回调) └── priv (指向MAC驱动的私有数据)理解这三个结构体的关系整条链路就通了。eth_device是协议层的入口phy_device是PHY层的入口mii_bus是MDIO层的入口。MAC驱动负责把这三个东西串起来。9. 几个实战中总结的经验9.1 关于PHY自协商自协商虽然方便但在某些场景下会出问题。比如对端是强制千兆的交换机而PHY默认自协商双方协商结果不一致可能导致link up但速度不对。这时候要么把对端也改成自协商要么在U-Boot里强制PHY的速度双工。强制的方法是在PHY驱动里覆盖config_aneg回调或者在设备树里加fixed-link节点。fixed-link的写法phy0: ethernet-phy1 { reg 0x1; fixed-link { speed 1000; full-duplex; }; };9.2 关于MDIO时钟MDIO规范规定MDC频率最高2.5MHz。但实际调试时我建议先用1MHz左右确认能通之后再往上调。有些PHY对MDC频率敏感太快了会读错。SoC的MDIO时钟分频系数通常在MAC的某个寄存器里具体看手册。9.3 关于RGMII延时RGMII延时是调试中最容易出问题的地方。我的经验是先用PHY默认延时如果丢包就调MAC侧延时每次调0.25ns左右用大文件传输测试。如果MAC侧调不了就调PHY侧的扩展寄存器。两边只能调一边调重了会采样错误。9.4 关于U-Boot版本不同版本的U-BootPHY框架和MAC驱动差异很大。2016年之前的版本很多还在用phy_register_device的老接口2018年之后基本都走设备树和phy_connect了。移植代码的时候一定要确认版本不要拿老代码往新U-Boot上套。9.5 关于调试顺序我的调试顺序永远是先MDIO再PHY ID再link再ping最后TFTP。每一步确认通过了再往下走。跳过步骤直接查上层往往会在错误的方向上浪费大量时间。10. 后续可以扩展的方向如果你已经把U-Boot网络调通了接下来可以往这几个方向深入一是多网口支持。很多板子有两个甚至四个网口需要理解U-Boot怎么管理多个eth_device怎么在ethact之间切换。二是网络启动优化。U-Boot的TFTP下载速度可以优化比如增大TFTP窗口、调整DMA描述符数量、开启校验和卸载。三是PHY固件加载。有些PHY需要加载固件才能工作U-Boot里可以通过MDIO写入固件数据。四是安全启动下的网络。如果开了secure boot网络镜像的签名验证流程需要跟U-Boot的verify框架结合。五是从U-Boot到内核的网络交接。U-Boot把网络配置MAC地址、PHY状态传给内核的方式以及内核怎么复用U-Boot已经初始化好的PHY。这些方向每一个都够写一篇长文等有机会再展开。网络这条链路从net到mac到phy看起来简单实际上每个环节都有细节。把这条链路吃透板级bringup的效率会高很多。