搞过U-Boot网络的人应该都有体会:明明在Linux底下网络跑得好好的,一进U-Boot就各种不通,ping不通、tftp超时、MAC地址读出来是一串F,甚至网口干脆不识别PHY。这类问题我前前后后踩了无数回,最后发现根源大多不是硬件坏了,而是没把U-Boot这套精简版网络栈的分层逻辑理顺。这篇文章就围绕U-Boot网络从net层到MAC再到PHY的完整链路,把每层干什么、怎么分工、调试时先查什么都给你盘清楚,适合正在移植板子、调网口驱动、或者被u-boot网络问题折磨的嵌入式工程师参考。我会把实际调试中用到的命令、寄存器、排查思路一并写出来,照着操作基本能定位九成以上问题。
1. U-Boot网络栈的整体骨架:三层分工,各管一摊
1.1 先从net核心层说起
U-Boot的net层在源码里对应net/目录,net.c是核心。它管的是协议逻辑:构造ARP请求、解析ARP应答、组ICMP Echo、处理TFTP的WRQ/DATA/ACK交互,还包括环境变量里的serverip、ipaddr、netmask这些参数的管理。这一层有一个核心概念,就是尽力不感知具体硬件细节,它只依赖一个抽象接口:eth_send()发一帧、eth_recv()收一帧。
这一点和Linux的网络协议栈很像,只是砍掉了socket、路由表、邻居表这些硕大子系统,保留了一个极简版本的收发包循环。U-Boot的net_loop()会一直轮询,调用eth_rx()处理收到的包,再把需要发出的包交给eth_send()。如果底层驱动说自己有数据,net层才会去处理;没有数据就继续跑自己的超时逻辑。
你要理解一点:U-Boot的网络栈是单线程、非抢占、无中断驱动的。也就是说,TFTP传输过程中CPU被占住,包是一轮一轮查询收上来的。这和Linux的NAPI、软中断完全是两个世界。很多驱动移植过来,第一件事就是把中断模式的收包改成轮询模式,否则U-Boot基本没法用。
1.2 MAC层:net层和PHY之间的桥梁
MAC层在U-Boot里体现为以太网控制器驱动,DMA控制器、FIFO、MAC地址寄存器、MDIO控制器都在这一层。U-Boot的MAC驱动在drivers/net/下,常见的像designware.c(DW GMAC)、fec_mxc.c(NXP i.MX系列FEC)、macb.c(Atmel/Microchip)、sunxi_emac.c(Allwinner),它们都封装成struct eth_ops接口。
MAC驱动干的活很明确:
- 初始化MAC控制器,配置DMA描述符(descriptor ring)
- 把待发送的数据包搬到TX描述符,启动DMA发送
- 接收时从RX描述符取包,交给上层
- 通过MDIO/MII总线访问PHY寄存器,做协商、读状态
- 管理MAC地址寄存器,确保源MAC能填对
注意,MAC驱动负责的是数据链路层的控制器部分,它不负责把电信号变成线路上的差分信号,那是后面PHY的事。所以你可以把MAC理解为"会讲数据帧协议但不会开口说话"的层,真正开口的是PHY。
1.3 PHY层:最后一公里的模拟前端
PHY(Physical Layer)是物理层收发器,板子上那颗独立芯片,或者集成在CPU/交换芯片内部的以太网PHY。它负责:
- 将MAC发来的并行数据编码成线路上的差分信号
- 负责自协商(Auto-Negotiation)确定速率、双工模式
- 提供链路状态检测(Link up/down)
- 提供MDI/MDIX自动翻转等等功能
U-Boot对PHY的管理在drivers/net/phy/下,核心数据结构是struct phy_device。它通过MDIO总线读写PHY寄存器,phy_connect()把PHY挂到MAC上,phy_startup()完成协商并设置MAC侧的速率和双工模式。
这里有个很关键的实现细节:PHY驱动通常只干配置和状态检测,数据流本身不经过PHY驱动的buffer,数据包始终是在MAC的DMA描述符和FIFO之间流动,PHY只是透传。所以遇到丢包、错包问题,不要总觉得是PHY驱动在搞鬼,很多时候PHY层面只是"信号质量不好"的背锅侠。
1.4 为什么不直接让MAC驱动裸调PHY
有人会问:U-Boot体量这么小,为什么不像早期江湖代码那样把PHY初始化直接写在MAC驱动里,而是单独搞一套phy_device框架?答案就在可移植性上。同一颗PHY芯片(好比说RTL8211F、AR8033)可能被用在几十种板卡上,接在NXP、TI、Qualcomm不同MAC控制器后面。如果PHY驱动写死在MAC驱动里,每移植一块板子就要复制改一遍PHY初始化代码,维护成本直接爆炸。
U-Boot的phy_device框架和Linux的phy subsystem思路一致:PHY驱动只负责"怎么操作这颗PHY芯片",MAC驱动只负责"怎么操作这个MAC控制器"。两者通过MDIO总线连接,phy_connect()用PHY地址加PHY ID去找匹配的驱动,phy_startup()把协商结果再告诉MAC。这样一份PHY驱动可以服务所有MAC,MAC驱动也不必关心最终选了哪颗PHY。
2. 一次完整的数据通路:从ICMP请求到网线信号
2.1 应用程序视角:ping到底做了什么
假设你插好网线,在U-Boot命令行敲一个ping 192.168.1.100。net层会先查ARP表,发现没有目标IP对应的MAC地址,于是先发一个ARP请求,广播问"谁是192.168.1.100,请告诉192.168.1.10"。对端回应ARP应答后,net层把IP-MAC对应关系记录下来,再组ICMP Echo Request,调eth_send()发出去。
在eth_send()里面,实际发生的事比想象中多一些。U-Boot的eth_send()会先拿到当前eth_current的struct udevice,然后调用eth_get_ops(dev)->send(dev, packet, length)。这个send回调就是MAC驱动实现的,它拿到net层组好的完整以太网帧(目标MAC、源MAC、类型字段、payload),把它写进TX描述符指向的DMA缓冲区,然后置位描述符,启动发送。
2.2 数据包过MAC控制器
以DesignWare GMAC为例,designware.c里dw_eth_send()干的事是这样的:把待发送数据按128字节对齐的要求拷贝到DMA TX缓冲区,写描述符的状态位和长度字段,然后读一个GMAC_TX_POLL寄存器触发DMA搬运。DMA引擎会把缓冲区里的数据按字节流推给MAC内核,MAC内核负责加前导码、加CRC、加帧间隙,然后推给GMII/RGMII接口。
这里有个很容易被忽略的点:MAC可能要求缓冲区在特定地址边界对齐,比如32字节、64字节对齐。DMA无法访问的地址范围也是坑。你往malloc出来的地址放数据没问题,但如果缓冲区来自奇怪的堆地址,DMA写回可能出错。所以我一般建议网卡缓冲区分配走memalign,别裸用malloc。
2.3 信号最终从PHY出去
MAC通过RGMII/GMII接口把并行数据送到PHY,PHY内部完成编码(1000BASE-T用PAM5,100BASE-TX用MLT-3,10BASE-T用曼彻斯特编码),加扰、电平转换,然后推上双绞线。对端PHY解码还原成帧,交给对端MAC,上协议栈,最终返回ICMP应答。
所以一次ping的完整路径是:
net层组包 -> eth_send() -> MAC驱动写DMA描述符 -> DMA搬运到MAC内核 -> MAC加前导/CRC -> RGMII送PHY -> PHY编码上线路 -> 对端解码 -> ...
收包路径就是完全反过来。MAC收到完整帧之后,DMA把数据放到RX描述符缓冲区,产生接收中断(U-Boot里可能只置标志位或回调),net层轮询到有包,调用eth_rx()把数据取走。net_process_received_packet()会解析以太网类型字段,IP包走ARP/ICMP/TFTP对应的处理函数。
2.4 收发路径上的关键判断点
数据通路上的每个环节,都可以有一个调试探测点:
| 层 | 关键的判断依据 | 常用调试手段 |
|---|---|---|
| net核心层 | 是否组了包、是否发出去 | net_loop日志、debug宏,eth_hdr结构检查 |
| MAC驱动 | DMA描述符是否正常、中断标志/轮询标志 | 读MAC中断状态寄存器、描述符地址与状态字 |
| MAC-PHY接口 | RGMII时钟、TXEN、TXD信号是否正常 | 示波器/逻辑分析仪抓RGMII波形 |
| PHY寄存器 | 链接状态、协商速率、中断状态 | mdio命令读写PHY寄存器 |
| 物理线路 | 线序、链路信号 | 网线测试仪、对端交换机指示灯 |
实际工作中,我从PHY寄存器开始看,然后逐层往上,基本能快速收窄范围。如果你一上来就扒代码,往往看半天也找不到问题在哪,因为你跳过了"看现场信号"这个环节。
3. 关键数据结构与驱动接口,别被“U-Boot没有驱动模型”带偏了
3.1 eth_ops与udevice:驱动模型下的网卡抽象
现代U-Boot(从2016年之后基本都支持DM,driver model)里,网卡驱动分两个层面:UCLASS_ETH表示以太网控制器,struct eth_ops定义操作接口;UCLASS_MDIO表示MDIO总线,struct mdio_ops定义MDIO读写操作。网卡驱动核心要实现的回调包括:
start(dev):初始化MAC、分配描述符、启动收发stop(dev):关闭网卡、释放缓冲区send(dev, packet, length):发送一帧recv(dev, flags):检查是否有收包,有则返回包地址free_pkt(dev, packet, length):释放接收缓冲区read_rom_hwaddr(dev):从板载eFuse/EEPROM读取MAC地址(可选)
这些回调是MAC驱动实现的重点。调网卡驱动时,你只要保证这几件事:start之后能正常收发包、send能把数据可靠送出去、recv能被net轮询及时调用、MAC地址能正确读到。
3.2 phy_device与phy_driver:PHY侧的抽象
struct phy_device里最重要的字段是:addr(PHY地址)、phy_id(PHY识别ID)、supported(支持的速率/双工能力)、advertising(广播的能力)、link(当前链接状态)、speed、duplex(协商结果)。
struct phy_driver则定义操作函数集:probe、config、startup、shutdown,以及read_status、config_aneg、config_init这些核心函数。一颗新PHY的移植,其实就是照着drivers/net/phy/realtek.c或atheros.c这些模板,把寄存器初始化序列填对,把read_status里的状态解析写对。
很多人移植PHY驱动时只抄config里的寄存器配置,不看read_status。结果就是PHY配置好了,但U-Boot读到的link始终是0。这时候你要去读PHY的BMSR寄存器(寄存器1),看看bit2是不是1,如果硬件已经link up,而驱动读出来是down,那就要检查read_status实现是否读对了寄存器位。
3.3 MDIO总线的设备树固定配置
设备树里以太网控制器节点的基本长这样:
&gmac0 { status = "okay"; phy-mode = "rgmii-id"; phy-handle = <&phy0>; mdio { #address-cells = <1>; #size-cells = <0>; phy0: ethernet-phy@0 { reg = <0>; reset-gpios = <&gpio0 12 GPIO_ACTIVE_LOW>; reset-delay-us = <10000>; }; }; };phy-mode里的rgmii-id表示RGMII接口且由PHY内部提供发送和接收的时钟延迟。如果MAC侧有延迟,你就要用rgmii-txid、rgmii-rxid或者rgmii,时钟延迟配置错了,链路能起来但跑千兆会时不时掉包,这类问题极其隐蔽,我后面会专门说。
reg = <0>是PHY的MDIO地址,这个地址由PHY芯片的ADDR引脚上下拉决定。如果你的PHY实际地址是1或4,但设备树写0,那你用mdio read会读到全F(也就是无应答),网卡初始化时phy_connect()会失败。所以遇到PHY读不到,先确认MDIO地址和硬件配置对不对。
3.4 环境变量对网络行为的影响
U-Boot网络层的行为受环境变量影响很大,调网络问题先看一眼环境变量往往能少走弯路:
ethaddr:MAC地址,如果没设置或设置成00:00:00:00:00:00,ARP和DHCP都可能直接失败ipaddr:本机IP,ping自己都不同先查这个serverip:TFTP服务器地址,配错了看起来像"TFTP超时"netmask:子网掩码,影响ARP广播范围autoload:如果为yes,dhcp命令成功后会尝试下载启动文件,很多人以为是网络问题,其实是autoload在搞事
我见过最经典的一个坑:板子ethaddr是空的,U-Boot自动从fuse里读了一个MAC地址,但是烧写新固件时fuse被锁死读出来全F,于是一切ARP、DHCP全部失败,看起来就像网卡坏了。查了半天,最后发现只是ethaddr全F,手工setenv ethaddr 00:11:22:33:44:55保存之后立刻恢复正常。
4. 实战排查三板斧:不动代码先看状态
4.1 第一板斧:mdio命令直接读PHY寄存器
U-Boot自带MDIO调试命令,这块必须熟练。进入U-Boot命令行,执行:
U-Boot> mdio list它会列出当前MDIO总线上的PHY设备。如果这里列出的PHY和你板上实际的不一样(显示两个PHY或者一个都没有),先查设备树里mdio节点和PHY的reg。
U-Boot> mdio read 0 2这是读PHY地址0的寄存器2(PHY ID高16位)。合理的返回值类似0x001c(Realtek RTL8211系列)或0x004d(Marvell 88E1512/AR8033等)。如果读出来是0xffff,说明PHY没应答,问题大概率在PHY电源、复位脚、MDIO地址或MDIO拉电阻上。
另一个必看寄存器是BMSR(寄存器1),bit2是链路状态。mdio read 0 1后,看bit2是否为1。如果你插着网线但这一位是0,那从PHY角度说链路就没建立,可能是线序、对端设备或者PHY配置问题。
一块常见PHY寄存器的速查表我贴在这里:
| 寄存器地址 | 名称 | 关键位 |
|---|---|---|
| 0 | BMCR(控制) | bit13-14速率选择,bit8全双工,bit12自协商使能 |
| 1 | BMSR(状态) | bit2链路状态,bit5自协商完成,bit6-11能力 |
| 2-3 | PHY ID | 厂商代码与型号 |
| 4 | ANAR(自协商通告) | bit5-7 千兆,bit8-11百兆/十兆 |
| 5 | ANLPAR(对端能力) | 对端通告的能力 |
| 6 | ANER(扩展状态) | 自协商错误指示 |
| 9 | 千兆控制 | 千兆主从、千兆能力 |
4.2 第二板斧:mii命令与mii dump全量查看
如果你的U-Boot较老,没有mdio命令,那mii命令同样能干这活。mii info会显示PHY基本信息和协商结果,mii dump可以打印PHY所有寄存器,非常直观。
U-Boot> mii info U-Boot> mii dump 0 0建议第一次拿到一块新板子时,把mii dump的输出完整保存下来,作为"健康基线"。之后调坏了,对照这份基线,很快就能看出哪个寄存器不对。我每移植一块板子都会做这件事,尤其是ANAR、BMSR、PHY ID这几个寄存器,能帮你少踩很多坑。
4.3 第三板斧:net list和ethact确认当前网卡
U-Boot支持多网卡,net list列出所有UCLASS_ETH设备,ethact设置当前活动网卡。如果在双网卡板子上,你明明在调试eth1,但默认网卡是eth0,那ping不通太正常了。
U-Boot> net list U-Boot> setenv ethact eth1 U-Boot> ping 192.168.1.100这里有个坑:很多人只改ethprime或ethact,但启动脚本里又用tftp,tftp命令会重新选择网卡。确保你设置的当前网卡和实际使用的环境变量一致,否则你会看到"tftp用的不是我在调的网卡"这种迷惑行为。
4.4 实测量化:没有示波器也能判断问题层
当板子ping不通,且PHY寄存器显示link已经up,这时候问题集中在MAC驱动和net层。有两种低成本验证工具:一是tftp一个大文件(200MB以上),统计传输速度,如果速度远远低于协商速率,说明可能有重传/丢包;二是直接打开U-Boot编译时的CONFIG_DEBUG或CONFIG_NET_DEBUG,看收发统计。
还有一种更狠的验证方法:在MAC驱动send回调里打印每个包的length和描述符状态字,对照对端tcpdump抓包。如果描述符状态显示已经写完且回读无误,但对端什么都没收到,那问题基本锁定在MAC到PHY的接口(RGMII时序、时钟、引脚mux配置)。
5. 高频问题与排查经验速查
5.1 ping不通的九个检查点
把ping不通按现象拆,可以排列组合成一个速查清单:
| 现象 | 优先排查 |
|---|---|
| ping对端IP不通,但ping自己通 | 检查ipaddr、netmask、ARP是否解析成功 |
| ping别人不通,但PHY link up | 检查MAC地址、vlan配置、对端防火墙 |
| 大包不通,小包通 | 检查MTU、巨型帧支持、RGMII时序 |
| 第一次通,第二次不通 | 检查ARP缓存是否过期、下一条是否被net_loop超时清掉 |
| 一直超时,net_loop卡住 | 检查phy_startup是否成功,link是否被反复检测 |
| 网口灯亮但ping不通 | 检查DMA描述符、MAC中断状态寄存器 |
| 只有tftp能通,ping不通 | 检查ICMP处理是否被裁剪(CONFIG_CMD_PING) |
| 板子冷启动不通,热启动通 | 检查PHY复位时序、供电稳定性 |
按这个顺序排查,80%的问题能在十分钟内定位。
5.2 PHY id读不出来
mdio read返回全F,或者phy_connect()报PHY not found。排查顺序:
- 检查PHY芯片供电是否正常,很多PHY有独立的1.0V/1.8V供电,电压不对会完全无应答。
- 检查PHY复位引脚。复位引脚一直拉低,PHY永远处于复位状态,自然读不到。U-Boot设备树里的
reset-gpios配了没有?reset-delay-us够不够?我见过复位释放后只等100us就访问PHY,结果失败的,一般至少等10ms才稳。 - 检查MDC/MDIO引脚mux。很多SoC的MDIO引脚是复用的,默认功能不是MDIO,必须在设备树或board代码里设置为GPIO/MDIO功能。
- 检查MDIO总线上的上拉电阻。MDIO规范要求上拉,如果板子漏了,读ID偶尔能读通,多读几次就全F,这类问题很隐蔽。
- 最后看一眼PHY的ADDR引脚。如果PHY地址由引脚决定,硬件上ADDR0浮空或下拉,那地址可能不是你预期的那个。
5.3 RGMII时序问题:千兆掉包的最大元凶
换一块新板子或新PHY,最常见的疑难杂症就是:百兆正常,千兆启动后频繁丢包,或者干脆link up后网卡完全不通。RGMII接口的时钟延迟配置不对是主因。
RGMII标准里,MAC和PHY在各自时钟边沿附近采数据,但PCB走线会引入延迟,导致时序不满足。解决方法是选择在哪一侧加延迟,设备树phy-mode的选项直接对应这个选择:
rgmii:不加延迟,适用于MAC内部已经加延迟或者走线非常短的板子rgmii-id:PHY内部加TX和RX两个延迟rgmii-txid:只加TX延迟rgmii-rxid:只加RX延迟
改phy-mode不用改代码,编译一次就能测。我建议调试时先试rgmii-id,如果还有问题,再逐个试rgmii-txid和rgmii-rxid。还有一种情况是U-Boot配置对了,但Linux下又不同,因为Linux可能由phy驱动覆盖了延迟配置,这类问题比U-Boot本身更头疼。
5.4 TFTP超时的真正原因
TFTP看起来协议简单,实际超时原因多得很:
serverip设错或者根本不通,表现为连续超时后报T FTP error- UDP回包被防火墙吃掉,Linux的tftp-hpa默认端口69,但回包来自随机端口,Windows防火墙经常拦,U-Boot侧看起来就是超时
- 对端tftp服务没启动,或者目录权限不对,表现为能发出WRQ但一直等不到ACK
- U-Boot侧缓冲区分配失败,大文件下载到特定地址触发内存覆盖,导致收包异常
优先做的是在服务器端开tcpdump,看U-Boot的请求到没到。没到就是网络不通,到了不答应就是TFTP服务或防火墙问题。
5.5 MAC地址=全F
ethaddr全F会让ARP直接失效。原因常见几种:
CONFIG_NET_RANDOM_ETHADDR没开,板上eFuse没有烧录MAC,导致读取失败- 驱动脚本从EEPROM读MAC的时序不对,读出来全F
- 环境变量没保存,下次启动又空
处理方式:确认硬件是否有烧录MAC的eFuse或EEPROM;临时用setenv ethaddr设置一个有效MAC;产品量产时必须解决eFuse/EEPROM读MAC的问题,否则每一台板子的MAC都一样,网络直接崩。
6. 把U-Boot网络调顺的几个实践心得
U-Boot的网络栈是嵌入式开发里少有的"麻雀虽小五脏俱全"的精简系统。我调过不少板子之后最大的体会是:先在U-Boot里把网络打通,后面Linux的网络问题基本都能少一半。因为U-Boot的网络链路更短,变量更少,排查起来比Linux容易得多。反过来,如果你在U-Boot里都调不通,贸然去调Linux,大概率会被各种驱动的自动协商策略绕晕。
调试顺序我建议固定成一条流水线:先确认PHY能被MDIO访问到,再确认PHY link up并读出协商速率,接着确认MAC能发包收包,最后才去net层查协议问题。每一个环节都有明确的"证据":PHY的寄存器值、DMA描述符状态、对端tcpdump抓包、U-Boot日志。不要猜,不要跳步,按顺序验证。
最后说一个技巧:给板子写一份"网络自检脚本",把mdio list、mdio read关键寄存器、mii info、ping、tftp这些命令打包成一个U-Boot脚本,每次拿到新板子先跑一遍,输出保存下来。移植过程中任何一次改动后对比一下这份基线,能非常快地发现是不是什么东西悄悄变了。我自己的板子公司后来把所有新板卡的验收都集成进了这个自检流程,省下的排查时间,比当初写脚本花掉的时间多十倍不止。