ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

uboot启动流程详解:从第一阶段汇编到内核引导的核心逻辑

uboot启动流程详解:从第一阶段汇编到内核引导的核心逻辑 1. 从一条上电指令说起uboot到底在干什么做嵌入式开发这么多年几乎每天都要跟uboot打交道但真正能把uboot启动流程从头到尾讲清楚的人说实话不多。很多人是会用但不懂用——知道怎么编译、怎么烧写但一旦遇到启动异常面对满屏的打印信息就抓瞎了。这篇文章我想用一整章的篇幅把uboot的启动流程从头到尾拆一遍把每一步背后为什么要这么做的逻辑讲透。先明确一个基本概念uboot全称是Universal Boot Loader它的核心工作就是两件事——初始化硬件和引导操作系统。你可以把它想成是嵌入式系统的接生婆芯片上电后内部固件ROM Code先接管然后找到ubootuboot负责把整个运行环境收拾利索最后把内核扶上马启动流程才算走完。这篇文章主要面向三类人正在做uboot移植的驱动工程师、刚入门嵌入式想搞懂系统启动逻辑的学生、以及被uboot启动问题折磨到头皮发麻的调试人员。读完你至少能搞清楚三件事uboot启动分哪几个阶段、每个阶段的核心任务和关键函数是什么、遇到启动异常该从哪个环节入手排查。整体内容基于我在i.MX6ULL、STM32MP1、4412等平台上的实际移植和调试经验尽量说人话不整虚的。2. 启动的整体节奏闭环式的两段式架构2.1 为什么uboot要分成两个阶段uboot的启动流程在设计上非常经典它把整个启动过程切成两段第一阶段stage1和第二阶段stage2。第一阶段的代码用汇编写第二阶段的代码用C语言写。这种划分不是拍脑袋想出来的而是由硬件的物理限制决定的。芯片刚上电时DDR内存还没有初始化CPU只能直接访问芯片内部的内存。问题来了内部内存SRAM或ROM通常只有几十KB到几百KB根本放不下完整的uboot镜像。所以uboot采用了这样一个策略第一阶段代码体积小可以在内部内存里跑负责把DDR初始化好一旦DDR可用就把完整的uboot镜像从存储介质SD卡、eMMC、Flash里搬进DDR然后跳到第二阶段继续执行。这个思路跟先搭个脚手架再盖大楼是一个道理。第一阶段是脚手架保证第二阶段有施展空间。2.2 整条启动链路的宏观视图我把uboot的完整启动流程按执行顺序整理如下你可以把这个清单当作全文的地图芯片上电CPU从固定地址取指通常是内部ROM的boot ROM代码内部boot ROM根据启动引脚配置从SD卡/eMMC/Flash等介质读取uboot第一阶段到内部SRAM第一阶段汇编代码执行设置CPU模式、关看门狗、初始化时钟、初始化DDR将完整的uboot镜像从存储介质拷贝到DDR中跳转到第二阶段入口进入C语言世界board_init_f初始化DDR、串口、定时器等基础外设规划内存布局代码重定位将uboot自身从SRAM搬移到DDR的高地址处board_init_r重新初始化所有外设包括完整板级硬件进入main_loop等待用户按键或自动执行bootcmdbootcmd中定义的内核启动指令通常是bootm或bootz被触发uboot将内核镜像拷贝到内存构造启动参数设备树、ATAG跳转到内核入口步骤1到5是第一阶段的范畴6到11是第二阶段的范畴。下面我分章节把这几个关键环节拆开细讲。3. 第一阶段汇编代码里的三把火3.1 入口文件与启动头uboot第一阶段的入口文件是arch/arm/lib/vectors.SARM平台真正的初始化代码则集中在arch/arm/cpu/armv7/start.S或arch/arm/cpu/armv8/start.S这类文件中。芯片上电后会从一个硬件规定好的地址取第一条指令在ARM平台这个地址通常指向_start符号。这里有个细节值得注意uboot镜像的最开头并不是直接的代码而是异常向量表。这是ARM架构对启动代码的硬性要求。异常向量表存放着复位、中断、abort等异常处理函数的跳转指令其中第一条就是复位异常处理入口也就是_start。当CPU上电复位或软复位时硬件会自动跳到这里。检查一下你的uboot二进制文件开头16字节你会看到类似这样的内容hexdump -C u-boot.bin | head -n 4第一条指令通常是跳转指令比如ea000012不同平台可能略有差异后面跟着的是uboot镜像头信息或设备树二进制数据。如果这个头信息不对后面就算代码写对了也跑不起来。3.2 核心硬件初始化关狗、切模式、配时钟进入_start之后代码做的事情可以用三把火来概括第一把火切换CPU工作模式并关闭中断。CPU刚上电时可能处于SVC模式超级用户模式或未定义状态uboot需要确保CPU处于一个确定的状态下执行代码。同时关掉所有中断cpsid i指令避免启动过程中被异常打断导致状态不可控。这一步类似于先清场再干活。第二把火关看门狗。很多SoC的看门狗默认是开启的如果在规定时间内没有喂狗系统就会不断复位。uboot启动是个相对漫长的过程特别是从SD卡读取数据时不关看门狗就等着无限重启吧。不同平台关狗的方式不一样有些是直接写寄存器有些是通过写特定的解锁序列。我曾在某国产芯片上踩过坑关狗寄存器需要先写入0x12345678解锁再写入0x87654321关闭顺序写反了直接卡死。第三把火初始化时钟和DDR。这个过程相对较复杂。芯片上电后默认的时钟频率可能很低为了省电和安全uboot需要将CPU频率、总线频率、外设时钟配置到工作频率。DDR初始化尤其重要因为后续所有代码都要在DDR里跑。DDR初始化涉及控制器寄存器配置、时序参数tRCD、tCL、tRP等设置、DDR Training训练校准等环节如果参数不对轻则运行不稳定重则根本起不来。可以理解为前两把火是保证活着第三把火是保证活得舒服。3.3 从存储介质到SRAM的第一跳硬件初始化的目的是为了能够顺利从外部存储介质读取数据。这一步在uboot里通常对应board_init_f之前的一段汇编代码核心任务是把uboot自身从SD卡/eMMC/NOR Flash搬运到SRAM中。这里的关键是搬运工具copyloop。代码通过轮询SD卡控制器状态寄存器等待数据读取完成数据到了之后利用ldr和str指令逐字节或逐字地搬到内部SRAM。我见过很多新手在这里犯迷糊为什么不能直接跳过去执行原因很简单——外部存储介质里的代码CPU没法直接取指执行。以SD卡为例SD卡控制器不映射到CPU的地址空间必须通过寄存器以块为单位去读。在i.MX6ULL上这段逻辑由drivers/mmc/mmc.c配合board/freescale/mx6ullevk/下的板级配置完成。uboot镜像在存储介质上是有固定存放位置的通常是偏移1KB或8KB处这个偏移位置由CONFIG_SYS_MMCSD_RAW_MODE_U_BOOT_SECTOR宏定义移植时务必确认这个值和烧写脚本里dd命令的偏移量一致否则就会出现烧进去了但启动不了的诡异现象。4. 第二阶段C语言接管后的重量级操作4.1 第一个C函数board_init_f的内存规划智慧当代码跳转到relocate_code之后第二次搬运完成uboot正式开始执行C语言代码。第一个重量级C函数是board_init_f这个_f后缀代表前阶段before relocation重定位之前的阶段。这个函数的核心职责不是把所有外设都初始化完而是规划好整个uboot运行期的内存布局。你可以这样理解uboot在SRAM里跑的时候不知道自己之后在DDR里应该怎么摆放代码、堆栈、全局数据、设备树。board_init_f就像一个设计师先量好地皮画好图纸然后才开工盖楼。具体来说board_init_f通过一串init_sequence_f[]函数指针数组依次执行了以下关键工作初始化DDR控制器确认DDR容量和可用性初始化串口让串口打印成为可能这就是为什么上电后能看到串口日志的起点初始化定时器为后续延时和超时机制打基础计算整个uboot代码段、数据段、堆栈、堆、设备树、内核加载地址等各自需要的空间最终确定栈顶地址gd-start_addr_sp和uboot重定位的目标地址gd-relocaddr这个阶段最容易被忽略的是内存布局的计算逻辑。uboot通过一系列宏CONFIG_SYS_MALLOC_LEN、CONFIG_SYS_STACK_SIZE等来确定各个区域的大小。画图时栈从高地址往下生长malloc堆区放在下面然后是uboot代码段、数据段最后是设备树。布局顺序一旦写错轻则内存重叠导致数据被覆盖重则直接死机。4.2relocate_codeuboot的乾坤大挪移board_init_f完成规划后relocate_code开始执行真正的重定位操作。这一步是整个流程里最具有炫技感的部分同时也是最容易出问题的部分。为什么要重定位因为uboot当时还在SRAM或DDR的低地址处执行但最终它希望自己能跑在DDR的高地址处。高地址区域不容易被后续加载的内核镜像覆盖也更安全。所以uboot要把自己从当前位置乾坤大挪移到目标位置。重定位涉及两部分代码复制和地址修正。代码复制好理解就是把程序从源地址搬到目的地址。地址修正则比较微妙——因为代码里的函数调用、全局变量访问编译链接时用的是链接地址但运行时用的可能是临时地址。如果链接地址和运行地址不一致就需要动态修正。现代uboot通过-fPIC位置无关代码的方式规避了大部分问题但并不能完全避免。实际调试中如果重定位后程序跑飞或者全局变量莫名清零多半是地址修正链路出了问题。验证重定位是否成功有一个土办法重定位后在串口调试终端输入bdinfo命令查看relocaddr的值是否指向DDR高地址区域。如果relocaddr和编译链接地址一致说明alpha和omega对上了。4.3board_init_r全量初始化的最终章重定位完成之后代码跳转到board_init_r这个_r代表后阶段after relocation重定位之后的阶段它才是全面初始化外设的舞台。board_init_r同样通过一组init_sequence_r[]函数指针数组来完成初始化但与_f阶段相比它的目标发生了根本性改变_f阶段的目标是让uboot能跑起来_r阶段的目标是让uboot能启动内核。这阶段的初始化项包括完整的板级外设初始化GPIO、I2C、SPI、网卡、USB等环境变量加载从存储介质中读取保存的环境变量如bootcmd、bootargs设备树文件准备将设备树的物理地址设置到gd-fdt_blob中中断使能允许中断控制台初始化准备好命令行交互界面初始化完成后board_init_r会调用run_main_loop正式进入uboot的用户态——命令行模式。5. 环境变量、bootcmd与内核引导的闭环逻辑5.1 环境变量uboot的配置文件机制到了main_loop之后uboot就等待用户交互了。但在交互之前必须先加载环境变量。环境变量是uboot的配置系统它决定了uboot启动时的行为。环境变量默认值在代码里定义default_environment[]但用户可以修改并通过saveenv命令保存到存储介质。环境变量加载有一个优先级链默认值 存储介质中的值eMMC/SD/Flash特定扇区 命令行临时设置。这个机制就像笔记本的系统默认设置和用户自定义设置之间的关系。实际调试中环境变量引起的问题非常隐蔽。比如你明明改了bootcmd重启后却发现执行的是旧命令。这种情况八成是因为环境变量没保存成功或者保存的扇区被其他数据覆盖了。排查时用printenv命令查看当前生效值用saveenv重新保存多数问题能解决。5.2 bootcmd的两级跳转逻辑bootcmd是整个uboot启动流程的终点站。当main_loop中的倒计时结束默认倒计时见CONFIG_BOOTDELAY宏系统没有收到用户的干预按键uboot就会自动执行bootcmd环境变量中定义的命令。bootcmd通常是一串命令的组合用;或连接。比较典型的设置是setenv bootcmd mmc dev 0; fatload mmc 0:1 0x80800000 zImage; fatload mmc 0:1 0x83000000 imx6ull.dtb; bootz 0x80800000 - 0x83000000 saveenv这条命令链的逻辑是先切换到MMC设备0然后把内核镜像zImage加载到DDR地址0x80800000把设备树imx6ull.dtb加载到0x83000000最后用bootz命令启动内核。bootz是启动非压缩zImage内核的命令如果内核是uImage格式则用bootm命令如果把内核编译进了uboot镜像则用booti命令。5.3 从uboot到内核最后的交接仪式启动内核以bootz为例时uboot会执行一次交接操作。这个交接包含三个步骤第一步准备启动参数。现代内核通过设备树DTB传递硬件信息uboot需要确保设备树地址正确且内容完整。同时内存大小、启动参数console、rootfs等也通过bootargs环境变量传给内核。第二步检查镜像合法性。bootz会校验镜像头的魔数Magic Number是否匹配确认该文件确实是合法的ARM Linux内核镜像。如果镜像格式不对uboot会报Wrong Image Format for bootm之类的错误。第三步跳转到内核入口。uboot会关掉MMU和D-Cache因为内核要重新建立自己的页表然后设置CPU寄存器r00, r1机器类型, r2设备树地址最后跳转到内核入口地址。从这里开始处理器执行权正式交接给Linux内核。注意从内核入口开始uboot的使命就完成了。一旦内核崩溃或退出不会有人再拉它一把这时候整个系统就死机了。所以uboot阶段虽然短暂但每一步都必须稳妥可靠——它没有第二次机会。6. 从零移植uboot的踩坑指南6.1 移植三板斧选平台、定配置、抓启动聊完流程必须讲讲移植。因为很多人搞明白流程就是为了移植。uboot移植以i.MX6ULL为例遵循三板斧套路第一板斧选对参考板。找一块和自己硬件最接近的官方参考板复制其板级目录。比如正点原子的alpha开发板是基于NXP官方的mx6ullevk改的移植起点就是board/freescale/mx6ullevk/这个目录。在现有基础上修改远比自己从零写一个board目录靠谱。第二板斧改配置。核心是configs/目录下的defconfig配置文件。需要重点关注的宏包括CONFIG_SYS_MMCSD_RAW_MODE_U_BOOT_SECTORuboot在SD卡中的偏移扇区、CONFIG_SYS_MEM_TOP_HIDE保留顶层内存给安全监控用、CONFIG_SYS_SDRAM_BASEDDR基地址i.MX6ULL为0x80000000等。大多数启动问题都是配置宏对应不上导致的。第三板斧抓串口打印。完成移植后第一时间看串口输出。串口是uboot调试的眼睛几乎80%的移植问题都能通过分析串口打印信息定位。6.2 经典启动异常与排查速查表我在不同平台移植uboot时积累了一些常见问题整理成下表供参考现象可能原因排查思路上电后串口无任何输出时钟未配置、串口引脚复用错误、uboot头部烧写偏移错误检查boot引脚、检查ddr训练日志、检查烧写脚本偏移量只打印第一行就卡死DDR初始化失败或时钟配置异常逐步添加打印信息定位卡死函数能进uboot但bootcmd无法启动内核内核镜像地址错误、设备树地址不对、bootargs配置错误单独在uboot命令行测试fatload和bootz命令启动内核后屏幕无显示设备树中显示节点配置错误或启动参数console不对检查bootargs中的consolettySAC0之类参数是否匹配实际串口环境变量保存失败存储分区写保护、扇区地址冲突检查CONFIG_ENV_OFFSET是否与uboot镜像区域重叠6.3 几个极其隐蔽的坑分享几个我在实际工程中踩过、网上资料又很少提及的坑。坑一SRAM容量不够。有些SoC的内部SRAM只有128KB而uboot第一阶段代码加上其他开销可能就接近上限了。当你发现代码能编译通过但烧写后总是启动失败先查SRAM容量和uboot SPL或相当于第一阶段镜像的实际大小关系。坑二DDR Training时间过长。某些平台DDR Training机制相对耗时比如几百毫秒甚至数秒这段时间里看门狗如果还在跑就会导致反复复位。解决思路有两种确保看门狗关闭代码在DDR初始化之前执行或者把看门狗的超时时间设置得足够长。坑三链接脚本与内存布局不匹配。如果你改了DDR大小千万别忘了同步修改u-boot.lds中的内存布局和CONFIG_SYS_SDRAM_SIZE。这个坑最阴险的地方在于编译和烧写都正常但uboot就是跑不起来或者起来后功能不正常。检查时第一反应应该是内存大小相关的宏和链接脚本是否一致。7. 把uboot流程变成你的肌肉记忆最后分享一点个人体会。uboot启动流程这事光看书和文章是不够的必须亲手调试出来才有感觉。我建议你找一个便宜的开发板比如i.MX6ULL或者STM32MP1系列按照以下顺序做三轮练习第一轮跑通。拿到官方的uboot源码和参考板配置编译、烧写、看到串口能输出Hit any key to stop autoboot为止。这个过程让你熟悉工具链和烧写方式。第二轮读懂。在board_init_f的init_sequence_f[]数组中逐个打印函数的执行标记看看每个函数执行的前后顺序。然后跟踪relocate_code前后的PC指针变化真正理解重定位在干什么。第三轮破坏。故意制造一些故障——把DDR参数改错、把CONFIG_SYS_MMCSD_RAW_MODE_U_BOOT_SECTOR改大、把bootcmd清空——然后观察系统表现。只有亲手把系统搞挂几次你才会真正记住这些环节的重要性。等这三轮做完你会发现uboot的启动流程已经刻进你的肌肉记忆里了。以后再遇到启动相关问题你不需要翻源码就能快速定位大概范围——这种能力才是嵌入式工程师最宝贵的财富。
返回列表