☰
从复位向量到main_loop:ARM平台U-Boot启动流程全解析
2026/10/6 3:56:51 网站建设 项目流程

好,这篇文章我想了很久怎么写。一开始我打算直奔源码,把arch/arm/lib/和common/下面那些文件挨个拉出来跑一遍,后来发现这恰恰是大部分人学U-Boot流程时最容易踩的坑:代码太多、函数调用层太深,一上来就扎进细节,出不来,最后只知道“哦这个函数被调用了”,但不知道怎么用起来。U-Boot的启动流程,说白了就是一块板子上电后,从CPU复位向量开始,把时钟、DDR、存储、串口、网络这些外设逐个点亮,最后把内核镜像交给处理器的过程。这篇文章以ARM平台常见的U-Boot 2020.04为参考,把这个流程拆成“整体设计、源码路径、核心细节、实操观察、问题排查”五块来讲,适合正在学嵌入式Linux、要移植U-Boot、或者被串口日志卡到怀疑人生的朋友。

1. U-Boot启动流程的整体链路与角色定位

1.1 启动流程前面的“三段式”:ROM固件、SPL、U-Boot proper

很多人以为U-Boot流程就是从复位向量开始,一路走到命令行提示符=>,其实在U-Boot真正接管CPU之前,芯片内部已经有一段固化在ROM里的代码在跑了。这颗ROM里的bootloader可能是厂商自己写的,也可能是ARM Trusted Firmware的一部分,它会完成最早的时钟设置、介质选择,然后去加载下一段代码。

从ROM固件开始,整个链条大致是:ROM固件 → SPL → U-Boot proper → 内核。SPL全称是Secondary Program Loader,有些芯片上叫MLO,有些叫BootROM引导程序。为什么中间要插一个SPL?因为现代DDR内存的初始化时序非常复杂,SoC内部SRAM通常只有几百KB,根本放不下完整的U-Boot。SoC厂商的做法是用SPL做最小初始化,只把CPU、时钟、串口、DDR控制器这些最底层的设备点亮,然后把完整U-Boot从SD卡、eMMC或Flash里搬运到DDR里,再跳转过去执行。

U-Boot proper就是我们从各种厂商源码包里看到的那一大坨,包含了完整驱动、命令行、环境变量、网络协议栈、各种镜像格式支持。它运行在DDR里,才有足够空间去加载Linux内核。所以你看源码时如果发现SPL和U-Boot是两个不同的defconfig、两套不同的链接脚本,不要觉得奇怪,这是刻意设计的分工,不是代码冗余。

1.2 U-Boot到底在“管”哪些事,哪些事它不管

想理解U-Boot启动流程,先要摆正心态:U-Boot不是操作系统,它只负责“把系统带起来,然后立刻让位”。它必须管的事情包括几个维度。第一,板级最基础的初始化,比如时钟树、电源域、GPIO复用、串口调试输出,因为连串口都不通,后面所有问题都没法观测。第二,内存控制器初始化,DDR的时序参数、频率、地址映射,这些一般由厂商的dram_init()函数搞定。第三,存储和外设驱动,MMC、USB、网卡、Flash这几种介质,U-Boot至少要知道从哪个设备上把内核读出来。第四,环境变量,相当于U-Boot和用户之间的“配置文件”,告诉你该去哪找内核、传什么参数。第五,把镜像加载到指定内存地址,并且准备好内核启动时需要的设备树、ramdisk等数据,最后跳转。

U-Boot不管的事情也很关键,比如它不管具体文件系统的解析细节,虽然它支持ext4访问,但只是“读取文件”这个层面的支持;它不管内核运行起来之后的各类驱动,那属于Linux的事;它也不管安全启动全套流程,虽然现在U-Boot里也引入了Verified Boot,但那是后续配置问题,不属于基础流程。把这些边界划清楚,你调试时才不会把内核的锅甩给U-Boot。

1.3 看源码之前需要建立的三个基本概念

第一个是链接地址与运行地址。U-Boot在编译时会通过u-boot.lds决定每个段放到哪个地址,这个地址就是链接地址。但板子上电后,U-Boot可能先从Flash里运行,或者在SPL阶段临时跑在SRAM里,这时候运行地址可能和链接地址不一致。为了解决这个问题,U-Boot在运行过程中会把自身代码复制到DDR里一个期望的位置,再跳过去继续跑,这一步就是重定位,英文叫relocate。不理解重定位,你就看不懂board_init_f和board_init_r为什么是两套初始化函数。

第二个是设备树(Flattened Device Tree,FDT)。U-Boot早期用board_flash这类写死的板级文件来描述硬件,后来逐步迁移到设备树。设备树描述CPU、内存、串口、MMC这些硬件的信息,U-Boot启动时会把它解析出来,最终还要把这个FDT在内存里的地址传给内核。所以U-Boot流程里经常能看到fdt_addr、fdt_high、loadaddr这类环境变量,它就是用来管理设备树和镜像在内存中的位置。

第三个是Kconfig和defconfig。U-Boot的配置体系是从Linux内核抄过来的,功能宏以CONFIG_开头,通过defconfig文件指定。不同开发板的差异,绝大多数就体现在configs/xxx_defconfig、include/configs/xxx.h和arch/arm/dts/xxx.dts这几个文件里。看流程不能只盯代码,哪个宏开关控制哪段逻辑,也是流程分析的一部分。

2. 从复位向量到main_loop:启动源码全路径拆解

2.1 reset向量的第一段汇编在做什么

ARM平台上U-Boot的入口是arch/arm/lib/vectors.S或者SoC厂商自己的start.S,这取决于具体架构版本。CPU复位后,首先进入异常向量表,U-Boot第一条指令基本就是b reset这种跳转指令,跳到真正的启动代码。

这段汇编做的事情看起来不多,但每一句都很关键。它会先把处理器设置为SVC模式,确保后续操作有足够的权限;然后关闭中断,因为此刻中断向量还没准备好;接着初始化Cache、MMU这些控制逻辑,有些芯片还会设置CPU频率。最重要的是设置栈指针SP,汇编阶段只有先有了栈,才能调用C函数。有些平台会先判断当前是从哪种介质启动的,比如SD卡、eMMC、Nor Flash,然后根据启动方式决定从哪里复制代码。

我最早看这段汇编时也很头疼,因为不同的SoC厂商会往里面塞大量自家逻辑,比如三星、NXP、瑞萨都各有各的花活。我的建议是不要试图完全读懂每个芯片的每个寄存器,先抓住主线:设置异常向量、进SVC模式、关中断、设栈、然后进入C世界。芯片特定的初始化细节,等移植真出问题时再回头扣。

2.2 board_init_f:把DDR、串口、设备树先点亮

从汇编跳进C代码后,第一个主要流程是board_init_f。这个“f”是“floating”的意思,因为此时U-Boot还没有重定位,运行的地址可能不是最终链接地址,它正处于一个“临时的”位置。可以先把U-Boot看成一个还在工棚里干活的施工队,先搭临时设施,等主体建筑起来后再搬进去。

common/board_f.c里有一个很大的函数指针数组,叫init_sequence_f,里面按顺序排着几十个初始化函数。典型的初始化顺序大概是:初始化早期堆空间、初始化串口、读取环境变量里的波特率、计算DDR大小、初始化DDR、初始化调试串口、初始化设备树、初始化Cache、最后计算重定位的目标地址。它和Linux内核的启动不太一样,不是“一件事做到底”,而是“把每个子系统都先弄到一个能用的最低水平”。

你可能会问,串口初始化为什么要分好几次?因为早期打印需要极简的调试串口,不依赖设备树和完整驱动模型;等DDR和更完整的环境准备好之后,才把标准控制台切到真正的serial驱动。这个分段设计也是很多新手看日志时发现“前面几行信息很少”的原因。

board_init_f的最后一步是relocate_code。它会根据gd->relocaddr,把U-Boot镜像复制到新的地址,同时修正所有全局变量、函数指针的偏移,然后通过一个跳转指令,把控制权交给重定位后的代码。从这一步开始,U-Boot才算真正稳定下来,后面再调用函数就不会因为地址错乱而崩溃了。

2.3 重定位与board_init_r:U-Boot从“草台班子”变成完整系统

重定位完成后,流程进入board_init_r,这里的“r”是“relocated”的意思。到了这个阶段,U-Boot已经运行在DDR里的最终地址上,可以放开手脚初始化各类外设和完整功能了。

common/board_r.c里同样有一个初始化数组,比如initr_caches、initr_malloc、initr_dm(驱动模型)、initr_mmc、initr_env、initr_net、initr_jumptable、initr_cli等等。每个函数负责一个子系统的初始化,所以串口日志里会看到“MMC: mmc@xxxx: 0”“Net: eth0”之类的信息。这个阶段的典型输出大概像这样:

U-Boot 2020.04-dirty (Jan 01 2024 - 12:00:00 +0800) CPU: Freescale i.MX6ULL rev1.1 at 396MHz DRAM: 512 MiB MMC: FSL_SDHC: 0, FSL_SDHC: 1 In: serial Out: serial Err: serial Net: eth0: ethernet@20b4000

board_init_r跑完后,U-Boot会进入run_main_loop,也就是主循环。到这里,所有硬件初始化基本完成,串口命令行交互已经可用,剩下的工作就是等待用户输入命令,或者执行预设的bootcmd自动启动系统。

2.4 main_loop与bootcmd:等待按键与自动启动逻辑

main_loop是整个U-Boot启动流程的“终点站”,它做两件事。第一件是检查启动延时,如果用户在规定时间内按了任意键,就进入命令行,给用户手动操作的机会。第二件事,如果超时没人按键,就自动执行环境变量bootcmd里面的命令。

这段逻辑对应的日志就是大家非常眼熟的:

Hit any key to stop autoboot: 3

倒计时数字由环境变量bootdelay控制。如果你在做产品,希望上电后不要等待,直接把bootdelay改成0或者-1,结果会有些差别。设成0代表立刻启动,设成-1则完全禁止自动启动,每次都进命令行。这种小细节在量产固件调试时特别有用。

bootcmd本身可以很简单,也可以很复杂。最简单的启动命令链可能是:

fatload mmc 0:1 0x82000000 Image booti 0x82000000 - 0x83000000

一行行把内核和设备树加载到内存,然后跳转。复杂的版本则会包含distro_bootcmd,也就是从多个介质里逐个尝试启动,像UEFI的启动项扫描一样。很多开发板默认的bootcmd就是这个分布式启动逻辑,它会依次尝试USB、网卡、MMC、SATA等设备,这也是为什么你有时能看到一串“Trying to boot from ...”的日志。

理解了main_loop,你就理解了U-Boot启动流程中“人和机器交互”的那个开关。后续想给U-Boot加开机画面、加按键逻辑,本质上都是在main_loop这个环节做文章。

3. 核心细节补全与关键环节配置要点

3.1 内存(DDR)初始化是启动流程中最容易翻车的环节

把DDR初始化单独拉出来讲,是因为它在整个流程里的地位太特殊了:SPL的核心使命之一就是初始化DDR,DDR没起来,完整U-Boot压根没法加载。DDR初始化这件事在硬件上涉及的东西极多,就算只看软件流程,也要处理时钟、复位、功耗、时序参数、DDR控制器寄存器、训练结果存储等一连串操作。

在i.MX6ULL这类芯片上,DDR初始化通常由厂商提供的一段DCD(Device Configuration Data)来完成,DCD数据里写着初始化时钟、DDR控制器、内存颗粒时序的一堆寄存器地址和值。在SPL阶段,这些数据会被解析并逐条写入寄存器。为什么很多移植教程都叫“要改DCD”“要改DDR初始化参数”?就是因为不同板卡使用的DDR颗粒、布线长度、频率不同,时序参数必须跟着调整,参数不对最直接的后果就是U-Boot在启动早期就死掉,串口连一个字都打不出来。

调试内存问题时的常见观察点是DRAM:这一行打印出来的容量。如果打印出来的容量和实际内存颗粒不符,先检查DDR大小探测逻辑,通常是dram_init_banksize()或get_ram_size()那块。还有一点要特别注意:很多SoC的DDR控制器支持多个rank和片选,U-Boot里gd->ram_base和gd->ram_size直接决定了内核能感知到的物理内存范围。这里的数值一旦和实际DDR布局不一致,后面启动内核大概率会遇到“Kernel panic - not syncing: Out of memory”之类的问题。

3.2 设备树与U-Boot流程的配合

U-Boot流程里和设备树相关的环节经常被一笔带过,但实际用起来坑非常多。U-Boot在编译时会把.dtb文件打包进自身固件,或者放在独立分区里由启动命令去加载。编译通过后,U-Boot会把设备树地址保存到环境变量fdt_addr或fdtcontroladdr里,后续booti/bootm命令执行时,U-Boot会把这个地址作为参数传给内核启动入口的r2寄存器。

这里有一个细节容易被忽略:设备树不一定能直接被内核“原样”使用。U-Boot启动过程中可能会修改内存节点、chosen节点,甚至根据板子实际配置调整status属性。例如有些SoC默认把FDT放在一个临时位置,U-Boot会把它移动到CONFIG_SYS_FDT_LOAD_ADDR指定的地址,最终确保内核访问到的FDT不会被U-Boot自身占用或者被解压出的内核覆盖。

设备树地址冲突是启动失败的高发原因之一。比如你用fatload把内核Image加载到0x82000000,又把FDT加载到0x84000000,但要确保Image解压后不会覆盖FDT。还有一种常见问题:启动命令里booti 0x82000000 - 0x83000000中间的“-”代表没有ramdisk,如果你有多余的ramdisk,中间要填它的地址,格式是booti <kernel> <ramdisk> <fdt>。中间少写一个地址,U-Boot可能不会报错,但内核会因为没有正确拿到FDT或initrd而表现异常。

3.3 环境变量与保存机制

环境变量是U-Boot启动流程里最像“用户配置”的部分,但你得明白它的存储原理,不然会遇到“为什么我saveenv了,下次启动还是老样子”的问题。环境变量在内存里以一组key=value的形式存在,U-Boot启动时会在指定存储设备上的分区去读取环境变量块,这个存储位置由CONFIG_ENV_IS_IN_MMC、CONFIG_ENV_IS_IN_SPI_FLASH、CONFIG_ENV_IS_IN_NAND这些宏决定。

每次启动时,U-Boot会先判断环境变量块是否有效,判断依据是一个CRC32校验值。如果CRC校验失败,U-Boot会使用编译进固件的默认环境变量,并打印类似“*** Warning - bad CRC, using default environment”的警告。这种问题通常出现在环境变量存储分区的地址或偏移配置和实际烧录内容不匹配时,解决办法不是简单重新saveenv,而是先确认CONFIG_ENV_OFFSET、CONFIG_ENV_SIZE这些参数是否和你的分区布局一致。

还有一个很实用的技巧:很多板子的默认环境变量里没有bootcmd,或者只写了简单的distro boot逻辑。为了让U-Boot流程完全可控,我习惯在编译前就把CONFIG_BOOTCOMMAND和默认bootargs这些环境变量固化到头文件或defconfig里,而不是等到板子上再用setenv去输入。生产环境更是要这样,因为没人会每次开机前手动敲一遍命令。

3.4 booti/bootm/bootz的差别,到底该用哪个

U-Boot支持好几种启动内核镜像的命令,新手经常搞混。bootm是最老牌的,支持uImage格式,也就是带U-Boot自带头部信息的内核镜像,头部会包含加载地址、入口地址、CRC和镜像类型。bootz专门用于ARM平台常见的zImage,就是Linux内核常用的自解压镜像。booti则用于ARM64平台的Image格式,也就是未经自解压的原始内核镜像。

这三个命令执行的大致流程是:先校验镜像头(bootm)或识别格式(booti/bootz),然后准备FDT和ramdisk,最后执行do_bootm_linux这类跳转函数。跳转前,U-Boot会在指定的内存地址上做一些内存映射和Cache清理,确保内核能看到干净的物理内存,然后把设备树地址写入r2寄存器,通过一条分支指令进到内核入口。

选用哪个命令,本质上是看你的内核镜像格式和入口地址。ARM32平台最常见的是zImage,所以bootz用得多;ARM64平台基本固定用booti;老一点的板子见过uImage,就会看到bootm。启动失败时,第一件事就要确认启动命令对应的镜像格式对不对,如果把Image格式传给bootz,它很可能会直接拒绝或者解压出乱码。移植时建议每块板子都明确写死启动命令,宁可一开始用手动命令验证,确认通过后再固化到bootcmd。

4. 实操过程观察:编译、串口日志与启动分段对照

4.1 编译U-Boot的最小步骤与defconfig选择

实际操作部分,我拿一个通用ARM环境的编译过程举例。U-Boot源码编译看起来简单,但很多人第一次都会卡在交叉编译工具链上。ARM32平台的用法通常是这样的:

export ARCH=arm export CROSS_COMPILE=arm-linux-gnueabihf- make distclean make qemu_arm_defconfig make -j4

如果是ARM64平台,命令会变成:

export ARCH=arm64 export CROSS_COMPILE=aarch64-linux-gnu- make qemu_arm64_defconfig make -j4

编译完成后,目录下会生成u-boot、u-boot.bin、u-boot.dtb、u-boot.cfg这些文件。不同SoC平台最终烧录的镜像可能叫u-boot.imx、u-boot-spl.bin、MLO等,这些格式差异都来自编译时引入的打包脚本。移植一块新开发板时,最合理的起步方式不是从零写代码,而是找一块配置最接近的板子,复制它的defconfig,然后改掉DDR、串口、存储、网卡这些板级差异。我见过最快的移植案例,其实就是把同系列板子的defconfig改了几个地址和宏开关,半小时就启动到了=>提示符,后续再慢慢调细节。

4.2 解析一份真实的串口日志的重要数据点

串口日志是观察U-Boot启动流程最直观的手段。假设你在实验环境里跑起来,串口输出可能是这样的:

U-Boot 2020.04 (Jan 01 2024 - 12:00:00 +0800) CPU: ARMv7 Processor rev 5 (v7l) CPU: VPFN_* bits from cpuinfo DRAM: 512 MiB WARNING: Caches not enabled Flash: 128 MiB MMC: MMC: 0 In: serial Out: serial Err: serial Net: eth0: virtio-net#0 Hit any key to stop autoboot: 3 =>

别看就这几行,每一行都能说明流程走到了哪一步。第一行“U-Boot 2020.04”出现,说明board_init_f已经能工作,串口已经进行了早期输出。紧接着是“CPU: ...”,这对应代码里的CPU信息打印,说明架构相关初始化完成。“DRAM: 512 MiB”这行出现,说明DDR探测成功,内存大小已经读出来,这背后的函数是dram_init()返回了gd->ram_size。

“MMC: MMC: 0”在board_init_r阶段出现,说明驱动模型初始化完成后,MMC控制器已经枚举成功。“Hit any key to stop autoboot: 3”说明主循环已经进入,等待用户打断自动启动。如果在“DRAM:”之后再也看不到后续日志,那基本可以断定是MMC、设备树或者某个驱动的初始化阶段崩了。串口日志里每一行都有定位价值,多和代码里的printf对应起来,调试速度会提升一个档次。

4.3 通过加打印和u-boot命令定位进程状态

调试U-Boot流程时,千万不要只会看串口日志。“既然日志不够,就往代码里加printf”这个思路简单粗暴但极其有效。当然,为了不污染源码,U-Boot本身提供了调试开关,比如DEBUG宏和debug_uart机制。在代码中你经常会看到debug(...)函数,它只在CONFIG_LOG或对应调试宏开启时才输出,常规运行会被编译掉。

现场调试时,我会在关键的跳转点临时加入打印,比如在board_init_f入口、DDR初始化完成、环境变量加载完成、跳转内核前各打一行。加上后重新编译烧录,观察日志卡在哪一行,再缩小范围。还有一类非常高效的定位方式,是直接在U-Boot命令行里用bdinfo查看板级信息,用mm、md命令读写内存,用fdt addr命令手动解析设备树。比如bdinfo会打印boot_params、memstart、memsize这些关键数据,能帮助你确认U-Boot认为的“世界”是否和实际硬件一致。

如果U-Boot已经卡死在流程中,没有进入命令行,那就需要前面的加打印策略。这里有个小技巧:早期打印尽量简单,不要用格式化次数太多的printf,因为此时串口驱动和环境可能还不稳定,有时一个复杂格式串就会让你误以为是崩了。

5. 常见问题与排查技巧实录:U-Boot启动卡住怎么定位

5.1 串口完全无输出:先从这四种方向排查

U-Boot启动流程中,串口完全无输出是最让人头疼的问题,因为看不到任何日志,所有猜测都可能成立。我的个人排查顺序是这样的。先看硬件线序,TX/RX有没有接反,电平转换芯片供电是否正常,波特率是否和U-Boot配置一致,这个在项目初期出现概率最高。再确认编译配置,查看defconfig中是否打开了对应UART外设,CONFIG_MXC_UART、CONFIG_SYS_NS16550这类宏不能少。然后用逻辑分析仪或示波器量UART TX引脚,开机瞬间有没有电平跳变,有跳变说明软件已经在输出,问题可能在线路或终端软件;没跳变说明U-Boot在非常早期就崩了,要么DDR没起来,要么时钟配置不对,要么Flash介质上读到的固件就是坏的。

还有一类低概率但实际存在的情况:U-Boot的早期串口输出被CONFIG_SILENT_CONSOLE关闭了。静静模式下,串口可以不输出任何内容,直到环境变量允许才打开。遇到莫名无输出时,翻一下defconfig里有没有这类“安静”配置,这也是排查方向之一。

5.2 启动卡在Early CPU、DRAM、board_init_f阶段

如果在串口日志里只能看到“U-Boot SPL ...”,然后就停了,说明问题几乎肯定在SPL阶段。SPL阶段最常见的三类死法分别是:DDR初始化失败、存储介质读取U-Boot proper失败、早期时钟配置不对导致外设无法工作。

DDR初始化失败的典型特征是RESET脚不断拉低重启,或者串口根本没有任何输出。这类问题要么是DCD参数和颗粒不匹配,要么是DDR供电没起来,要么是地址线/数据线硬件接触不良。存储介质读取失败则比较容易判断,通常SPL能打印“Trying to boot from MMC”或类似信息,但读不到u-boot.bin分区,这时要检查分区表、文件名、文件系统格式。实际项目的调法中,我会把SPL阶段的调试打印全部打开,尤其是CONFIG_SPL_DEBUG这类宏,让SPL把每个步骤都输出出来。

5.3 能启动但环境变量CRC错误或autoboot一直倒计时无法跳过

环境变量CRC错误,也就是日志里的“bad CRC, using default environment”,并不算致命,但会带来很多诡异问题。比如你明明保存过bootcmd,重启后却执行了默认的distro boot逻辑,怎么都进不了你的系统。这通常是因为CONFIG_ENV_OFFSET或CONFIG_ENV_SIZE设置和实际分区布局不符,环境变量被写在了一个不该写的位置,或者环境变量块被其他固件覆盖。

处理办法是重新确认存储介质的分区,查看mtdparts或eMMC boot partition配置,然后手动擦除指定区域再saveenv。还有一种很隐蔽的情况:环境变量块放在某个分区,但U-Boot启动时加载环境变量的代码和我们实际烧录的定义不是同一套配置,导致两边格式不一致,这时要对比编译生成的u-boot.cfg,看CONFIG_ENV_IS_IN_*和偏移到底是哪个。

至于autoboot倒计时无法跳过,多数时候不是按键坏了,而是串口接收路径或控制台选择有问题。U-Boot在启动延时阶段会轮询输入设备,如果当前控制台输入对应的串口驱动没有正常工作,按什么键都进不了命令行,这时可以用CONFIG_AUTOBOOT_KEYED这类宏调整交互逻辑,或者检查输入设备编号是不是被改到了别的串口。

5.4 启动到“Starting kernel ...”后无反应

这个场景也比较经典:U-Boot打印了“Starting kernel ...”,屏幕就再也没反应了。首先说明U-Boot已经成功把控制权交给了内核入口,问题基本可以锁定在U-Boot传给内核的数据上。最值得怀疑的是设备树地址,还有内核本身的入口地址。如果内核Image被加载到了一个错误地址,CPU一跳进去就会取指令异常,表现为完全无输出。

解决这类问题,我一般分三步排查。第一步,用fdt addr命令查看FDT地址处是否真的是一个合法设备树,用fdt print /看看能不能解析出来。第二步,确认内核Image的入口地址和加载地址是否一致,如果加载地址和链接地址不一致,有些内核原本支持位置无关启动,但ARM64平台通常要求booti加载到指定地址。第三步,把bootargs里的console=参数和实际调试串口对应起来,比如U-Boot跑在ttymxc0,但内核却从ttymxc1输出,那就会看到“死机”的假象,实际内核已经在跑了。

5.5 收藏级排查命令与调试配置列表

最后分享一些我平时必用的命令和配置,这块对排查U-Boot启动流程的“黑屏问题”特别有用。首先是bdinfo,它能把板级信息全盘打印出来,包括内存地址、环境变量地址、CPUID这些。其次是md <addr> <length>,查看某个内存地址处的二进制数据,用来确认加载进内存的镜像是否完整。然后是fdt addr和fdt print,手动解析设备树,确认FDT内容合法。还有mmc list、usb start、dhcp这类外设探测命令,用来判断具体外设是否枚举成功。

调试编译配置方面,值得打开的开关包括CONFIG_DEBUG_UART,它能在极早期用最简单的方式输出调试信息,不依赖完整驱动模型;CONFIG_BOOTSTAGE可以记录启动耗时和关键阶段信息;CONFIG_LOG配合log level可以按等级过滤日志,比单纯printf更灵活。这些配置在最终量产固件里可以关掉,会减小体积、提高速度,但在开发阶段千万不要省,它们能帮你省下大量猜测时间。

说实话,我在实际做板子调试时,遇到“串口无输出”也还是会先紧张一下。但经历几次之后你会发现,U-Boot启动流程其实并不神秘,它就是一个分阶段、分职责的接力过程:ROM先交棒给SPL,SPL把DDR和基础外设准备好,再交棒给完整U-Boot,最后由U-Boot把设备树和内核一起交棒给Linux。每一个阶段都有明确的入口、明确的职责、明显的日志特征。只要能定位到日志卡在哪一行,问题就缩小了一半。后续如果你要做开机logo、启动动画、快速启动这些优化,都可以回到这个流程里去找到对应的插入点,因为不管功能怎么加,核心链路还是从reset到main_loop这一条。

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

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

立即咨询