U-Boot新版Driver Model驱动移植与设备树调试指南

发布时间:2026/10/26 15:13:26
U-Boot新版Driver Model驱动移植与设备树调试指南

1. 先把驱动这摊子事想明白:改什么、改到哪一步

老版本的 U-Boot 升到新版本,最让人头疼的往往不是编译器报错,而是原来那一堆在board/目录下直接摸寄存器的代码,在新版本里几乎没地方落脚。我在几个项目里都踩过同一个坑:板子上电,串口能吐一两个字符,随后就死在DRAM:后面,或者干脆卡在U-Boot SPL之后就再无输出。这种现象十有八九不是硬件坏了,而是驱动没有被正确接管——外设的时钟没开、管脚没复用、设备树节点和驱动的 compatible 对不上,三者只要缺一个,驱动就不会 probe。

新版本 U-Boot 的驱动框架叫Driver Model(下文简称 DM),核心思想是把"设备"和"驱动"拆开,用设备树描述硬件拓扑,用 uclass 做同类设备的统一抽象,用 udevice 表示一个具体的设备实例。这套东西和 Linux 内核的 platform bus 思路非常接近,写过内核驱动的人上手会很快;但如果你之前只写过老式 U-Boot 的board_init()里那种"写死地址直接操作"的代码,第一次看 DM 会觉得绕了好几层,一个printf都要经过serial-uclass -> 具体串口驱动 -> 寄存器操作三层。

这篇内容主要面向三类人:一是手里有一块老板子、想把 U-Boot 从 2015 年前后的版本升到 2020 之后的版本;二是接手了一个别人做了一半的移植工程,串口能跑但网络、eMMC、PMIC 全都不工作;三是做量产固件,需要把驱动裁剪到刚好够用、启动时间压到最短。不管属于哪一类,下面的思路和步骤都是通用的,我只是把不同项目里反复出现的环节抽出来做了一遍整理。

在动手改驱动之前,有一个前置动作必须做扎实:把板子的硬件信息整理成三张表。这三张表不是走形式,后面每一行设备树、每一个 Kconfig 选项,基本都能在这三张表里找到依据。

提示:三张表分别是地址映射表(外设基地址、中断号、DMA 通道)、时钟树表(每个外设的时钟源、分频关系、门控位)、管脚复用表(每个功能对应的 GPIO bank、pin 号、复用功能号、上下拉与驱动能力)。老板的原理图加 SoC 手册就能凑齐,缺一项都会在后面某个环节卡住。

地址映射表的作用最直接。设备树里reg = <0x... 0x...>的基地址写错一位,驱动 probe 时读到的 ID 寄存器就是随机值,日志里会显示成"device not found"或者读出一个完全不认识的版本号。时钟树表决定了两件事:一是你在设备树里要不要写clocks = <&clk 12>,二是clock-frequency这类属性填多少。管脚复用表最容易被忽略,很多人改完设备树发现串口还是不出字,最后查出来是 pinctrl 里把 TX 脚配成了 GPIO 输入。

我个人的经验是,这三张表直接整理成三段注释写在板级 dtsi 的开头。看着有点土,但三个月后回来改第二版硬件时,你会感谢当时的自己。

1.1 从"能启动"到"能用"的分水岭

很多人判断移植成功与否的标准是"串口出字了",这个标准定得太低。串口能出字只说明 CPU 起来了、时钟配对了、Debug UART 的寄存器地址没写错,离"能用"还差得远。我把移植过程分成四个档位,每一档对应不同的驱动修改量,你可以对照看看自己卡在哪一档。

档位现象主要涉及的驱动典型卡点
一档串口只输出 SPL 阶段日志SPL 串口、DDR 初始化SPL 设备树未启用、串口 pinmux
二档U-Boot proper 能启动到命令行串口、时钟、pinctrl、定时器时钟驱动 probe 顺序、CONFIG_CLK未开
三档能读写存储、能加载内核MMC/NAND、分区、文件系统、FAT存储 host 控制器时序、总线宽度
四档能网络启动、能进 fastboot以太网 PHY、USB gadget、PMICPHY 时钟、地址扫描、驱动 regulator

从一档到二档,最常见的失败是串口在前 relocation 阶段就要用。新版本的 U-Boot 支持debug_uart机制,在 DM 还没初始化、甚至内存都还没配好的时候,用一套极简的printascii直接操作 UART 寄存器输出字符。这套机制依赖CONFIG_DEBUG_UART和CONFIG_DEBUG_UART_BASE,以及debug_uart_init()里你自己填的寄存器配置。我见过不少人 SPL 阶段能看到字、U-Boot proper 阶段反而没输出,原因就是 proper 阶段把串口交给了 DM 驱动,而 DM 驱动 probe 失败,老的非 DM 串口代码又被CONFIG_DM_SERIAL屏蔽了,两头落空。

从二档到三档,问题集中在时钟和复位上。存储控制器这类外设,除了功能时钟之外往往还有一路独立的host时钟和一路card时钟,有的 SoC 还需要手动释放复位。设备树里clocks只写了一路、resets忘了写,驱动 probe 会成功但读写全超时,日志表现为mmc_init: -95(EOPNOTSUPP)或者Timeout for status update。

从三档到四档,纯粹是外设适配的体力活。PHY 的地址要扫、phy-mode要选对(RMII 还是 RGMII,内部延时还是外部延时),PMIC 的 I2C 地址和电压表要对,USB gadget 的dr_mode要改成peripheral。这一档的问题不难,但特别琐碎,建议一次只改一个外设,改完立刻上板验证,别攒着一起烧。

1.2 新版本驱动模型和老代码的差异到底在哪

老式 U-Boot 的板级初始化基本是一条线走到底:board_init_f()里按顺序调一堆函数,串口、定时器、环境变量、存储,全靠人肉排序。这套写法的问题是隐式依赖太多——串口初始化必须早于任何printf,存储初始化必须早于环境变量加载,一旦某个环节出错,后面的全部连锁失败,而且很难定位。

DM 把这条线改成了按 uclass 分层的树状结构。每个外设是一个udevice,挂在某个uclass下面,probe 顺序由三样东西决定:uclass 的post_probe/pre_probe优先级、设备树里的u-boot,dm-pre-reloc等标签、以及驱动里声明的DM_FLAG_PRE_RELOC标志。串口之所以能在 relocation 之前就用,是因为它被标了 pre-reloc,DM 会在搬移之前就把它 probe 好,把 udevice 指针存进全局的gd->cur_serial_dev。

理解这一点非常关键:你在新版本里改驱动,改的不是"初始化顺序",而是"设备的拓扑描述和 probe 条件"。顺序这件事交给框架,你只需要保证:设备树节点写对、compatible 和驱动对上、依赖的资源(时钟、复位、GPIO、regulator)在设备树里都写全了。框架会自动按依赖关系去 resolve。

注意:DM 不会帮你处理"隐式依赖"。比如某个外设的电源由 PMIC 提供,你必须显式写vmmc-supply = <&buck2_reg>,否则框架不知道要先 probe PMIC。这种隐式依赖在原理图上往往只画了一根线,最容易漏。

1.3 一份可复用的板级 dtsi 骨架

我一般会在arch/arm/dts/下建两个文件:一个是共用的 SoC 级 dtsi(通常上游已经有了,直接用),一个是板级 dtsi,里面只写"这块板子和参考设计不一样的地方"。下面这个骨架基本每个项目都会用到,我把它当成模板:

/* 板级 dtsi:只写与参考设计不同的部分 */ / { model = "Generic A7 Board"; compatible = "vendor,generic-a7-board", "vendor,a7-soc"; chosen { stdout-path = &uart0; bootargs = "console=ttyS0,115200 earlycon"; }; memory@80000000 { device_type = "memory"; reg = <0x80000000 0x20000000>; /* 512MB */ }; }; &uart0 { pinctrl-names = "default"; pinctrl-0 = <&uart0_pins>; status = "okay"; }; &uart0_pins { /* 具体复用值按管脚复用表填 */ };

chosen节点里的stdout-path是 DM 选串口的依据,写&uart0或者serial0:115200n8都行。我倾向于写 label 形式,因为改板子换串口的时候只需要改一处。memory节点的起始地址和大小必须和实际 DDR 一致,写大了会在 relocation 阶段踩到不存在的地址,表现为随机死机,非常难查。

2. 驱动修改的通用套路:设备树、Kconfig、defconfig 三条线

新版本 U-Boot 加一个驱动,几乎必然要同时动三个地方。只改其中一处,编译能过,但功能不生效——这是刚上手时最迷惑的地方。我按"改什么、为什么改、不改会怎样"的顺序把三条线拆开讲。

第一条线是设备树,负责告诉框架"硬件长什么样"。节点在不在、status是不是okay、依赖的资源写没写全,决定了驱动会不会被 probe。

第二条线是 Kconfig,负责告诉编译系统"这段驱动代码要不要编进去"。新版本里驱动的 Kconfig 通常放在drivers/<类别>/Kconfig,由上级 Kconfigsource进来,选项名一般是CONFIG_DM_xxx或者CONFIG_xxx_yyy。

第三条线是 defconfig,负责"这块板子默认打开哪些选项"。文件在configs/<板名>_defconfig,本质是一堆CONFIG_xxx=y。

三条线的关系可以这么理解:defconfig 打开 Kconfig 里的开关,Kconfig 打开决定驱动源码进不进编译,设备树节点决定编译进去的驱动在运行时会不会被 probe。任何一条断了,功能就没有。

2.1 设备树里到底要写什么,哪些能省

一个外设节点最少要写四个东西:compatible、reg、status、以及它依赖的资源引用。compatible是驱动匹配的唯一依据,格式是"厂商,型号",具体字符串必须和驱动里of_match表最后一项完全一致,包括逗号和大小写。reg是寄存器基地址加长度,长度写错不影响 probe,但会影响后续dev_read_addr_size之类的调用。status不写默认是okay,但如果 SoC 级 dtsi 里写了disabled,板级就必须显式改回okay。

依赖资源这块要细说,因为漏得最多。以 MMC 控制器为例,完整节点大概长这样:

&mmc1 { pinctrl-names = "default", "state_uhs"; pinctrl-0 = <&mmc1_pins_default>; pinctrl-1 = <&mmc1_pins_uhs>; bus-width = <8>; max-frequency = <200000000>; non-removable; no-sdio; no-sd; cap-mmc-highspeed; mmc-hs200-1_8v; vmmc-supply = <&vcc_3v3>; vqmmc-supply = <&vcc_1v8>; status = "okay"; };

bus-width = <8>对应 eMMC 的 8 位数据线,写错成 4 位不影响识别,但读写速度会腰斩,而且某些高速模式会握手失败。max-frequency不是让你随便填的,它受限于 SoC 的 host 控制器上限和板子走线质量,我一般先用一个保守值(比如 50MHz)把卡识别通过,再逐步往上加。vmmc-supply和vqmmc-supply分别对应卡的工作电压和 IO 电压,1.8V 的 HS200 模式必须有vqmmc提供的 1.8V,缺了这行,mmc-hs200-1_8v选项会被驱动直接忽略。

提示:non-removable、no-sd、no-sdio这几个布尔属性是性能优化,不是必须的。但如果你明确知道这个控制器只接 eMMC,加上它们能省掉驱动去探测 SD 卡和 SDIO 设备的时间,启动阶段通常能快几十毫秒。

还有一类属性容易漏:pinctrl 的多状态。默认状态default不够用,eMMC 在高速模式下 IO 电压从 3.3V 切到 1.8V,部分 SoC 需要同时切换管脚的驱动能力和上下拉配置,这就要求设备树提供state_uhs这个状态,并在驱动里通过pinctrl_select_state切换。如果只写了default,低速能跑,一开 HS200 就各种 CRC 错误。

2.2 Kconfig 与 defconfig 的开关怎么配才不踩坑

Kconfig 这块,我建议先搞清楚"我用的驱动属于哪个 uclass",然后顺着 uclass 的依赖链往下看。以串口为例,drivers/serial/Kconfig大致结构是这样的:

config DM_SERIAL bool "Enable Driver Model for serial drivers" depends on DM help Enable driver model for serial drivers. config SYS_NS16550 bool "NS16550 UART support" depends on !DM_SERIAL help Legacy non-DM NS16550 support. config DEBUG_UART bool "Enable an early debug UART for debugging" depends on DEBUG_UART_BASE != ""

这里有个坑:SYS_NS16550和DM_SERIAL是互斥的,老版本一起打开能用,新版本会冲突。升版本时你就会发现,原来 defconfig 里那行CONFIG_SYS_NS16550=y在新版本里变成了废弃选项,编出来直接没有串口输出。

另一个坑是SPL 和 U-Boot proper 的选项是分开的。CONFIG_SPL_SERIAL_SUPPORT、CONFIG_SPL_DM、CONFIG_SPL_OF_CONTROL这一组开关决定 SPL 阶段能不能用 DM、能不能解析设备树。我见过一些工程为了省空间把CONFIG_SPL_OF_CONTROL关掉,结果 SPL 阶段所有外设只能靠board_init_f里硬编码,等于把老代码又搬回来了,DM 的意义就没了。除非 SPL 体积真的卡死,否则不建议关。

defconfig 的写法要注意两点:顺序和覆盖。Kconfig 的依赖是有顺序的,某些选项必须在它依赖的选项后面写,否则会被静默丢弃。稳妥的做法是在菜单里用menuconfig配好,然后savedefconfig生成一个干净的xxx_defconfig,而不是手工往文件里堆。覆盖问题则是另一种情况:SoC 级的 Kconfig 里可能有select语句,select不受 defconfig 控制,你在 defconfig 里写# CONFIG_XXX is not set也关不掉,这时候只能改 Kconfig 或者看有没有提供choice。

选项作用关掉的后果
CONFIG_DM启用驱动模型全部 DM 驱动失效,退回老式初始化
CONFIG_DM_SERIAL串口走 DM串口只能靠 legacy 代码,新 SoC 基本没有
CONFIG_DM_MMCMMC 走 DMmmc list为空,无法加载内核
CONFIG_DM_ETH网络走 DMping提示 no ethernet found
CONFIG_DM_REGULATOR电源管理走 DM依赖xxx-supply的设备 probe 失败
CONFIG_OF_CONTROL解析设备树全部of_match匹配失效

2.3 compatible 匹配与 probe 的完整链路

搞不清 probe 为什么没发生的时候,最好的办法是把整条链路在脑子里跑一遍。以串口为例,链条大概是这样:

  1. 启动后 DM 扫描设备树,为每个status为okay的节点创建udevice,挂到对应 uclass 下(uclass 由节点的u-boot,dm-pre-reloc或者驱动注册信息决定)。
  2. 扫描注册表里所有U_BOOT_DRIVER的of_match表,找到和节点compatible匹配的那一条,把驱动和 udevice 绑上。
  3. 调驱动的probe函数,传入udevice。
  4. probe里通常做三件事:拿寄存器地址(dev_read_addr)、拿时钟(clk_get_by_index)、配管脚(pinctrl_select_state),然后初始化硬件。
  5. 成功后 udevice 标记为 active,同一 uclass 下后续的操作都通过uclass_get_device_by_seq之类拿到这个 udevice。

这条链路里最常断在第二步。原因有三种:compatible字符串拼错(多一个空格就不行)、驱动根本没编进去(Kconfig 没开)、驱动注册宏写错(比如把U_BOOT_DRIVER写成了U_BOOT_LEGACY_DRIVER)。第二种最容易误判,因为编译不报错,只是dm tree里看不到那个节点。

提示:dm tree是排查 probe 问题的第一把工具。它按 uclass 列出所有设备和状态,+表示已 probe 成功,-表示已绑定未 probe, 表示未绑定。节点在树里但是状态是-,说明依赖没满足;节点压根不在树里,说明设备树或者status有问题。

2.4 一个串口驱动的完整改造示例

下面这段是我在某颗 Cortex-A7 平台上改过的一版串口驱动精简版,去掉了一些平台私有逻辑,保留了主干结构。老代码是一个board/serial.c里的serial_init()直接写寄存器,新代码必须包成标准 DM 驱动:

#include <common.h> #include <dm.h> #include <errno.h> #include <serial.h> #include <asm/io.h> struct a7_uart_priv { void __iomem *base; u32 clock; u32 baudrate; }; static int a7_uart_setbrg(struct udevice *dev, int baudrate) { struct a7_uart_priv *priv = dev_get_priv(dev); u32 divisor; /* 16 倍过采样:div = clk / (16 * baud) */ divisor = priv->clock / (16 * baudrate); if (!divisor) return -EINVAL; writel(divisor & 0xffff, priv->base + UART_BRR); priv->baudrate = baudrate; return 0; } static int a7_uart_probe(struct udevice *dev) { struct a7_uart_priv *priv = dev_get_priv(dev); struct clk clk; int ret; priv->base = dev_read_addr_ptr(dev); if (!priv->base) return -EINVAL; ret = clk_get_by_index(dev, 0, &clk); if (ret) return ret; ret = clk_enable(&clk); if (ret) return ret; priv->clock = clk_get_rate(&clk); return a7_uart_setbrg(dev, 115200); } static const struct dm_serial_ops a7_uart_ops = { .putc = a7_uart_putc, .pending = a7_uart_pending, .getc = a7_uart_getc, .setbrg = a7_uart_setbrg, }; static const struct udevice_id a7_uart_ids[] = { { .compatible = "vendor,a7-uart" }, { } }; U_BOOT_DRIVER(a7_uart) = { .name = "a7_uart", .id = UCLASS_SERIAL, .of_match = a7_uart_ids, .probe = a7_uart_probe, .priv_auto = sizeof(struct a7_uart_priv), .ops = &a7_uart_ops, .flags = DM_FLAG_PRE_RELOC, };

几个关键点值得展开说。priv_auto是新版本 DM 的自动分配机制,驱动不用自己malloc,框架会按这个大小分配私有数据并清零,省掉一堆内存管理的代码。DM_FLAG_PRE_RELOC让这个驱动在 relocation 之前就 probe,串口必须加,否则早期printf没有输出去处。dev_read_addr_ptr直接从设备树reg里取地址并转成指针,比老代码里#define UART_BASE 0x...灵活得多,同一份驱动可以给多个串口用。

波特率那段的分频计算,用具体数字走一遍:假设串口时钟 80MHz,目标 115200,80000000 / (16 * 115200) = 43.4,取整后 43,实际波特率80000000 / (16 * 43) = 116279,误差(116279 - 115200) / 115200 ≈ 0.94%,在 UART 允许的 2% 以内,可以接受。如果时钟是 24MHz,24000000 / (16 * 115200) = 13.02,取 13 得到115384,误差 0.16%,更稳。这就是为什么有些板子换晶振之后串口出乱码——分频值算出来误差超标了。

3. 逐类外设改造实录:从串口到 PMIC

这一节按外设类别拆开讲,每一类我会讲清楚"设备树写什么、Kconfig 开什么、驱动注意什么、上板怎么验"。这些内容有先后依赖关系:串口是一切的前提,时钟和 pinctrl 是基础设施,存储决定能不能加载内核,网络和 PMIC 属于锦上添花。

3.1 Debug UART 与正式串口的双层配置

新版本 U-Boot 里,串口实际上有两套并行机制,很多人只配了一套,结果就是前半段有输出、后半段没有,或者反过来。

第一套是debug_uart,属于"裸寄存器"级别,不走 DM,通过CONFIG_DEBUG_UART系列选项配置。它依赖三个关键值:CONFIG_DEBUG_UART_BASE(寄存器基地址)、CONFIG_DEBUG_UART_CLOCK(时钟频率)、CONFIG_DEBUG_UART_SHIFT(寄存器移位,决定寄存器间隔为 1、2 或 4 字节)。这套机制在board_debug_uart_init()里被调用,通常在 DRAM 初始化前后使用。

第二套是DM 串口,走UCLASS_SERIAL,由设备树chosen/stdout-path指定,负责 relocation 之后的正式输出。两套机制的波特率和寄存器配置必须一致,否则会出现"前面能看、后面乱码"的情况。

我的做法是:先在 defconfig 里把 debug_uart 打开,把 SPL 和 DRAM 阶段的日志跑通,确认硬件没问题;然后再打开CONFIG_DM_SERIAL,把设备树串口节点配好,验证 proper 阶段。两步分开做,出问题的时候能立刻判断是哪一层的事。

配置项建议值说明
CONFIG_DEBUG_UART_BASE按地址表填必须和 dts 里 uart 节点 reg 一致
CONFIG_DEBUG_UART_CLOCK按时钟树填填错就乱码,误差超标
CONFIG_DEBUG_UART_SHIFT0/2 视寄存器间隔32 位寄存器间隔通常填 2
CONFIG_DEBUG_UART_ANNOUNCEy启动时打印一行提示,方便确认生效

3.2 存储控制器:eMMC 与 SPI NAND 的分头处理

存储这块的坑最多,因为涉及时钟、复位、电压切换、总线宽度四重因素,任何一个没配对都会表现成"识别不到"或者"读写超时"。

eMMC 的改动重点在设备树。除了前面提到的bus-width、vmmc、vqmmc、mmc-hs200-1_8v,还有几个参数值得关注。mmc-hs400-1_8v要求 host 支持 HS400 模式并且有专属的state_hs400pinctrl,达不到别硬开。broken-cd用于没有卡检测引脚的场景,加上它就等于告诉驱动"别去读 CD 引脚,直接当卡一直在"。cd-gpios和wp-gpios在 SD 卡槽上常用,用于卡检测和写保护。

Kconfig 方面,eMMC 通常走CONFIG_MMC_SDHCI+CONFIG_MMC_SDHCI_GENERIC的组合,SoC 有专用控制器的则选对应的CONFIG_MMC_xxx。SPL 阶段如果要加载 U-Boot proper,还需要CONFIG_SPL_MMC_SUPPORT和CONFIG_SPL_FS_FAT或者CONFIG_SPL_RAW_IMAGE_SUPPORT。

SPI NAND 的路子不一样,它挂在 SPI 控制器下面,所以要先配好CONFIG_DM_SPI和 SoC 的 SPI 驱动,再打开CONFIG_NAND和CONFIG_SPI_NAND。设备树里 NAND 是 SPI 控制器的子节点,compatible填spi-nand,reg = <0>表示片选 0。SPI 模式(mode 0/3)和最高频率(spi-max-frequency)这两个属性必须和芯片手册一致,我见过把 mode 3 写成 mode 0 导致读 ID 全 0xFF 的案例。

&spi0 { status = "okay"; cs-gpios = <&gpio1 12 GPIO_ACTIVE_LOW>; flash@0 { compatible = "spi-nand"; reg = <0>; spi-max-frequency = <50000000>; spi-tx-bus-width = <1>; spi-rx-bus-width = <1>; }; };

spi-tx-bus-width和spi-rx-bus-width决定单线还是四线模式,写 4 之前先确认芯片和板子都支持,否则读出来全是错位数据。

3.3 以太网 PHY:地址扫描与时钟源

网络驱动在新版本里走UCLASS_ETH,绝大多数 SoC 用CONFIG_ETH_DESIGNWARE这一套通用驱动就能跑起来,真正麻烦的是 PHY 的配置。

设备树里 MAC 节点需要写三样东西:phy-mode(rmii/rgmii/gmii)、phy-handle(指向 PHY 子节点)、clock-frequency(部分 SoC 需要)。PHY 子节点里写reg = <地址>和reset-gpios。PHY 地址这事我建议先用扫描的办法确认:打开CONFIG_PHYLIB和CONFIG_CMD_MII,启动后在命令行敲mii info,它会遍历 0-31 号地址,能读出 ID 的那个就是实际地址。

RMII 模式下有个经典问题:50MHz 参考时钟从哪来。三种情况——PHY 自己产生、SoC 输出给 PHY、外部晶振提供。设备树里靠clocks属性表达,Kconfig 里靠CONFIG_ETH_DW_...之类的选项控制。搞错了的现象是mii info能读到 ID(因为 MDIO 是独立的两根线),但ping一直超时或者收不到包,因为 TX/RX 时钟不对。

注意:ethaddr环境变量如果没设置,网络初始化会报错退出。第一次烧板子的时候要么在环境变量里手工setenv ethaddr,要么在设备树aliases里加ethernet0 = &mac0,让驱动用默认地址。

3.4 PMIC 与 regulator:被-supply带出来的连带问题

regulator 这块最容易被当成"顺手一行的事",实际上一旦某个设备写了vmmc-supply,U-Boot 就会在 probe 它之前先去 probe PMIC 和对应的 regulator,这条依赖链断了,整个设备都起不来。

PMIC 走UCLASS_I2C下挂的UCLASS_PMIC,Kconfig 需要CONFIG_DM_PMIC加具体型号的驱动(CONFIG_PMIC_xxx)。设备树里 PMIC 是 I2C 控制器的子节点,reg = <I2C地址>,地址必须和原理图一致(7 位地址和 8 位地址写法不同,这点极容易错)。regulator 是 PMIC 的子节点,每个输出一路,regulator-name、regulator-min/max-microvolt、regulator-boot-on这几个属性按需写。

&i2c1 { pmic@34 { compatible = "vendor,a7-pmic"; reg = <0x34>; regulators { buck2_reg: BUCK2 { regulator-name = "vcc_1v8"; regulator-min-microvolt = <1800000>; regulator-max-microvolt = <1800000>; regulator-always-on; }; ldo1_reg: LDO1 { regulator-name = "vcc_sd"; regulator-min-microvolt = <3300000>; regulator-max-microvolt = <3300000>; }; }; }; };

regulator-always-on是个保命属性。U-Boot 的 regulator 框架有个默认行为:没有被任何设备引用的 regulator 会被关掉以省电。如果你的 DDR 或者某个关键外设的电源在设备树里没人引用,U-Boot 可能真的把它关了,然后你就看到一个莫名其妙的"随机死机"。加上always-on能规避这个问题,代价是多耗一点电,启动阶段这不算事。

3.5 看门狗、复位与环境变量存储

这三样东西往往被放到最后才改,但它们对量产固件的稳定性影响很大。

看门狗分两部分:SoC 内置的 WDT 和外部独立看门狗芯片。内置的走CONFIG_WDT+ 具体驱动,设备树节点里可以写timeout-sec。外部芯片一般通过 GPIO 喂狗,用法是在board_init()或者hw_watchdog_reset()里拉一下电平,这个通常没有现成驱动,需要自己在板级代码里补。

复位控制走CONFIG_RESET加UCLASS_RESET,设备树里用resets = <&rst 3>引用。很多驱动在 probe 里会调reset_control_assert/deassert,如果没有复位驱动或者引用写错,probe 直接返回-ENOSYS。

环境变量存储位置的选择要结合前面的存储决定。存在 eMMC 的某个固定偏移(比如CONFIG_ENV_OFFSET=0x400000)性能最好,但风险是量产时分区表如果变了就全废;存在 FAT 分区的一个文件里(CONFIG_ENV_IS_IN_FAT)最灵活,但依赖文件系统。我一般量产用CONFIG_ENV_IS_IN_MMC,研发阶段用CONFIG_ENV_IS_IN_FAT方便改。CONFIG_ENV_SIZE不要省,至少 8KB,因为带网络参数的默认环境加上冗余就接近这个数了。

4. 编译、烧录与上板调试的完整流程

改完代码只是完成了一半,另一半是把它烧进去、看着它跑起来、然后从日志里读出问题。这一节按实际操作顺序走。

4.1 编译链路与产物对应关系

新版本 U-Boot 的编译产物比老版本多不少,搞清楚每个文件干什么能省很多事。典型的编译命令:

make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf- generic_a7_defconfig make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf- -j$(nproc)

产出的文件里,u-boot.bin是原始的 proper 镜像,u-boot.img是加了 mkimage 头的版本(如果配置了CONFIG_SPL,这个是给 SPL 加载的),u-boot-dtb.bin是带设备树二进制拼接的版本,SPL是 SPL 镜像,u-boot.map是符号表。

烧录方式取决于板子:有 USB 下载模式的用厂商工具,有 SD 卡槽的把镜像按固定偏移写到卡里,有 SPI NAND 的用 SPL 的sf命令写。我倾向先做 SD 卡启动验证,因为改坏了直接拔卡重写,不用动板子上任何东西。

第一次上板建议打开CONFIG_LOG或者CONFIG_DEBUG_UART,让日志尽量详细。CONFIG_LOGLEVEL=7会把 debug 级别的日志也打出来,虽然啰嗦,但排查阶段信息量就是一切。

4.2 命令行里的排查工具

U-Boot 自带的几个命令,几乎能覆盖 80% 的驱动排查场景,我把它们和对应的用途列在下面。

命令用途典型输出判读
bdinfo打印板级信息看 DRAM 起始、大小、reloc 地址
dm tree打印设备树与 probe 状态+成功,-未 probe
dm uclass按类别统计设备快速确认某类驱动是否注册
mmc list列出 MMC 设备为空说明 MMC 驱动没起
mmc dev 0 && mmc info切到 0 号设备并读信息看容量、总线宽度、速度模式
part list mmc 0打印分区分区表没识别就查 GPT/MBR
mii info/mdio list扫描 PHY读不到 ID 就是 PHY 侧问题
net list列出网络设备为空说明 ETH 驱动没 probe
i2c bus/i2c probe扫描 I2C 设备PMIC 不在列表里就是地址或总线错
md.l <addr> <count>读内存/寄存器直接验证寄存器值
mw.l <addr> <val>写内存/寄存器调试验证时钟门控是否打开

dm tree的输出值得单独说说。它按 uclass 分组,每行开头的状态符加缩进表示层级,设备名后面会跟着[active]之类的标记。如果一个设备显示未激活,八成是它的probe函数里某一步返回了错误,这时候要看它依赖的资源有没有列在树里。比如 MMC 节点的子节点里应该能看到mmc@...blk(块设备)和mmc@...本身,缺了子节点说明probe中途失败了。

md/mw这对命令是我用得最多的。比如怀疑时钟没打开,可以直接读时钟控制器的门控寄存器,看目标位的值;怀疑管脚复用没配,读 pinctrl 寄存器和手册对照。这比反复改代码重新编译快得多,尤其是在驱动 probe 早期就失败、日志都来不及打的时候。

提示:mw往寄存器写值是有风险的操作,写完立刻断电重启,不要在一个已经被写乱的状态下继续调试。我之前图省事直接改了一个时钟分频寄存器,结果串口时钟被改了,后面所有日志都不是人看的,白白浪费半小时。

4.3 上板后的排查顺序

上板之后的排查要有固定顺序,乱试只会浪费时间。我通常按这个顺序走:

第一步看 SPL 有没有输出。没有的话,问题在 debug_uart 配置、DDR 初始化、或者镜像没有正确加载到 SRAM。

第二步看 proper 有没有输出。SPL 有输出 proper 没输出,问题在 SPL 加载镜像的地址、镜像头格式、或者 proper 的CONFIG_SYS_TEXT_BASE位置。

第三步进命令行敲dm tree。看有没有哪个 uclass 下是空的,空的那个就是下一步要查的。

第四步敲bdinfo。确认 DRAM 大小和实际一致,reloc 地址在合理范围。

第五步挨个验外设:mmc list、mii info、i2c bus。哪个报错查哪个。

这个顺序的好处是每一步都能给出明确结论,不会出现"什么都试了但不知道问题在哪"的局面。

5. 常见问题速查与踩坑实录

这一节我把不同项目里反复出现的问题整理成表,再补几个印象最深的案例。事情过去挺久了,但当时排查的思路到现在还能用。

5.1 驱动不工作的问题速查表

现象高概率原因排查动作
dm tree里完全没有目标节点dts 节点status = "disabled"或节点没被 include用dtc -I dtb -O dts反编译看最终设备树
节点在但状态是-依赖的时钟/复位/regulator 没准备好查probe返回值,加dev_dbg打印
串口前段有后段无DM 串口 probe 失败或stdout-path写错看chosen节点,检查CONFIG_DM_SERIAL
MMC 识别不到时钟或复位未释放,总线宽度错读 host 控制器寄存器,对照手册时序
MMC 能识别但读超时pinctrl 高速状态未配,电压未切换检查vqmmc-supply和state_uhs
网络 PHY 读不到 IDMDIO 时钟未使能,PHY 复位未释放mii info扫地址,查reset-gpios
网络能识别但 ping 不通phy-mode错,时钟源错对照原理图确认 RMII/RGMII,量时钟
PMIC 不在 I2C 列表总线号错,地址 7/8 位混用i2c bus逐个扫,i2c probe看地址
启动随机死机内存节点大小不符,regulator 被关查memory节点,加regulator-always-on
编译过了但功能没变defconfig 没生效,或select覆盖savedefconfig后对比,看 Kconfig 依赖

5.2 几个印象比较深的坑

第一个坑:时钟驱动自己也需要时钟。我在某个项目上遇到所有外设 probe 都返回-EPROBE_DEFER,日志里什么都看不出来。最后发现是时钟控制器驱动自己没有使能——它依赖的参考时钟来自外部晶振,而外部晶振的使能脚由 GPIO 控制,这个 GPIO 又属于另一个控制器。整条链最底下一环没配好,上面全部卡住。解决办法是在设备树里把时钟控制器的clocks属性补全,让框架能 resolve 整条依赖链。

第二个坑:u-boot,dm-pre-reloc漏标。早期阶段要用的设备(串口、定时器、DDR 控制器)必须标这个属性,否则 relocation 之后 udevice 指针失效,表现是启动到一半 printf 输出野指针或者直接死。老版本靠CONFIG_SYS_...宏控制,新版本改成了设备树标签,升级的时候最容易漏。

第三个坑:pinctrl 属性名写错。pinctrl-0里的0不能省,写成pinctrl驱动会找不到。另外pinctrl-names的顺序必须和pinctrl-0/1/2一一对应,写反了状态切换就错位。这个错误编译器不报,只有上板才能发现,而且现象往往是"低速正常、高速出错"这种迷惑性很强的问题。

第四个坑:环境变量的默认值覆盖了设备树的配置。U-Boot 有些参数(比如bootargs、baudrate)在环境变量里有默认值,会覆盖设备树chosen节点里的设置。排查的时候先env default -a恢复默认,再printenv看看实际生效的值,别对着设备树改半天发现根本没被用上。

第五个坑:DMA 和 cache 的一致性问题。有些网络和存储驱动用 DMA 传数据,如果驱动里忘了做flush_dcache_range或者invalidate_dcache_range,会出现"发出去的包内容是对的,收回来的包是旧的"这种现象。新版本 DM 提供了一些辅助接口,但具体要不要做 cache 操作,取决于驱动自己有没有声明dm_dma相关能力。这类问题最难查,因为逻辑上完全正确,只是数据是脏的。

注意:排查这类问题时,先把 cache 关掉试一次。CONFIG_SYS_DCACHE_OFF打开后如果问题消失,基本可以确定是 cache 一致性,再去定位具体是哪个缓冲区没同步。

5.3 关于裁剪与启动时间的几句实话

移植做完了,接下来通常会被要求"把启动时间压下来"。这时候最容易想到的是裁驱动,但我建议先做测量再动手。新版本 U-Boot 支持CONFIG_BOOTSTAGE,打开之后bdinfo里能看到各个阶段的耗时,比瞎猜有效得多。

从经验看,驱动层的耗时大头通常是三块:PHY 自动协商(能到 2-3 秒)、USB 枚举、以及环境变量从慢速存储读取。PHY 这块可以通过固定速率和双工模式省掉协商,代价是换交换机环境要重新配;USB 可以在不需要的时候直接不编译;环境变量存到 eMMC 比存到 SPI NAND 快一个量级。

裁驱动的时候要注意一个顺序:先裁用不到的外设驱动,再裁用得到的驱动里的可选功能,最后才考虑 SPL 精简。SPL 一旦精简过头,加载镜像失败,排查成本比省下来的那几十毫秒大得多。

最后再提一个细节。有些板子在实验室跑得好好的,量产批次里出现零星启动失败,排查下来是 eMMC 的max-frequency定得太高,某批次的颗粒时序余量不够。这种情况把频率从 200MHz 降到 150MHz 就解决了,代价是加载内核慢一点点。参数这东西,留余量永远比压到极限划算。