☰
u-boot设备模型DM初始化核心:board_init_r的三大跃迁
2026/10/1 15:58:25 网站建设 项目流程

1. 为什么说board_init_r是u-boot设备模型的“龙门”——从裸机到驱动世界的临界点

很多人学u-boot设备模型(Device Model,简称DM)时,卡在board_init_f和board_init_r之间。他们反复看drivers/core/下的代码,翻遍struct udevice和struct driver定义,却始终没搞明白:那些驱动怎么就“活”过来了?不是driver_init()早就执行过了吗?不是dm_init_and_scan()也调用了?那board_init_r里到底还干了什么?——这恰恰是绝大多数人对DM理解最模糊、最危险的断层。

我第一次调试一块新ARM64板子时,就在board_init_r里卡了整整三天。串口打印停在dm_init_and_scan()之后,但eth0死活不注册,mmc0探测不到卡,i2c0连设备树节点都读不出来。用dm tree命令一看,整个设备树只挂了根节点/,下面空空如也。当时以为是设备树写错了,重写了五版dts;又怀疑是CONFIG_DM_*宏没开全,把Kconfig翻来覆去grep了十几遍;最后甚至怀疑u-boot版本太老,换到2023.04还是不行。直到某天凌晨两点,我突然意识到:dm_init_and_scan()只是搭了个骨架,而真正让骨架长出血肉、接上神经、能呼吸能响应的,全在board_init_r后续几十行代码里。这几十行,就是DM从“静态描述”走向“动态运行”的龙门。

这个龙门之所以关键,在于它完成了三个不可逆的跃迁:
第一,从全局单例到实例化对象——dm_init()只创建了一个gd->dm_root指针,而board_init_r中dm_init_and_scan()会根据设备树真实节点,为每个compatible匹配的驱动分配独立的struct udevice *dev,并完成内存绑定、父-子-兄弟链表构建;
第二,从声明式配置到命令式初始化——设备树里写的status = "okay"只是“允许启动”,而board_init_r里调用的device_probe()才是真刀真枪地执行驱动的.probe()函数,读寄存器、设时钟、拉GPIO、发复位信号;
第三,从内核视角到u-boot语境的语义转换——Linux内核的of_platform_populate()走的是platform_bus路径,而u-boot的DM必须兼容simple-bus、amba_bus、pci甚至自定义总线,board_init_r里的dm_scan_fdt()正是做这种总线语义映射的翻译官。

所以,“屠龙刀在手”不是指你拿到了u-boot源码,而是你真正看清了board_init_r这一段——它像一把解剖刀,把DM的初始化流程切开,露出血肉、神经和脉络。接下来四节,我就带你一帧一帧拆解这段代码,不讲概念,只看寄存器、看调用栈、看内存布局、看实际跑飞的bug怎么定位。你不需要背熟所有API,但必须知道每一行代码执行后,gd->dm_root指向的那棵树,哪个节点多了一条边,哪个设备多了一块内存,哪个驱动多了一次printf输出。

提示:本文所有分析基于u-boot 2023.04 LTS版本,主线代码路径为arch/arm/lib/board.c→common/board_f.c→common/board_r.c。如果你用的是2021.04或更早版本,dm_init_and_scan()可能还在board_init_f里,那是旧DM架构,本文不覆盖。请先确认你的CONFIG_DM和CONFIG_OF_CONTROL已启用,且CONFIG_SPL_DM未开启(SPL阶段DM逻辑完全不同)。

2.dm_init_and_scan()不是终点,而是起点:骨架搭建的三步原子操作

很多开发者看到dm_init_and_scan()返回0就以为DM初始化成功了,这是最大的认知陷阱。实际上,这个函数只是完成了骨架搭建的前半场,真正的“血肉填充”发生在它返回之后的board_init_r主流程中。我们得把它拆成三个原子操作来看,每个操作都对应一次内存分配、一次链表插入、一次函数跳转——少一个,设备就“瘫痪”。

2.1 第一步:dm_init()——分配根节点与全局管理器(Root + Manager)

dm_init_and_scan()的第一行就是dm_init(),它的作用极其朴素:给DM系统分配一块固定大小的内存池,并初始化根设备节点。这不是简单的malloc(),而是u-boot特有的memalign()+memset()组合:

// drivers/core/root.c int dm_init(void) { struct driver_model_data *dm; // 分配一块对齐的内存,大小为 CONFIG_DM_MAX_DEVICES * sizeof(struct udevice) // 注意:CONFIG_DM_MAX_DEVICES 默认是 1000,但可被 board config 覆盖 dm = memalign(ARCH_DMA_MINALIGN, sizeof(*dm) + CONFIG_DM_MAX_DEVICES * sizeof(struct udevice)); if (!dm) return -ENOMEM; // 初始化根节点:/,类型为 UCLASS_ROOT,无父节点,无驱动 dm->root = &dm->devices[0]; device_init(dm->root, NULL, UCLASS_ROOT, 0); // 将全局指针 gd->dm_root 指向它 gd->dm_root = dm->root; return 0; }

这里的关键细节在于CONFIG_DM_MAX_DEVICES。它不是理论最大值,而是编译期硬编码的设备实例上限。如果你的板子设备树里有1024个节点(比如带大量I2C传感器、SPI Flash、PCIe设备),而CONFIG_DM_MAX_DEVICES=1000,那么第1001个设备就会因内存越界直接导致memcpy()崩溃,串口输出乱码。我见过三次类似问题:一次是客户在dts里加了64路ADC通道,一次是某国产SoC把所有GPIO控制器都列成独立节点,还有一次是误把#address-cells写错导致设备树解析出上千个无效节点。解决方法不是调大这个值(会吃掉宝贵的RAM),而是用fdtgrep检查真实有效节点数:

# 在编译后的u-boot.dtb上执行 $ fdtget -t s u-boot.dtb /soc/i2c@... compatible | wc -l # 查I2C子节点数 $ fdtget -t s u-boot.dtb / soc@... reg | wc -l # 查所有reg属性节点

注意:dm_init()分配的内存池是只增不减的。u-boot不会在运行时释放某个udevice占用的空间,因为无法保证驱动卸载的安全性(毕竟u-boot没有内存回收机制)。所以这个池子必须在编译前就估算准确——我的经验是:基础板级设备(CPU、DRAM、UART、MMC)约50个,每路I2C挂5个传感器算25个,每路SPI挂2个Flash算10个,PCIe设备按1:1算,最后再加30%冗余。宁可多留200个,也不要少1个。

2.2 第二步:dm_scan_fdt()——设备树解析与节点映射(Parse + Map)

dm_init()完成后,dm_init_and_scan()调用dm_scan_fdt(gd->fdt_blob, false)。这才是真正开始“搭骨架”的环节。它不做任何硬件操作,只做三件事:遍历设备树、匹配驱动、创建udevice实例。整个过程不碰寄存器,纯内存操作。

核心逻辑在drivers/core/fdt.c的dm_scan_fdt_node()递归函数中。它从/节点开始,对每个子节点执行:

  1. 状态过滤:检查status属性。如果status = "disabled"或不存在,直接跳过;如果status = "fail",打日志但继续;只有"okay"或未定义才进入下一步;
  2. 驱动匹配:遍历所有已注册驱动(gd->dm_root->uclass->drivers链表),用of_match_device()比对compatible字符串。注意:匹配是最长前缀优先,比如驱动支持"vendor,chip-v2",设备树写"vendor,chip-v2"就精确匹配,写"vendor,chip"则可能匹配到v1驱动(如果v1驱动也注册了);
  3. 实例创建:为匹配成功的节点分配struct udevice *dev(从dm_init()分配的池子里取),调用device_bind_by_name()绑定驱动、设置name、seq、parent指针,并插入到父节点的child_head链表中。

这个过程会产生一个关键副作用:设备节点的seq编号不是按设备树顺序,而是按匹配成功顺序。比如你的dts里&uart0在前面,&i2c0在后面,但如果I2C驱动模块编译进了u-boot而UART驱动是模块化加载(CONFIG_DM_SERIAL=n),那么i2c0的seq可能是0,uart0的seq反而是1。这直接影响dm devlist命令的输出顺序,也影响某些依赖seq做索引的驱动(如serial_pl011的base地址计算)。

我曾在一个项目中遇到console=参数失效的问题,最终发现是因为serial@...节点的seq被I2C节点抢占,导致serial_find_console_or_pre_console()查seq=0时拿到的是I2C设备而非UART。修复方案很简单:在dts里给UART节点加u-boot,dm-spl-init;属性,强制它在SPL阶段就初始化,确保seq靠前。

2.3 第三步:device_probe()——驱动探针与资源激活(Probe + Activate)

dm_scan_fdt()返回后,dm_init_and_scan()的最后一行是device_probe(gd->dm_root)。这才是让骨架“活过来”的关键一击。它递归遍历整棵树,对每个udevice调用其驱动的.probe()函数。注意:此时所有设备节点已在内存中构建完毕,但没有任何一个.probe()被执行过。

device_probe()的执行流程非常精炼:

// drivers/core/device.c int device_probe(struct udevice *dev) { // 1. 如果设备已probe过,直接返回 if (dev->flags & DM_FLAG_ACTIVATED) return 0; // 2. 先probe父设备(保证总线就绪) if (dev->parent && !(dev->parent->flags & DM_FLAG_ACTIVATED)) device_probe(dev->parent); // 3. 调用驱动probe函数 ret = dev->driver->probe(dev); // 4. 标记为已激活 dev->flags |= DM_FLAG_ACTIVATED; return ret; }

这里有两个极易被忽略的陷阱:
陷阱一:父设备probe失败,子设备永不probe。比如i2c@...节点probe失败(时钟没启、复位没释放),那么它下面所有&sensor@48、&eeprom@50都不会执行.probe(),dm tree里它们会显示为[ + ](已扫描但未激活)。你必须逐级向上查dm tree -p看哪个节点卡在[ + ]状态。
陷阱二:probe函数里不能调用device_find_first_child()等需要子设备已probe的API。因为子设备probe是在父设备probe返回后才开始的。我曾在一个SPI Flash驱动里,probe时想提前读ID,结果调用spi_xfer()触发了SPI控制器probe,而SPI控制器probe又试图访问GPIO,形成死锁。解决方案是把ID读取放到bind()阶段,或用device_get_child()获取子设备指针但不probe。

实操心得:当你发现某个设备dm tree里有节点但dm devlist里没有,或者md.l 0x12345000能看到寄存器值但ping不通网口,90%概率是.probe()函数里某行代码返回了非0值(比如-ENODEV或-ETIMEDOUT)。这时不要急着改驱动,先在device_probe()里加一行debug("probing %s: %d\n", dev->name, ret);,立刻定位是哪个设备、哪行代码挂了。

3.board_init_r主流程中的DM暗线:那些被忽略的“二次初始化”动作

dm_init_and_scan()执行完,很多人以为DM初始化结束,开始往下走initr_net()、initr_flash()这些函数。但事实上,board_init_r的后续流程里,还埋着三条关键的DM“暗线”——它们不显眼,却决定了设备能否真正工作。漏掉任何一条,你的“屠龙刀”就只是块铁疙瘩。

3.1 暗线一:initr_dm()——驱动数据结构的二次填充(Data Structure Refill)

在common/board_r.c里,board_init_r()函数体中,dm_init_and_scan()之后紧跟着的就是initr_dm()。这个函数名极具误导性——它根本不是“再次初始化DM”,而是为已probe的设备填充运行时必需的数据结构。它只做一件事:调用每个设备驱动的.ofdata_to_platdata()函数(如果存在),把设备树里解析出的reg、interrupts、clocks等属性,转换成驱动私有的struct xxx_platdata结构体。

以drivers/serial/serial_pl011.c为例:

static int pl011_ofdata_to_platdata(struct udevice *dev) { struct pl011_serial_platdata *plat = dev->platdata; fdt_addr_t addr; // 从设备树获取基地址 addr = dev_read_addr(dev); if (addr == FDT_ADDR_T_NONE) return -EINVAL; plat->base = (phys_addr_t)addr; // 获取中断号 plat->irq = irq_of_parse_and_map(dev); // 获取时钟频率(如果设备树里有 clocks 属性) plat->clock = dev_read_uint32_default(dev, "clock-frequency", 0); return 0; }

这个函数执行前后,dev->platdata的内容天差地别:之前是NULL或未初始化的野指针,之后是填满真实物理地址、中断号、时钟频率的结构体。如果initr_dm()没执行,后续serial_putc()调用时读plat->base就是随机值,往错误地址写寄存器,轻则串口无输出,重则总线锁死。

为什么这个步骤要单独拎出来?因为ofdata_to_platdata()可能涉及复杂的计算。比如PCIe设备需要解析ranges属性做地址空间映射,USB主机控制器需要从phys属性里提取PHY配置。这些计算不能放在.probe()里(probe要求快,不能做复杂解析),也不能放在dm_scan_fdt()里(那时platdata内存还没分配)。initr_dm()就是专门为此设计的“缓冲区”。

注意:initr_dm()的执行顺序是按设备树节点顺序,不是按seq顺序。这意味着即使你的&uart0seq=5,只要它在dts里排第一,它的ofdata_to_platdata()就最先执行。这在多UART板子上很重要——如果console=指定的是seq=0但dts里排第三,initr_dm()时它还没填充platdata,console_init_f()就会失败。解决方案:在dts里把console UART节点移到最前面,或用u-boot,dm-pre-reloc;属性强制它在relocation前就处理。

3.2 暗线二:initr_bootstage()——启动阶段标记与性能分析(Bootstage Marking)

initr_dm()之后是initr_bootstage()。这个名字看起来和DM无关,但它干了一件至关重要的事:为每个已probe的设备打上“启动阶段”时间戳。u-boot的bootstage机制(include/bootstage.h)会记录从BOOTSTAGE_ID_START到BOOTSTAGE_ID_MAIN_LOOP之间每个关键节点的耗时,而initr_bootstage()会遍历DM设备树,对每个设备调用bootstage_device_add()。

这个动作的意义远超性能分析。它让每个设备拥有了唯一的bootstage_id,后续调试时你可以用bootstage report命令看到:

ID Name Time (ms) Delta (ms) 1 start 0.000 0.000 ... 12 dm_init 0.123 0.012 13 dm_scan_fdt 0.456 0.333 14 dm_probe_uart 0.789 0.333 15 dm_probe_i2c 1.023 0.234 ...

看到dm_probe_uart耗时0.333ms,而dm_probe_i2c耗时0.234ms,你就知道UART初始化比I2C慢——这提示你该检查UART的.probe()里有没有不必要的udelay(1000)。更关键的是,当某个设备probe失败时,bootstage report里会显示ID 14: dm_probe_uart后面直接跳到ID 16: initr_net,中间缺了一行,立刻暴露问题节点。

我在线下培训时,常让学员现场用bootstage report对比两块板子:一块正常,一块网口不亮。正常板子dm_probe_eth耗时12ms,故障板子这一行直接消失。学员顺着这个线索,很快发现故障板子的&eth0节点漏写了phy-handle属性,导致phy_connect()返回-ENODEV,probe函数提前退出。

3.3 暗线三:initr_jumptable()——函数指针表的动态注册(Jump Table Registration)

initr_bootstage()之后是initr_jumptable()。这又是名字极具欺骗性的一环。它不操作跳转表(jumptable),而是将已probe设备的struct driver中定义的函数指针,注册到全局函数指针数组中。u-boot为了减少函数调用开销,对常用操作(如read、write、ioctl)做了函数指针缓存。

以drivers/mmc/mmc-uclass.c为例,当mmc_probe()成功后,initr_jumptable()会执行:

// drivers/core/jump.c void initr_jumptable(void) { struct udevice *dev; // 遍历所有设备 for (uclass_first_device(UCLASS_MMC, &dev); dev; uclass_next_device(&dev)) { struct mmc_uclass_priv *priv = dev_get_uclass_priv(dev); // 将dev->driver->ops->send_cmd等函数指针,复制到priv->ops memcpy(&priv->ops, dev->driver->ops, sizeof(priv->ops)); } }

这样,后续mmc_send_cmd()函数就不需要每次都device_get_uclass_priv()再查dev->driver->ops,而是直接调用priv->ops.send_cmd(),省去了两次指针解引用。在嵌入式系统里,每次调用节省2-3个周期,积少成多。

但这个优化带来一个隐藏风险:如果驱动的.ops结构体在probe后被动态修改(比如根据芯片revision切换不同实现),initr_jumptable()注册的指针就失效了。我遇到过一次:某SoC的MMC驱动在probe时检测到是A0版,就用memcpy()把ops_v1拷贝给dev->driver->ops,但initr_jumptable()早已注册了ops_v0的指针。结果A0版芯片跑V0的代码,SD卡识别率暴跌。修复方案是把memcpy()移到initr_jumptable()之后,或干脆放弃函数指针缓存,用dev->driver->ops->xxx()直调。

提示:initr_jumptable()的执行时机非常微妙——它在initr_bootstage()之后、initr_console()之前。这意味着,如果你的console驱动(如serial_pl011)依赖某个MMC设备的函数指针(比如通过SPI Flash加载console字体),那么这个依赖关系必须在initr_jumptable()前就建立好,否则printf()会因找不到mmc_ops而panic。这就是为什么u-boot要求console驱动必须是built-in,不能是module。

4. 真实踩坑全链路:从dm tree空壳到ping通网口的72小时排查实录

理论讲完,现在来一场真实的“屠龙刀实战”。去年帮一家做工业网关的客户调试一款RK3399平台,现象是:u-boot能跑起来,串口有输出,但ping 192.168.1.1永远超时,md.l 0xff7b0000(GMAC寄存器)能看到值,dm tree里eth@ff7b0000节点存在,dm devlist里却找不到eth0。整个过程耗时72小时,我把完整排查链路还原给你,每一个步骤都对应一个DM核心知识点。

4.1 第一天:dm tree有节点,dm devlist无设备——锁定probe失败

客户第一反应是网口硬件坏了。我接过板子,第一件事不是看原理图,而是串口输入:

=> dm tree ... |- eth@ff7b0000 [ + ] | |- phy@0 [ + ] ... => dm devlist | grep eth # 空输出!

[ + ]表示已扫描(scanned)但未激活(activated),说明device_probe()在eth@ff7b0000这层卡住了。立刻加debug:

// drivers/net/rockchip_gmac.c int gmac_probe(struct udevice *dev) { debug("gmac_probe start\n"); ... ret = gmac_ofdata_to_platdata(dev); debug("gmac_ofdata_to_platdata ret=%d\n", ret); if (ret) return ret; ... ret = gmac_eth_ofdata_to_platdata(dev); debug("gmac_eth_ofdata_to_platdata ret=%d\n", ret); if (ret) return ret; ... debug("gmac_probe end\n"); return 0; }

重新编译烧写,串口输出:

gmac_probe start gmac_ofdata_to_platdata ret=0 gmac_eth_ofdata_to_platdata ret=-2

-2是-ENOENT,说明gmac_eth_ofdata_to_platdata()里某个dev_read_prop()失败。查代码:

static int gmac_eth_ofdata_to_platdata(struct udevice *dev) { struct gmac_rockchip_plat *plat = dev_get_platdata(dev); plat->grf = syscon_get_first_range(ROCKCHIP_SYSCON_GRF); if (IS_ERR(plat->grf)) return PTR_ERR(plat->grf); // 这里返回-2! ... }

syscon_get_first_range()需要ROCKCHIP_SYSCON_GRF这个uclass存在。dm tree里搜:

=> dm tree | grep syscon |- syscon@ff770000 [ + ]

有节点,但没激活!继续追syscon@ff770000的probe:

// drivers/misc/syscon-rockchip.c int rockchip_syscon_probe(struct udevice *dev) { debug("rockchip_syscon_probe start\n"); ... ret = dev_read_addr_index(dev, "reg", 0); debug("reg addr ret=%d\n", ret); ... }

输出reg addr ret=-1,-1是-FDT_ERR_NOTFOUND。查dts:

&grf { compatible = "rockchip,rk3399-grf"; reg = <0x0 0xff770000 0x0 0x1000>; };

reg属性写对了,但dev_read_addr_index()返回-1,说明dev->of_offset指向的节点里根本没有reg属性!用fdtget验证:

$ fdtget -t x u-boot.dtb /grf reg <0x00000000 0xff770000 0x00000000 0x00001000>

有!那问题只能是:&grf节点在设备树里,但dm_scan_fdt()没扫描到它。原因只有一个:&grf节点的status属性不是"okay"。查dts:

&grf { status = "disabled"; // 客户为了省电,手动disable了GRF! };

真相大白:客户把GRF(General Register File)这个系统控制寄存器组disable了,而GMAC驱动probe时需要它来配置PHY模式。dm_scan_fdt()看到status="disabled",直接跳过,syscon@ff770000节点根本没创建,gmac_eth_ofdata_to_platdata()自然找不到grf。

修复:dts里删掉status = "disabled",或改成status = "okay"。重新编译,dm devlist立刻出现eth0,ping通。

教训:status属性是DM的“开关”,不是Linux内核里那种软开关。status="disabled"意味着这个节点在DM世界里彻底不存在,所有依赖它的设备都会连锁失效。调试时,第一件事就是fdtget -t s u-boot.dtb /path/to/node status,确认所有父节点status都是okay。

4.2 第二天:ping通但丢包率50%——时钟与复位的隐性依赖

网口能ping通了,但ping -c 100 192.168.1.1丢包50包。md.l 0xff7b0000看GMAC寄存器,MAC_MDIO_ADDR值正确,MAC_MDIO_DATA读PHY ID也正确,但MAC_TX_STATUS一直显示TX_BUSY。这说明MAC发包卡住了。

查RK3399 TRM,GMAC TX需要两个时钟:aclk_gmac(AXI总线时钟)和pclk_gmac(APB外设时钟)。dm tree里:

=> dm tree | grep clk |- clock@ff750000 [ + ] | |- aclk_gmac [ + ] | |- pclk_gmac [ + ]

都有节点,但[ + ]。加debug到drivers/clk/rockchip/clk_rk3399.c的rk3399_clk_probe(),发现aclk_gmacprobe返回-EPROBE_DEFER。-EPROBE_DEFER是u-boot里最狡猾的错误码,意思是“我现在不能probe,你等会儿再试”。

EPROBE_DEFER的触发条件是:驱动probe时,它依赖的另一个设备(比如reset controller)还没probe成功。查aclk_gmac的dts:

aclk_gmac: aclk-gmac@0 { #clock-cells = <0>; clocks = <&cru ACLK_GMAC>, <&cru PCLK_GMAC>; clock-names = "aclk", "pclk"; assigned-clocks = <&cru ACLK_GMAC>, <&cru PCLK_GMAC>; assigned-clock-rates = <100000000>, <50000000>; resets = <&cru SRST_A_GMAC>; };

它依赖&cru(Clock and Reset Unit)的reset信号。dm tree里:

=> dm tree | grep cru |- cru@ff760000 [ + ]

又是[ + ]!继续追cru@ff760000的probe,发现它依赖&pmu(Power Management Unit):

cru: cru@ff760000 { compatible = "rockchip,rk3399-cru"; reg = <0x0 0xff760000 0x0 0x1000>; clocks = <&xin24m>, <&xin32k>, <&osc>; clock-names = "xin24m", "xin32k", "osc"; #clock-cells = <2>; #reset-cells = <2>; rockchip,grf = <&grf>; rockchip,pmu = <&pmu>; // 关键!依赖PMU };

dm tree里&pmu节点呢?

=> dm tree | grep pmu # 空!

原来客户为了省电,把&pmu节点也status="disabled"了!cruprobe时调用syscon_get_by_phandle(dev, "rockchip,pmu")失败,返回-EPROBE_DEFER,导致aclk_gmac、pclk_gmac全部defer,GMAC时钟没启,TX自然卡死。

修复:dts里&pmu节点status="okay"。但要注意,&pmu本身也依赖&grf,所以&grf和&pmu必须同时enable。这就是DM的“依赖链”——一个节点disable,整条链上的设备全瘫。

教训:-EPROBE_DEFER不是错误,而是u-boot的“智能等待”。但它有个致命缺陷:u-boot不会无限等待,它只重试3次,然后永久放弃。所以你会看到dm tree里节点存在,但dm devlist里没有,bootstage report里对应ID缺失。遇到-EPROBE_DEFER,必须顺藤摸瓜,找到那个被disable或probe失败的父节点。

4.3 第三天:ping稳定但dhcp失败——PHY连接状态的误判

一切看似正常,但客户要求dhcp自动获取IP,执行dhcp命令后超时。md.l 0xff7b0000看MAC_PHY_STATUS寄存器,bit0(link up)是0,但用网线测试仪测物理链路是通的。

查drivers/net/rockchip_gmac.c的gmac_phy_init():

static int gmac_phy_init(struct udevice *dev) { ... ret = phy_connect_dev(phydev, dev, &gmac_ops, PHY_INTERFACE_MODE_RGMII); if (ret) return ret; phy_startup(phydev); // 关键!这里会读PHY状态寄存器 if (!phydev->link) { printf("No link on %s\n", dev->name); return -ENOLINK; } ... }

phy_startup()里调用genphy_update_link(),读PHY的MII_BMSR寄存器。MII_BMSR的bit2是LINK_STATUS,但RK3399的GMAC PHY接口有特殊要求:必须先写MII_BMCR的BMCR_ANRESTART位,才能让PHY更新link状态。而genphy_update_link()没做这个操作。

查Linux内核驱动,果然有:

// drivers/net/phy/genphy.c int genphy_config_aneg(struct phy_device *phydev) { ... /* Restart auto-negotiation */ phy_write(phydev, MII_BMCR, bmcr | BMCR_ANRESTART); ... }

u-boot的genphy驱动漏了这一步!补上:

// drivers/net/phy/genphy.c int genphy_update_link(struct phy_device *phydev) { int bmsr; // 新增:重启AN,强制更新link状态 phy_write(phydev, MII_BMCR, phy_read(phydev, MII_BMCR) | BMCR_ANRESTART); bmsr = phy_read(phydev, MII_BMSR); ... }

重新编译,dhcp成功。md.l 0xff7b0000里MAC_PHY_STATUSbit0变成1。

教训:PHY驱动是u-boot里最“黑盒”的部分。不同厂商PHY芯片的寄存器行为差异极大,genphy只是通用模板,遇到问题必须对照PHY datasheet,逐字节核对MII_BMCR、MII_BMSR、MII_ANAR等寄存器的读写时序。不要迷信genphy,该魔改就魔改。

5. 终极心法:三张表掌握DM初始化全貌——从代码到内存的映射关系

经过前面四节的深度拆解,你已经看到了board_init_r里DM初始化的每一帧画面。但要把这些碎片拼成完整地图,你需要三张核心表格。它们不是文档里的概念图,而是我从u-boot内存里dump出来的真实数据映射关系,每一张都对应一个调试场景。

5.1 表一:设备树节点 → udevice实例 → 驱动匹配关系表(Debug Device Tree Mismatch)

这张表帮你回答:“为什么我的dts节点没生成udevice?” 它展示了dm_scan_fdt()执行后,设备树

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询