2. 启动流程全景从复位向量到第一个C函数整个启动流程我认为可以分为三个大的阶段来理解硬件初始化阶段关中断、设置CPU模式、初始化时钟、初始化DDR。这个阶段的目标是让CPU“活过来”并且有一个可用的内存环境。代码重定位与搬运阶段将U-Boot自身从只读的Flash/NAND/eMMC搬运到可读写的RAM中运行。这解决了“程序存储介质运行速度慢”和“无法修改自身”的矛盾。板级初始化与命令行阶段初始化串口、网卡、Flash等外设然后进入一个死循环等待用户输入命令或自动启动内核。我画了一张粗粒度的流程图纯文字版方便你对照着看复位入口 (_start) | |---- 设置SVC模式关IRQ/FIQ中断 | |---- 初始化关键寄存器 (如CPU频率、L2 Cache等) | |---- 初始化DDR (内存控制器) | |---- 设置栈指针 (SP) | |---- 代码重定位 (复制自身到RAM修复链接地址) | |---- 跳转至RAM中的C语言入口 (board_init_f / main_uboot) | |---- 初始化串口、网卡、Flash、环境变量等 | |---- 进入main_loop等待命令或加载内核这张图可能看着有点抽象下面我一步步拆开细讲重点说清楚每个环节“是什么”、“为什么要做”、“怎么做”。2.1 硬件初始化阶段让CPU先活过来这段代码是汇编写的也是移植U-Boot时几乎必改的地方。它最核心的几件事1设置CPU工作模式关闭中断CPU上电后默认可能有各种未知的中断状态。如果不先把中断全关了一旦配置内存到一半来了个中断程序跑飞后面的初始化步骤全白费。所以第一步往往是/* 设置SVC模式关IRQ和FIQ */ mrs r0, cpsr bic r0, r0, #0x1f orr r0, r0, #0xd3 msr cpsr, r00xd3这个值其实包含了两层意思低5位0x13表示 SVC模式0xc0这两位表示关掉IRQ和FIQ。这两个一位都不要省。在汇编里加注释是极度重要的习惯。很多项目代码看起来像天书不是因为它复杂而是因为根本没有注释。启动代码前20行看懂了后面基本是类似的套路。2初始化时钟CPU主频和总线频率如果不匹配外设访问就会出现时序问题。比如I2C、SPI这类低速外设如果时钟源没配好数据根本传不出来。规范的平台通常会有lowlevel_init.S或clock_init.c里面分门别类地把ARM核时钟、总线时钟、外设时钟逐项设好。单独提个醒很多平台在调试DDR时明明时序对了仍不稳定大概率就是时钟没先配好DDR控制器和高频外设是强相关的。3初始化DDR内存这一步完成后C语言环境才有地方放栈和变量。DDR初始化通常包括配置内存控制器基地址和地址映射设置时序参数如CAS延迟、刷新周期、读写时序设置DDR电源和终端电阻有些平台会把DDR参数做成宏或表格放在include/configs/xxx.h或设备树中。如果是自己移植的板子DDR参数通常由板级厂商提供或参考芯片手册一步步计算。下面举个非常简化的DDR初始化示例伪代码风格实际寄存器名请对照手册void ddr_init(void) { /* 使能DDR控制器时钟 */ writel(0x01, DDR_CLK_ENABLE); /* 设置内存块映射 */ writel(0x1122, DDR_BASE_ADDR); /* 设置时序参数 */ writel(0x0A0A0A0A, DDR_TIMING_PARAM); /* 启动DDR控制器 */ writel(0x80000001, DDR_CTRL_START); /* 等待就绪 */ while (!(readl(DDR_STATUS) 0x1)) ; }这里的数值没有通用含义不同芯片完全不同。重点是想表达一个过程DDR初始化本质是给控制器写一组配置参数然后启动再确认状态。2.2 代码重定位把U-Boot搬进RAM这一步有许多初学者容易卡住。U-Boot最初是从 Flash/NAND/SD 卡里启动的这些介质访问速度慢而且有些介质如NOR Flash不能随机写。如果直接在上面运行代码整体性能会被严重拖慢同时也没法更新自身。所以U-Boot会在 DDR 初始化完成后把自身代码从启动介质复制到内存中。这里有个关键概念叫“链接地址”。编译器在编译U-Boot时会预设一个运行地址比如0xC3E00000所有函数调用、全局变量访问都基于这个地址生成机器码。但在上电初期U-Boot实际是先从低地址的Flash里跑起来的这时CPU取指地址和链接地址不匹配怎么办答案就在“位置无关码”和“重定位修复”这两个机制位置无关码启动早期汇编代码里只使用相对跳转b/bl不依赖绝对地址代码放在哪里都能跑。重定位修复在把代码复制到RAM链接地址后需要把所有绝对地址引用的地方修复成实际链接地址对应的数值。U-Boot中有专门一段代码负责遍历并修正.rel.dyn段中的重定位表。我拿一个生活例子来解释想象你搬新家搬家前所有家具上贴的便签写的是“新家地址房间号”。搬家车开到新家后你需要根据便签把家具一一放到对应房间。便签就是链接地址新家就是RAM而搬运工就是重定位代码。理论上如果代码包含位置无关码不修复也能跑但因为有绝对地址访问比如全局变量、函数指针不修复就会出现“指针指错内存”的诡异问题。这就是为什么启动流程里必须有一整套重定位逻辑。2.3 从汇编跳入C语言世界在汇编完成了最基本的初始化和重定位后U-Boot会设置好栈指针然后跳进一个C函数——通常是board_init_f或main_uboot具体名字视版本和平台而定。栈指针SP为什么必须在这个阶段设定因为C代码第一次调用函数时需要用栈保存局部变量、返回地址和现场。没有栈的C语言就像没有桌子的厨师菜都放不到台面上。从跳入C函数那一刻起U-Boot的绝大部分可移植代码就开始起作用了。它会使能串口、初始化定时器、扫描总线上的设备、解析环境变量、配置网络……这一系列操作让U-Boot从“裸机程序”逐渐变成一个“微小型操作系统”。3. U-Boot 的启动阶段划分两个关键的init函数老版U-Boot2014年之前的启动路径是board_init_f - board_init_r新版虽然转向了initr_*系列函数并在不少平台上引入了设备树FDT驱动模型但核心逻辑仍然保留了两个阶段的思想。我把这两个阶段分别展开讲。3.1board_init_f前期的静态初始化这个阶段整体上还在板级代码里主要做这么几件事初始化DRAM有些平台已提前但这里会再次确认把DRAM大小、栈位置等存入全局变量gd结构体分配后续阶段使用的内存空间为重定位做准备执行早期的板级初始化比如点亮调试LEDgd结构体是U-Boot里的重要结构体全称global_data它保存了内存大小、启动标志、波特率、环境变量地址等信息。许多函数都依赖它。这里有个常见问题有人问为什么有了全局变量还要搞个gd因为启动早期全局变量所在的地址可能还没准备好链接地址对应的RAM还没有初始化完成所以U-Boot暂时把关键信息保存在一个固定的寄存器如r9指向的区域等内存好了再复制到全局变量中。这是一种阶段化设计的智慧。3.2board_init_r重定位完成后的全面初始化当代码重定位到RAM并跳转到正确的链接地址后U-Boot会进入board_init_r执行全面的板级初始化。在这个阶段U-Boot会初始化完整的内存分配器创建设备树如果使用FDT初始化串口控制台初始化I2C、SPI、GPIO等总线控制器初始化Flash / NAND / MMC 驱动初始化网络接口初始化环境变量包含bootcmd、ipaddr、serverip等执行initr_*系列函数让各子系统逐步工作起来这一阶段结束时U-Boot就准备好进入命令行交互了。3.3 启动内核前的最后一公里当用户在命令行执行boot或者U-Boot依据bootcmd自动启动时它会做几件非常关键的事获取内核镜像地址通常从环境变量kernel_addr或loadaddr获得检查镜像格式与CRC校验如果有设备树会把它加载到指定内存地址设置内核启动参数如bootargs包括控制台、根文件系统位置等跳转到内核入口点之前我踩过的一个坑bootargs里忘了写root/dev/mmcblk0p2结果内核启动后挂不上根文件系统一直 kernel panic。排查这问题比想象中费时间因为那个阶段连串口打印都是正常的你根本想不到是U-Boot传参错了。所以bootargs一定在移植初期就固定好规范模板一句一句拆别嫌麻烦。4. 从移植者视角看几个关键源码文件很多人说我看了启动代码但不知道从哪里改起。下面几个文件是移植时最高频修改的建议你在工程里先定位出来逐个过一遍4.1include/configs/board.h或设备树这个文件被称为“板级配置头文件”它定义了大量宏包括内存大小、环境变量默认值、启动命令、控制台参数等。如果是新版U-BootFDT很多配置从头文件挪到了设备树里但环境变量默认值和一些编译开关仍在这里。改配置、加命令都要这里动手。4.2board/vendor/board/目录这里放的是板级代码比如board.c、lowlevel_init.S、Kconfig。这是每个板子最独特的部分也是与其他板卡拉开差距的地方。4.3arch/arm/cpu/armv7/start.S或其他架构对应文件这是汇编启动的入口。虽然一般不会大改但理解它非常必要。如果你要用自定义的启动方式比如从特殊介质启动就要动这个文件。4.4drivers/目录各类外设驱动。串口、网卡、存储、显示等。驱动模型DM是U-Boot现代化的核心不理解DM就去配置设备树你会不断撞墙。移植U-Boot时有一个非常朴素的建议拿到一块新板子后先查看它的defconfig里开启了哪些子系统再通过make menuconfig逐项核对。启动流程跑不通往往不是启动代码本身的问题而是某个外设初始化失败导致整体卡死。5. 启动流程常见问题与排查技巧实录这部分我觉得最实用整理几个我在调试中真实踩过的坑5.1 串口只打印几个字符就卡死这种现象通常发生在lowlevel_init之后的DDR初始化阶段。原因不外乎DDR时序参数不对初始化不稳定代码跑着跑着就挂了时钟配置错误DDR工作频率超出规格代码重定位时地址映射错误导致跳转后执行了错误数据排查手段在汇编代码的关键节点添加串口打印用最原始的putc函数用示波器量DDR时钟引脚看频率是否正常检查内存控制器寄存器配置值与芯片手册对照5.2 启动时U-Boot没有输出如果串口一点点输出都没有优先怀疑串口引脚复用或波特率不对U-Boot源码中的串口基地址与板子实际地址不一致晶振频率偏差太大导致波特率失调代码根本没有跑起来比如引导介质里没烧写成功以前调试一块板子因晶振频率配错串口输出的全是一堆乱码最后通过读回U-Boot二进制确认烧写无误后才想到去检查晶体配置。5.3 内核启动时崩溃或不能加载根文件系统这个阶段往往不是U-Boot本身的问题而是传给内核的启动参数不正确。典型错误bootargs中缺少consolettySAC0,115200导致内核无控制台输出看起来像“死机”root参数指定的分区号与实际不符mtdparts分区定义与内核分区表不一致遇到这类问题不要上来就怀疑U-Boot代码。先在U-Boot命令行手敲bootargs确认能启动后再去分析为什么自动化流程失败。这个步骤能节省大量时间。5.4 如何精准定位卡死在哪个环节最笨但最有效的方法在汇编和C入口函数里临时加点串口打印打印“我到了第N步”一步步定位。只要串口是好的这种方法就像给代码挂了个GPS。有个小技巧需要强调调试打印别随手乱加都打完了记得统一移除或者用调试宏包起来不然版本发布后用户看到满屏调试信息体验会非常差。5.5 小总结启动流程排查清单现象优先排查方向常用手段无任何输出烧写、串口、复位检查烧写地址、串口配置输出乱码波特率、晶振测波形、核对配置输出后卡死DDR、时钟寄存器对照、打印定位U-Boot能跑但内核起不来bootargs、内核地址手敲命令验证能启动但外设失灵设备树、驱动逐个外设排查6. 启动流程的现代化演进设备树与驱动模型如果你看的是2016年之后的U-Boot你会发现启动流程里到处是dm_*函数和fdt_*函数。这背后其实是两个大变化第一设备树FDT全面取代了板级头文件里的硬件描述。旧的方式是把各种硬件参数写成宏编译进U-Boot二进制改硬件就要重新编译。设备树则把硬件的描述分离出去同一份U-Boot二进制可以配合不同设备树启动不同板子这对产品线繁多的厂商来说便利性极大。在启动代码里U-Boot会解析设备树为每个节点绑定驱动。没有设备树就panic。所以移植的第一步往往是把基础设备树创建好。第二驱动模型DM重写了驱动架构。旧的驱动各自为战初始化顺序靠函数表写死。新的驱动模型用U_BOOT_DRIVER宏注册驱动用设备树节点自动匹配并提供了层级化的probe顺序。驱动模型更优雅但也意味着你要多了解一点uclass、udevice这些概念才能排查驱动初始化问题。我对刚接触U-Boot的同学有一个真诚的建议先不要急着追求最新版本代码细节把经典启动流程理解透彻再去看新机制你会发现很多所谓的新概念本质还是为了解决老问题演进出来的。7. 手把手梳理一份实战启动日志这一节我打算用一个模拟的串口启动日志带你过一遍。这里我用虚构的平台日志但流程与真实情况基本一致。U-Boot 2024.10 (Jan 13 2025 - 10:00:00 0800) CPU: ARMv7 Processor [410fc051] rev 1 (ARMv7) Core: ARMv7 Cortex-A9 Clock: CPU 800 MHz, DDR 400 MHz DRAM: 512 MiB MMC: sdhci12340000: 0 Net: eth0: ethernet12350000 In: serial Out: serial Err: serial Hit any key to stop autoboot: 3我来拆解一下每一行背后的含义U-Boot 2024.10 ...版本号这是环境变量和代码分支的重要参考。排查问题第一步先看版本。CPU...CPU类型和版本。这行是CPU驱动通过读取CP15寄存器得到的。Clock...CPU和DDR频率。频率异常时这行会很明显地暴露出来。DRAM: 512 MiB检测到的内存大小。这里如果显示错误说明DDR初始化参数或地址映射有问题。MMC: sdhci12340000: 0注册了一个MMC控制器实例名字是设备树节点名。如果你这个板子有两个MMC控制器这里可能会显示0,1。Net: eth0...网络接口。没有这一行说明网卡驱动没起来或者网卡不在线。Hit any key to stop autoboot: 3倒计时3秒按任意键进入命令行否则执行bootcmd。从这几行你已经能看出U-Boot做了多少事CPU识别、时钟配置、DDR探测、MMC驱动、网卡驱动、串口控制台、环境变量加载。每一个子系统都是上面提到的启动流程中对应“initr_*”函数完成的。如果其中某一行缺失你可以顺着对应的初始化函数去追效率会高很多。8. 两个必须掌握的命令和一个小技巧最后分享两个我在调试启动流程时几乎天天用的命令以及一个实用小技巧。8.1bdinfo命令它打印当前板子的内存布局、波特率、环境变量地址等信息。每当你怀疑内存映射有问题或者环境变量指针被破坏时先敲这个。8.2printenv/setenv命令启动流程最终很大程度上由环境变量掌控。你可以用printenv bootcmd看看默认启动命令是怎么设置的用setenv临时修改后saveenv保存。很多启动问题就是环境变量里某个地址或参数字符串写错了。8.3 调启动时间的技巧接上串口后从第一条输出到命令行出现的时间可以用秒表量一下。如果发现启动太慢可以查一下U-Boot是否在等待某个外设超时。例如网卡自动协商没完成时会等待MMC初始化超时也会拖慢启动。把不需要的驱动在defconfig里关掉是缩短启动时间最直接的手段。9. 结尾的一点个人体会踏踏实实把U-Boot启动流程过一遍远比在网上搜各种“疑难杂症”的答案有用。因为很多坑之所以难排查本质上就是对“启动顺序”和“依赖关系”缺乏全局把握。我自己在调试过程中最大的收获不是记住某个寄存器的值而是养成了“按阶段画图、按顺序推演”的习惯。拿到一块新板子第一步看start.S第二步看DDR初始化第三步看重定位第四步看设备树和外设驱动每一层都有清晰的边界。这个习惯在后面调试根文件系统、调试内核启动、做产品量产时都帮了大忙。如果你刚开始接触U-Boot不要急着追求读完全部源码。按我上面给的顺序把一条主线走通剩下的细节会自然展开。等你把启动流程像看地图一样印在脑子里再遇到任何“起不来”的问题都会多一份底气。
阅读完成 · 觉得有帮助?