Uboot详解
1. 文档结构介绍
首先简介uboot, 给出uboot的官网.
然后介绍uboot的编译系统, 让你能了解到SPL和u-boot.bin是如何编译出来的, 哪些C代码会被编译进SPL和u-boot.bin.
接着会介绍uboot的启动流程, 从第一行汇编代码开始, 梳理一遍代码的运行流程.
最后一章会介绍uboot里面命令的执行流程, 如何定义一个命令, 以及uboot当前已经支持的命令.
2. u-boot 简介
U-boot全称UniversalBootLoader, 即通用bootloader.
它是德国DENX小组的开发用于多种嵌入式CPU的bootloader程序, UBoot不仅仅支持嵌入式Linux系统的引导, 它还支持NetBSD,VxWorks,QNX,RTEMS,ARTOS,LynxOS嵌入式操作系统。UBoot除了支持PowerPC系列的处理器外,还能支持MIPS、x86、ARM、NIOS、XScale等诸多常用系列的处理器
官网:
Code: http://git.denx.de/?p=u-boot.git;a=summary
Wiki: http://www.denx.de/wiki/U-Boot
3. u-boot编译系统
假设你有一块ARM CPU的板子, 当给板子插上电源后, 最先运行的程序是什么?
最先运行的程序CPU内部的ROMcode. 这段ROMcode的运行逻辑决定了该CPU支持从哪些外设启动(sdcard, Emmc, nandflash …). 一般CPU的芯片手册会解释ROMcode的运行逻辑.
假设ROMcode决定从sdcard启动, 那么接下来ROMcode会做什么呢?
它会从sdcard拷贝一段程序到CPU内部的SRAM.
具体如何拷贝, 取决于ROMcode的实现, 可以是读扇区或者读FAT文件系统.
拷贝到哪里呢? 上面已经说了, 是CPU内部的SRAM, 为什么是内部的SRAM而不是外部的DDRAM呢? 因为ROMcode不可能知道CPU外部挂的到底是什么型号的DDRAM, 因此无法初始化外挂的DDRAM, 只能拷贝到内部的SRAM.
CPU内部的SRAM都不大, 一般也就96KB左右, 因此size限制决定了ROMcode只能搬移一段很小的程序(image-stage1)到SRAM, 然后SRAM中运行的这段小程序会初始化DDRAM, 并将一段更大的程序(image-stage2)搬移到DDRAM运行.
对于u-boot而言, 它完全满足上述场景. u-boot可以编译出image-stage1和image-stage2这2段代码.
image-stage1一般叫做SPL, 或者MLO, 大小约为60KB左右. (后文统称SPL)
image-stage2一般叫做u-boot.bin, 大小约360KB左右.
3.1 u-boot编译步骤
要编u-boot, 总共分几步? 3步:
1> 获取u-boot源代码, 配置好CROSS_COMPILE 和 ARCH
2> make xxx_defconfig
3> make
xxx_defconfig是其他人已经编写好的配置文件, 用于在编译u-boot之前配置u-boot.
如果defconfig里面使能了SPL, 那么你将得到SPL和u-boot.bin这两个image.
那么问题来了:
哪些代码将被编译进SPL, SPL的入口函数是谁?
哪些代码将被编译进u-boot.bin呢?
make xxx_defconfig又干了些什么事情呢?
接下来, 让我们深入进去看看u-boot的编译系统. 要了解它的编译系统, 你得先有点make & Makefile的基础知识.
3.2 make & Makefile
NOTE: 下面内容是对<跟我一起写Makefile-陈皓.pdf>一文的缩写, 主要列出了u-boot的Makefile中用到的一些语法. 如果你想全面了解Makefile, 建议你在百度里面找到这篇文章.
make
一般我们在编译某个代码时, 会进入到代码所在目录, 然后敲一个make命令. 之后, make命令就会在当前目录下寻找Makefile或者makefile文件, 解析并执行该文件.
当然, make命令后面可以跟一些参数, 例如:
make -C DIRECTORY -f FILE
进入到DIRECTORY目录, 找到FILE文件, 解析并执行该文件.
如果没有-f参数, FILE默认就是Makefile或makefile
make target
在Makefile中找到target目标, 并执行该目标. 关于target的概念, 我们随后就会介绍.
如果make命令后面没有显示指明target, 那么make会将Makefile中的第一个target做为默认目标.
对于非默认目标, 则只能通过make target这种显示指定目标的方式使其执行.
Makefile
target
target的基本语法格式如下:
[ ]里面的内容是可选的
targets代表目标名, 也就是当前目录下某个文件的名称.
prerequisites代表该targets的依赖关系
command代表命令. 如果命令与targets在同一行, 则需以”;”分隔, 如果另起一行, 则前面必须是”TAB” . Makefile中用”TAB”标示一个command.
这个语法代表个什么意思呢? 概况起来就一句话, 记牢这句话:
如果targets不存在; 或者targets存在但prerequisites有更新: 则执行command命令.
举个例子, 假设我们在当前目录下有个main.c文件, 要将其编译成main.o, 则Makefile可以这样写:
main.o : main.c
gcc -c main.c
当敲了make命令之后, make会把main.o做为默认目标:
然后发现main.o在当前目录下不存在, 则会执行gcc -c main.c命令. 该命令就会生成main.o, 也就是会生成目标文件main.o
如果再次敲make命令, 则什么都不会发生
如果修改了main.c, 再次敲make命令, 则会重新运行gcc -c main.c命令, 再次生成main.o
PHONY target
我们来看看这样一个Makefile
all :
@echo this is all
Note: 在command前加上@, 代表不显示该命令本身, 只显示命令的结果.
如果你敲一个make命令, 会发生什么?
如果你再次敲一个make命令, 会发生什么?
touch all, 然后在敲make命令, 会发生什么?
结合前一节, 思考一下这里的答案.
发现这个target跟前一节那个例子的不同之处了吗?
前一节的target会创建目标文件main.o, 但是这里的target永远不会创建目标文件all. 假设这个target是Makefile中的第一个目标, 那么每次敲make命令, 都会导致该target下面的command被执行, 因为目标文件永远不存在.
这种不会创建目标文件的target就叫伪目标. 伪目标的主要用途就是用于那些需要无条件执行的target. 例如clean, 用于清除编译生成的临时文件.
但是, 如果当前目录下恰好有一个同名的目标文件, 例如我们手动touch了一个all文件, 那这个target就永远都不会执行了. 因为目标文件已经存在, 而且依赖关系没有更新(没有依赖关系也就是没有更新).
为了防止这种意外, 我们可以用.PHONY: target显示指明某个目标为伪目标, 例如:
.PHONY: all
all :
@echo this is all
这样, 不管当前目录下是否有all这个目标文件, all这个target都会执行.
伪目标除了不会生成目标文件, 其它规则与正常的target一模一样, 伪目标后面也可以定义依赖关系. 如果伪目标是Makefile的第一个目标, 也会被做为默认目标.
VPATH
默认情况下, make命令会在当前路径下搜寻目标文件和依赖文件, 例如上例中的main.c和main.o
但是, 如果目标文件或者依赖文件不在当前目录下, 而是被组织到了某个子目录下, 该怎么办呢?
Makefile 文件中的特殊变量”VPATH”就是用于解决这个问题的, 如果没有指明这个变量, make 只会在当前的目录中去找寻依赖文件和目标文件. 如果定义了这个变量, 那么, make
就会在当当前目录找不到的情况下, 到所指定的目录中去找寻文件了.
VPATH = src:../headers
上面的的定义指定两个目录: “src”和”../headers”, make 会按照这个顺序进行搜索. 目录由“冒号”分隔. (当然, 当前目录永远是最高优先搜索的地方)
更多关于Makefile
Makefile中也可以定义变量, 引用其他Makefile, 使用条件判断, 使用通配符等等. 它还有一些内建的关键字.
如果你在阅读某个Makefile文件时遇到了障碍, 你可以去查询<跟我一起写Makefile-陈皓.pdf>一文, 或者直接在网上搜索.
3.3 make xxx_defconfig
make xxx_defconfig, 基于前一节的知识可以得知, 去Makefile里面找xxx_defconfig这个target.
那我们来看看u-boot根目录下的Makefile:
![]()
%是通配符, %config就是匹配任何config结尾的目标, 所以xxx_defconfig就是匹配的这个target
它依赖后面的几个其他目标, 就不细看了
然后执行命令
$(Q)用于控制打印信息的, 先不管;
$(MAKE)代表make命令
build在scripts/Kbuild.include文件中被定义为build := -f $(srctree)/scripts/Makefile.build obj, 顶层Makefile中include了scripts/Kbuild.include
$@代表目标, 也就是xxx_defconfig.
整条语句相当于
make -f scripts/Makefile.build obj=scripts/kconfig xxx_defconfig
scripts/Makefile.build中并没有定义xxx_defconfig这个目标, 不过它include了scripts/kconfig/Makefile. 代码如下, 逻辑就不细说了
![]()
scripts/kconfig/Makefile中定义了xxx_defconfig这个目标, 如下:
![]()
obj在之前被设置过了, 为scripts/kconfig
%_defconfig这个目标依赖$(obj)/conf, 也就是依赖scripts/kconfig/conf.
但是你在scripts/kconfig/Makefile中找不到目标$(obj)/conf的定义. 不过没关系, Makefile可以自动推导(参见 跟我一起写Makefile-陈皓.pdf).
自动推导的结果就是:
gcc -o conf -c conf.c
接下来, 就会运行命令了
$< 代表依赖目标
$(SRCARCH)在Makefile中被赋值为../
$(Kconfig)在Makefile中被赋值为Kconfig
所以整个命令翻译过来就是:
$(obj)/conf --defconfig=arch/../configs/xxx_defconfig Kconfig
在接下来发生了什么, 你就得去看看scripts/kconfig/conf.c这个C文件了. 它会接受并解析参数--defconfig.
由于conf.c太过复杂, 这里就不多说了, 只需要了解这个原理即可, 在有需要的时候, 再来深入分析conf.c文件.
.config
搞了这么久, make xxx_defconfig到底干了些什么呢, 简单总结一下:
它会把u-boot根目录下的configs/xxx_defconfig做为参数, 传递给scripts/kconfig/conf.c.
conf.c会根据这个配置文件, 最终在u-boot根目录了下生成.config
.config文件里面是各种配置, 决定着编译哪些c代码等等.
我们以mx6qsabreauto_defconfig这个配置文件为例:
当输入命令 make mx6qsabreauto_defconfig, 我们可以在u-boot根目录下看到生成的.config文件. .config中有几个重要的信息:
vi .config

红色框内的信息, 决定了当前的配置是针对哪个板子的. 这些信息是如何得到的呢? 会是在mx6qsabreauto_defconfig中定义的吗? 那我们看看mx6qsabreauto_defconfig这个文件的内容:
cat configs/mx6qsabreauto_defconfig

并没有定义CPU, SOC等. 只定义了一个CONFIG_TARGET_MX6QSABREAUTO=y
其实真正的定义在这里:
cat board/freescale/mx6qsabreauto/Kconfig

新增一块板的支持
根据.config一节的解释, 如果想在u-boot下新增一块板的支持, 只需要如下步骤:
- 在configs/下创建一个配置文件xxx_defconfig. 在配置文件里面选中某个TARGET, 即: CONFIG_TARGET_MYOWNTARGET=y
- 在board/MYVENDOR/MYBOARD目录下创建一个Kconfig文件, 在Kconfig里面定义如下内容:
if TARGET_MYOWNTARGET
config SYS_BOARD
default "MYBOARD"
config SYS_VENDOR
default "MYVENDOR"
config SYS_SOC
default "MYSOC"
config SYS_CONFIG_NAME
default "MYCONFIGNAME"
endif
- 在include/configs/目录下创建一个名为MYCONFIGNAME.h的文件. 在这个文件中存放特定的一些配置.
- 在board/MYVENDOR/MYBOARD/目录下创建板级文件myboardfile.c和Makefile.
如果你在板级文件中include <common.h>, 那么你的板级文件将自动include “include/configs/MYCONFIGNAME.h”.
引用的顺序是这样的: include/common.h -> include/config.h -> include/configs/$(CONFIG_SYS_CONFIG_NAME).h
其中, include/config.h是自动生成的, 当你敲下make命令之后, 根据重重依赖关系, 最终由scripts/Makefile.autoconf自动生成.
3.4 make
当敲下make命令之后, 会发生什么呢? 根据Makefile一章的知识, 它会去寻找第一个target做为默认目标.
那我们来看看u-boot顶层目录下的Makefile的第一个target是什么:

这个_all目标的依赖关系怎么是空呢? 原因是在Makefile的后面重新定义了该target的依赖关系:

由于KBUILD_EXTMOD一般是空, 因此_all: all成立. _all依赖all这个target.
那all这个target又是如何定义的呢? 继续看看Makefile中对于all的定义:

all依赖$(ALL-y)这个目标
继续找找$(ALL-y)在Makefile中是如何定义的:

这里只截取了关于ALL-y的部分代码, 不过我们需要的重要信息都在这里了. 从这段代码里面, 我们可以得出两个重要信息:
ALL-y 默认被赋值为u-boot.srec u-boot.bin System.map binary_size_check
all: $(ALL-y), 也就是说all目标依赖于u-boot.srec u-boot.bin System.map binary_size_check 这几个目标. 而这几个目标就是用于生成image-stage2阶段的镜像, 也就是用于生成u-boot.bin
ALL-$(CONFIG_SPL) += spl/u-boot-spl.bin
如果定义了CONFIG_SPL, 那么all目标还会依赖于spl/u-boot-spl.bin这个目标. 这个目标就是用于生成image-stage1阶段的镜像, 也就是用于生成SPL
了解了上述逻辑之后, 接下来我们分别分析SPL和u-boot.bin是如何生成的.
SPL
本小节我们主要要弄清楚2个问题
1. 到底哪些代码会被编译进SPL
2. SPL的入口代码在哪里
接下来开始我们的探索吧.
从前面的分析可知, spl/u-boot-spl.bin这个目标会负责生成SPL, 我们先在顶层Makefile中找到这个目标:
![]()
spl/u-boot-spl.bin依赖spl/u-boot-spl.
spl/u-boot-spl的定义如下:

它依赖tools和prepare这两个目标, 这两个目标是做一些准备工作, 就不细看了.
重点在于下面的command : $(Q)$(MAKE) obj=spl -f $(srctree)/scripts/Makefile.spl all , 它代表将参数obj设置为spl, 然后执行scripts/Makefile.spl中的all目标.
接下来我们去scripts/Makefile.spl这个文件里面看看它的all目标是什么样子的:

很清楚了, 最重要的一个目标是$(obj)/$(SPL_BIN).bin
$(obj)/$(SPL_BIN).bin的定义如下:
![]()
它依赖于$(obj)/$(SPL_BIN)
$(obj)/$(SPL_BIN)的定义如下:
![]()
它依赖于3个重要的目标: $(u-boot-spl-init) $(u-boot-spl-main) $(obj)/u-boot-spl.lds
其中前两者决定了哪些代码将被编译进SPL
最后一个lds是链接文件, 决定了入口代码
下面, 我们就本节开头提出的2个问题, 分别进行分析.
哪些代码会被编译进SPL
由前文的分析可知, scripts/Makefile.spl中的$(u-boot-spl-init) $(u-boot-spl-main)这两个目标决定了哪些代码会被编译进SPL.
我们看看这两个目标的定义:
![]()
head-y
在scripts/Makefile.spl中, head-y的定义如下:
![]()
这句话的意思是给head-y加上前缀$(obj)/ , 那真正的head-y是在哪里定义的呢?
我们在scripts/Makefile.spl的其它地方找不到任何head-y的定义, 这不科学! 别急, scripts/Makefile.spl中还有这样一句话:
![]()
ARCH的定义是在顶层目录的config.mk中, config.mk解析顶层目录下的.config以获取相关信息.
下面一行include了另外一个Makefile, 就像c语言中引用.h文件一样, 这个Makefile中的内容会被插入到scripts/Makefile.spl中.
如果ARCH=arm, 那么这个Makefile就是arch/arm/Makefile, 查看这个文件, 我们可以从该文件中找到head-y的定义:

意思也很清楚了:
默认情况下 head-y被赋值为arch/arm/cpu/$(CPU)/start.o, 也就是会把arch/arm/cpu/$(CPU)/目录下的start.S编译成start.o .
不过如果定义了CONFIG_SPL_BUILD与CONFIG_SPL_START_S_PATH, 则会编译CONFIG_SPL_START_S_PATH目录下的start.S
至此, 我们已经看到了第一个被编译进SPL的代码了. 汇编文件start.S
需要提醒注意的是, arch/arm/Makefile中不仅定义了head-y, 也定义了很多libs-y, libs-y将在下面介绍.
libs-y
在scripts/Makefile.spl中, libs-y的定义如下(注意, arch/$ARCH/Makefile中也定义了很多libs-y):
HAVE_VENDOR_COMMON_LIB = $(if $(wildcard $(srctree)/board/$(VENDOR)/common/Makefile),y,n)
libs-y += $(if $(BOARDDIR),board/$(BOARDDIR)/)
libs-$(HAVE_VENDOR_COMMON_LIB) += board/$(VENDOR)/common/
libs-$(CONFIG_SPL_FRAMEWORK) += common/spl/
libs-$(CONFIG_SPL_LIBCOMMON_SUPPORT) += common/
libs-$(CONFIG_SPL_LIBDISK_SUPPORT) += disk/
libs-$(CONFIG_SPL_DM) += drivers/core/
libs-$(CONFIG_SPL_I2C_SUPPORT) += drivers/i2c/
libs-$(CONFIG_SPL_GPIO_SUPPORT) += drivers/gpio/
libs-$(CONFIG_SPL_MMC_SUPPORT) += drivers/mmc/
libs-$(CONFIG_SPL_MPC8XXX_INIT_DDR_SUPPORT) += drivers/ddr/fsl/
libs-$(CONFIG_SYS_MVEBU_DDR) += drivers/ddr/mvebu/
libs-$(CONFIG_SPL_SERIAL_SUPPORT) += drivers/serial/
libs-$(CONFIG_SPL_SPI_FLASH_SUPPORT) += drivers/mtd/spi/
libs-$(CONFIG_SPL_SPI_SUPPORT) += drivers/spi/
libs-y += fs/
libs-$(CONFIG_SPL_LIBGENERIC_SUPPORT) += lib/
libs-$(CONFIG_SPL_POWER_SUPPORT) += drivers/power/ drivers/power/pmic/
libs-$(CONFIG_SPL_MTD_SUPPORT) += drivers/mtd/
libs-$(CONFIG_SPL_NAND_SUPPORT) += drivers/mtd/nand/
libs-$(CONFIG_SPL_DRIVERS_MISC_SUPPORT) += drivers/misc/
libs-$(CONFIG_SPL_ONENAND_SUPPORT) += drivers/mtd/onenand/
libs-$(CONFIG_SPL_DMA_SUPPORT) += drivers/dma/
libs-$(CONFIG_SPL_POST_MEM_SUPPORT) += post/drivers/
libs-$(CONFIG_SPL_NET_SUPPORT) += net/
libs-$(CONFIG_SPL_ETH_SUPPORT) += drivers/net/
libs-$(CONFIG_SPL_ETH_SUPPORT) += drivers/net/phy/
libs-$(CONFIG_SPL_USBETH_SUPPORT) += drivers/net/phy/
libs-$(CONFIG_SPL_MUSB_NEW_SUPPORT) += drivers/usb/musb-new/
libs-$(CONFIG_USB_DWC3_GADGET) += drivers/usb/dwc3/
libs-$(CONFIG_USB_DWC3_GADGET) += drivers/usb/gadget/udc/
libs-$(CONFIG_SPL_USBETH_SUPPORT) += drivers/usb/gadget/
libs-$(CONFIG_SPL_WATCHDOG_SUPPORT) += drivers/watchdog/
libs-$(CONFIG_SPL_USB_HOST_SUPPORT) += drivers/usb/host/
libs-$(CONFIG_OMAP_USB_PHY) += drivers/usb/phy/
libs-$(CONFIG_SPL_SATA_SUPPORT) += drivers/block/
libs-y := $(addprefix $(obj)/,$(libs-y))
libs-y := $(patsubst %/, %/built-in.o, $(libs-y))
我们看看最后一行: libs-y := $(patsubst %/, %/built-in.o, $(libs-y))
它的意思是从libs-y定义的路径中, 截取”/”之前的部分, 然后加上/built-in.o的后缀. 然后重新赋值给libs-y.
假设libs-y := fs/ , 则经过上述转换后, libs-y变为 libs-y := fs/built-in.o. 而fs下的built-in.o由于fs目录下的Makefile生成的(这块的细节就不推导了, 记住这个结论即可).
所以, 上述代码综合来看, 就是libs-y依赖蓝色字体定义的那些目录下的Makefile文件, 依赖这些Makefile文件决定哪些代码会被编译进SPL
至此, 我们就知道所有被编译进SPL的代码了.
SPL的入口代码在哪里
一个程序的入口函数是由链接脚本决定的. SPL也不例外.
那么SPL的链接脚本是哪个呢?
我们前文介绍过, scripts/Makefile.spl中, $(obj)/$(SPL_BIN)的定义如下:
![]()
它依赖于3个重要的目标: $(u-boot-spl-init) $(u-boot-spl-main) $(obj)/u-boot-spl.lds
其中前两者决定了哪些代码将被编译进SPL
最后一个lds是链接文件, 决定了入口代码
那我们在看看$(obj)/u-boot-spl.lds的定义:
![]()
依赖于$(LDSCRIPT)
再来看看$(LDSCRIPT)的定义:

首先看CONFIG_SPL_LDSCRIPT有没有定义链接脚本的路径, 如果没有, 依次去BOARDDIR、CPUDIR、$(ARCH)/cpu下面找. 找到任何一个就不继续往后找了.
一般情况下, 都会最终找到arch/$(ARCH)/cpu/u-boot-spl.lds . 对于arm架构, 就是arch/arm/cpu/u-boot-spl.lds . 来看看这个文件.
这个文件很长, 只看与我们相关的一段:

ENTRY表示入口函数是前面编译的那一堆代码中的_start函数.
_start函数一般定义在arch/arm/lib/vectors.S中.
至此, 我们也可知, SPL最先运行的代码就是vectors.S中的_start函数.
MLO是如何被编译出来的
先简单回顾一下前面的内容.
uboot顶层目录的Makefile中有如下这段代码:

由上面这段代码可知, scripts/Makefile.spl 里面的 all目标决定了如何编译SPL. 不过all目标最终只会生成spl/u-boot-spl.bin这个文件.
在scripts/Makefile.spl中, 也定义了一个用于编译MLO的目标:

不过scripts/Makefile.spl并没有人显示的依赖MLO这个目标, 那么MLO又是如何生成出来的呢?
![]()
这个顶层目录下的config.mk最终会include $(srctree)/$(CPUDIR)/config.mk
如果我们是在编译TI系列的uboot, CPUDIR会被设置为 arch/arm/cpu/armv7/am33xx, 在该目录下的config.mk里面有这样一段代码:

ALL-y += MLO, 表示ALL-y这个目标依赖MLO, 从而最终导致MLO会在编译SPL阶段被编译出来.
如果你回头仔细看看MLO这个目标, 会发现它其实就是把spl/u-boot-spl.bin这个二进制文件经过某种转换变成MLO这个二进制文件. 这种转变是针对TI系列特有的.
实际上, 也只有在编译TI系列的uboot时, 才会生成MLO这个文件.
u-boot.bin
分析完SPL是如何生成的之后, 我们再来分析一下u-boot.bin是如何生成的.
u-boot.bin的生成过程与SPL非常类似, 主要的不同之处在于编译SPL的过程中, 会在scripts/Makefile.spl中设置KBUILD_CPPFLAGS += -DCONFIG_SPL_BUILD; 而编译u-boot.bin的时候没有设置此标志. 这样在同一个C代码中, 用条件编译, 就可以保证某些代码被编译进SPL, 而另一些代码被编译进u-boot.bin
本小节我们也要弄清楚2个问题
1. 到底哪些代码会被编译进u-boot.bin
2. u-boot.bin的入口代码在哪里
接下来开始我们的探索吧.
从前面的分析可知, uboot顶层目录下的Makefile中, 有如下这段代码:
![]()
u-boot.bin这个目标就是用来生成文件u-boot.bin的.
在顶层Makefile中, u-boot.bin这个目标定义如下:

首先, 它依赖目标u-boot, 我们稍后看u-boot这个目标的定义
其次, $(call DO_STATIC_RELA,$<,$@,$(CONFIG_SYS_TEXT_BASE)) 这句话的意思是将u-boot.bin会被链接到CONFIG_SYS_TEXT_BASE这个地址, 链接到该地址的意思是u-boot.bin只有被搬移到CONFIG_SYS_TEXT_BASE这个地址才能正常运行. CONFIG_SYS_TEXT_BASE的值可以在.config中找到.
在顶层Makefile中, u-boot这个目标定义如下:

它依赖于3个重要的目标: $(u-boot-init) $(u-boot-main) u-boot.lds
其中前两者决定了哪些代码将被编译进u-boot.bin
最后一个lds是链接文件, 决定了入口代码
$(u-boot-init) $(u-boot-main) u-boot.lds这3个目标的定义都在顶层Makefile中.
余下的分析过程与SPL一模一样, 这里就不在赘述了. 我们直接说结论吧.
哪些代码会被编译进u-boot.bin
在顶层Makefile中, 有如下定义, 据此可知哪些代码会被编译进u-boot.bin
#########################################################################
# U-Boot objects....order is important (i.e. start must be first)
HAVE_VENDOR_COMMON_LIB = $(if $(wildcard $(srctree)/board/$(VENDOR)/common/Makefile),y,n)
libs-y += lib/
libs-$(HAVE_VENDOR_COMMON_LIB) += board/$(VENDOR)/common/
libs-$(CONFIG_OF_EMBED) += dts/
libs-y += fs/
libs-y += net/
libs-y += disk/
libs-y += drivers/
libs-y += drivers/dma/
libs-y += drivers/gpio/
libs-y += drivers/i2c/
libs-y += drivers/mmc/
libs-y += drivers/mtd/
libs-$(CONFIG_CMD_NAND) += drivers/mtd/nand/
libs-y += drivers/mtd/onenand/
libs-$(CONFIG_CMD_UBI) += drivers/mtd/ubi/
libs-y += drivers/mtd/spi/
libs-y += drivers/net/
libs-y += drivers/net/phy/
libs-y += drivers/pci/
libs-y += drivers/power/ \
drivers/power/fuel_gauge/ \
drivers/power/mfd/ \
drivers/power/pmic/ \
drivers/power/battery/
libs-y += drivers/spi/
libs-$(CONFIG_FMAN_ENET) += drivers/net/fm/
libs-$(CONFIG_SYS_FSL_DDR) += drivers/ddr/fsl/
libs-y += drivers/serial/
libs-y += drivers/usb/eth/
libs-y += drivers/usb/gadget/
libs-y += drivers/usb/host/
libs-y += drivers/usb/musb/
libs-y += drivers/usb/musb-new/
libs-y += drivers/usb/phy/
libs-y += drivers/usb/ulpi/
libs-y += common/
libs-$(CONFIG_API) += api/
libs-$(CONFIG_HAS_POST) += post/
libs-y += test/
libs-y += test/dm/
libs-y += $(if $(BOARDDIR),board/$(BOARDDIR)/)
libs-y := $(sort $(libs-y))
u-boot-dirs := $(patsubst %/,%,$(filter %/, $(libs-y))) tools examples
u-boot-alldirs := $(sort $(u-boot-dirs) $(patsubst %/,%,$(filter %/, $(libs-))))
libs-y := $(patsubst %/, %/built-in.o, $(libs-y))
u-boot-init := $(head-y)
u-boot-main := $(libs-y)
u-boot.bin的入口代码在哪里
我们也可知, u-boot.bin最先运行的代码就是arch/arm/lib/vectors.S中的_start函数.
4. u-boot启动流程
嵌入式系统启动的基本流程是这样的:
RomBoot --> SPL --> u-boot --> Linux kernel --> file system --> start application
RomBoot是固化在CPU内部的代码, 负责从各种外设(SDcard, eMMC, NANDFLASH, 等等)中加载SPL到CPU内部SRAM
SPL的主要目的是初始化板载的DDRAM, 然后将u-boot搬移到DDRAM
u-boot就可以做很多事情了, 最主要的目的就是加载Linux Kernel到DDRAM
SPL一般是地址无关的, 设计成地址无关的主要目的是为了保证SPL被搬移到任何地方都能运行. 这样设计的目的是我们不清楚RomBoot会把SPL搬移到哪个地址, 也许是SRAM的0地址, 也许是SRAM的某一个偏移地址.
u-boot一般是地址相关的, 在编译u-boot.bin的时候, 就决定了u-boot被链接到哪个地址, u-boot只能被搬移到这个地址才能正常运行.
为什么不把u-boot也设计成地址无关的呢? 让我们先了解一下地址无关与地址相关的概念, 在那里解答这个疑问.
4.1 地址无关和相关代码
位置无关代码, 即该段代码无论放在内存的哪个地址, 都能正确运行. 究其原因, 是因为代码里没有使用绝对地址, 都是相对地址.
而位置相关码, 即它的地址与代码处于的位置相关, 是绝对地址, 如:
mov PC , #0xff;
ldr pc, =0xffff
等等
位置无关的写法
B指令

B指令接受一个相对地址,因此在汇编里用B跳转到一个标号时,实际编译的结果是
一个相对跳转。相对地址有个范围限制,即目标不能太远,一般目标放在同一个文件里
是肯定可以的。 Offset must IN 32Mbit
除了B指令, 还有几个汇编指令也能用于相对跳转: BL, ADR.
百度文库里面有一篇文章还不错, 欲了解更多细节, 可以参考: 位置相关和无关码
综合来看, 位置无关代码要求使用的都是相对地址, 跳转距离有限制, 代码编写起来可能也不方便, 这也许是SPL被设计成地址无关的原因, 因为SPL够小.
4.2 SPL启动流程分析
从u-boot编译系统一节可知, SPL的入口代码是在arch/arm/lib/vectors.S中的_start函数.
下面我们就从它开始分析吧.
_start

直接跳转到reset处
ldr pc, _xxx定义的是中断的处理方式, 类似于中断向量表
对于中断的处理, SPL阶段和u-boot阶段是不一样的: SPL阶段不允许发生中断, u-boot阶段可以处理中断.
具体的细节也在vectors.S中, 代码如下:

reset
代码路径: arch/arm/cpu/$(CPU)/start.S
/*************************************************************************
*
* Startup Code (reset vector)
*
* Do important init only if we don't start from memory!
* Setup memory and board specific bits prior to relocation.
* Relocate armboot to ram. Setup stack.
*
*************************************************************************/
.globl reset
.globl save_boot_params_ret
reset:
/* Allow the board to save important registers */
b save_boot_params
save_boot_params_ret:
/*
* disable interrupts (FIQ and IRQ), also set the cpu to SVC32 mode,
* except if in HYP mode already
*/
mrs r0, cpsr
and r1, r0, #0x1f @ mask mode bits
teq r1, #0x1a @ test for HYP mode
bicne r0, r0, #0x1f @ clear all mode bits
orrne r0, r0, #0x13 @ set SVC mode
orr r0, r0, #0xc0 @ disable FIQ and IRQ
msr cpsr,r0
/*
* Setup vector:
* (OMAP4 spl TEXT_BASE is not 32 byte aligned.
* Continue to use ROM code vector only in OMAP4 spl)
*/
#if !(defined(CONFIG_OMAP44XX) && defined(CONFIG_SPL_BUILD))
/* Set V=0 in CP15 SCTLR register - for VBAR to point to vector */
mrc p15, 0, r0, c1, c0, 0 @ Read CP15 SCTLR Register
bic r0, #CR_V @ V = 0
mcr p15, 0, r0, c1, c0, 0 @ Write CP15 SCTLR Register
/* Set vector address in CP15 VBAR register */
ldr r0, =_start
mcr p15, 0, r0, c12, c0, 0 @Set VBAR
#endif
/* the mask ROM code should have PLL and others stable */
#ifndef CONFIG_SKIP_LOWLEVEL_INIT
bl cpu_init_cp15
bl cpu_init_crit
#endif
bl _main
bl save_boot_params: 如果没有重新定义save_boot_params,则使用<arch/arm/cpu/armv7/start.S>中的save_boot_params. 其不做任何事情,直接返回
有的厂商会重新定义save_boot_params, 主要目的就是把RomCode启动完毕之后的一些状态保存下来, 比如RomCode知道是从哪个外设(sdcard/eMMC/…)启动的系统
禁止FIQ, IRQ; 设置CPU工作在SVC32模式
如果没有定义CONFIG_SKIP_LOWLEVEL_INIT, 则执行如下两个操作:
对于有的CPU, RomCode已经做了足够的初始化操作, 这种情况下, 就可以在代码中定义SKIP_LOWLEVEL_INIT, 跳过下面这些操作.
bl cpu_init_cp15: Setup CP15 registers (cache, MMU, TLBs), 具体见head.S中此函数的代码中注释
bl cpu_init_crit: 该函数会直接调用lowlevel_init, 主要目的是设置CPU的PLL, GPIO管脚复用, memory等. 具体见下面
bl _main : 跳转到<arch/arm/lib/crt0.S>中的_main. 具体见下面
lowlevel_init
代码路径: arch/arm/cpu/$(CPU)/ lowlevel_init.S
ENTRY(lowlevel_init)
/*
* Setup a temporary stack. Global data is not available yet.
*/
ldr sp, =CONFIG_SYS_INIT_SP_ADDR
bic sp, sp, #7 /* 8-byte alignment for ABI compliance */
#ifdef CONFIG_DM
mov r9, #0
#else
/*
* Set up global data for boards that still need it. This will be
* removed soon.
*/
#ifdef CONFIG_SPL_BUILD
ldr r9, =gdata
#else
sub sp, sp, #GD_SIZE
bic sp, sp, #7
mov r9, sp
#endif
#endif
/*
* Save the old lr(passed in ip) and the current lr to stack
*/
push {ip, lr}
/*
* Call the very early init function. This should do only the
* absolute bare minimum to get started. It should not:
*
* - set up DRAM
* - use global_data
* - clear BSS
* - try to start a console
*
* For boards with SPL this should be empty since SPL can do all of
* this init in the SPL board_init_f() function which is called
* immediately after this.
*/
bl s_init
pop {ip, pc}
ENDPROC(lowlevel_init)
以前老版本的uboot, lowlevel_init一般都是在board/xxx下面的板级文件夹下面实现的. 现在直接放到CPUDIR下面了, 那它做了什么事情呢
将stack pointer赋值成CONFIG_SYS_INIT_SP_ADDR
确保sp是8字节对齐
将gdata的地址存入到r9寄存器中
做上面这几件事情的主要目的就是为下面这个跳转做准备, 做完这些准备, 下面的跳转就可以调用C代码了.
跳转到 s_init: 这个s_init就需要芯片厂商或者我们自己在板级文件里面用C实现了. 它主要做的事情从代码注释中可以看出来.
另外, 从代码的注释中还可以看到, s_init里面做不做事情无所谓, 因为随后就会调用board_init_f, 在board_init_f中在做这些初始化的事情也可以.
为什么新版本的uboot, lowlevel_init会放在CPUDIR下?
我想着应该是uboot架构的优化, 在CPUDIR的lowlevel_init里面统一初始化好sp, 这样各厂商在实现s_init的时候就可以用C语言了. 而以前的老版本里面, s_init里面要做的事情都是用汇编做的
_main
代码路径: arch/arm/lib/crt0.S
代码比较长, 我们只截取出与SPL相关的代码:
ENTRY(_main)
/*
* Set up initial C runtime environment and call board_init_f(0).
*/
#if defined(CONFIG_SPL_BUILD) && defined(CONFIG_SPL_STACK)
ldr sp, =(CONFIG_SPL_STACK)
#else
ldr sp, =(CONFIG_SYS_INIT_SP_ADDR)
#endif
#if defined(CONFIG_CPU_V7M) /* v7M forbids using SP as BIC destination */
mov r3, sp
bic r3, r3, #7
mov sp, r3
#else
bic sp, sp, #7 /* 8-byte alignment for ABI compliance */
#endif
mov r2, sp
sub sp, sp, #GD_SIZE /* allocate one GD above SP */
#if defined(CONFIG_CPU_V7M) /* v7M forbids using SP as BIC destination */
mov r3, sp
bic r3, r3, #7
mov sp, r3
#else
bic sp, sp, #7 /* 8-byte alignment for ABI compliance */
#endif
mov r9, sp /* GD is above SP */
mov r1, sp
mov r0, #0
clr_gd:
cmp r1, r2 /* while not at end of GD */
#if defined(CONFIG_CPU_V7M)
itt lo
#endif
strlo r0, [r1] /* clear 32-bit GD word */
addlo r1, r1, #4 /* move to next */
blo clr_gd
#if defined(CONFIG_SYS_MALLOC_F_LEN)
sub sp, sp, #CONFIG_SYS_MALLOC_F_LEN
str sp, [r9, #GD_MALLOC_BASE]
#endif
/* mov r0, #0 not needed due to above code */
bl board_init_f
......
ENDPROC(_main)
_main里面的思路与lowlevel_init里面很像, 也是为调用C函数做前期准备, 这个C函数就是board_init_f.
来看看_main里面主要做了哪些事情:
重新对SP赋值, 确认sp是8字对齐
在栈顶保留一个global_data的大小, 这个global_data是uboot里面的一个全局数据, 很多地方都会用到. 俗称 gd_t
确认更新后的sp是8字对齐
r9指向global_data, 后面别的地方想用global_data时候, 可以直接从r9这个寄存器里面获取地址
clr_gd : 清零global_data这段存储区域
如果定义了CONFIG_SYS_MALLOC_F_LEN, 则表示Enable malloc() pool before relocation.
bl board_init_f: 跳转到board_init_f. 在编译SPL时, 分析scripts/Makefile.spl可以看出, 该函数的实现是在<arch/arm/lib/spl.c>
board_init_f
代码路径: arch/arm/lib/spl.c
/*
* In the context of SPL, board_init_f must ensure that any clocks/etc for
* DDR are enabled, ensure that the stack pointer is valid, clear the BSS
* and call board_init_f. We provide this version by default but mark it
* as __weak to allow for platforms to do this in their own way if needed.
*/
void __weak board_init_f(ulong dummy)
{
/* Clear the BSS. */
memset(__bss_start, 0, __bss_end - __bss_start);
#ifndef CONFIG_DM
/* TODO: Remove settings of the global data pointer here */
gd = &gdata;
#endif
board_init_r(NULL, 0);
}
__weak: 表明该函数可以被重新定义(有的厂商重新定义了该函数)
对BSS段进行清零操作
board_init_r : 在编译SPL时, 分析scripts/Makefile.spl可以看出, 该函数的实现是在<common/spl/spl.c>
board_init_r
代码路径: common/spl/spl.c
函数比较长, 我们分段分析:
DECLARE_GLOBAL_DATA_PTR;
……
void board_init_r(gd_t *dummy1, ulong dummy2)
{
u32 boot_device;
int ret;
debug(">>spl:board_init_r()\n");
#if defined(CONFIG_SYS_SPL_MALLOC_START)
mem_malloc_init(CONFIG_SYS_SPL_MALLOC_START,
CONFIG_SYS_SPL_MALLOC_SIZE);
gd->flags |= GD_FLG_FULL_MALLOC_INIT;
#elif defined(CONFIG_SYS_MALLOC_F_LEN)
gd->malloc_limit = CONFIG_SYS_MALLOC_F_LEN;
gd->malloc_ptr = 0;
#endif
gd就是指的global_data, gd的定义用的宏DECLARE_GLOBAL_DATA_PTR <arch/arm/include/asm/global_data.h>; 一个C代码中如果想使用gd, 直接在最前面加上这个宏即可.
我们来看看这个宏的细节
#define DECLARE_GLOBAL_DATA_PTR register volatile gd_t *gd asm ("r9")
还记得r9这个寄存器吗, 在上面初始化过了
如果定义了:CONFIG_SYS_SPL_MALLOC_START, 则进行memory的malloc池初始化; 或者定义了CONFIG_SYS_MALLOC_F_LEN, 则直接赋值. 以后调用malloc就在这个池子里面分配内存
if (IS_ENABLED(CONFIG_OF_CONTROL) &&
!IS_ENABLED(CONFIG_SPL_DISABLE_OF_CONTROL)) {
ret = fdtdec_setup();
if (ret) {
debug("fdtdec_setup() returned error %d\n", ret);
hang();
}
}
if (IS_ENABLED(CONFIG_SPL_DM)) {
ret = dm_init_and_scan(true);
if (ret) {
debug("dm_init_and_scan() returned error %d\n", ret);
hang();
}
}
与device tree机制相关, 这里不细看了
#ifndef CONFIG_PPC
/*
* timer_init() does not exist on PPC systems. The timer is initialized
* and enabled (decrementer) in interrupt_init() here.
*/
timer_init();
#endif
如果没有定义:CONFIG_PPC, 则进行timer的初始化. 例如samsung的CPU是在<arch/arm/cpu/armv7/s5p-common/timer.c>里面定义的
#ifdef CONFIG_SPL_BOARD_INIT
spl_board_init();
#endif
SPL阶段, 如果还需要做什么初始化动作, 可以放在这里. 具体的实现可以在BOARDDIR下面.
boot_device = spl_boot_device();
debug("boot device - %d\n", boot_device);
switch (boot_device) {
#ifdef CONFIG_SPL_RAM_DEVICE
case BOOT_DEVICE_RAM:
spl_ram_load_image();
break;
#endif
#ifdef CONFIG_SPL_MMC_SUPPORT
case BOOT_DEVICE_MMC1:
case BOOT_DEVICE_MMC2:
case BOOT_DEVICE_MMC2_2:
spl_mmc_load_image();
break;
#endif
#ifdef CONFIG_SPL_NAND_SUPPORT
case BOOT_DEVICE_NAND:
spl_nand_load_image();
break;
#endif
#ifdef CONFIG_SPL_ONENAND_SUPPORT
case BOOT_DEVICE_ONENAND:
spl_onenand_load_image();
break;
#endif
#ifdef CONFIG_SPL_NOR_SUPPORT
case BOOT_DEVICE_NOR:
spl_nor_load_image();
break;
#endif
#ifdef CONFIG_SPL_YMODEM_SUPPORT
case BOOT_DEVICE_UART:
spl_ymodem_load_image();
break;
#endif
#ifdef CONFIG_SPL_SPI_SUPPORT
case BOOT_DEVICE_SPI:
spl_spi_load_image();
break;
#endif
#ifdef CONFIG_SPL_ETH_SUPPORT
case BOOT_DEVICE_CPGMAC:
#ifdef CONFIG_SPL_ETH_DEVICE
spl_net_load_image(CONFIG_SPL_ETH_DEVICE);
#else
spl_net_load_image(NULL);
#endif
break;
#endif
#ifdef CONFIG_SPL_USBETH_SUPPORT
case BOOT_DEVICE_USBETH:
spl_net_load_image("usb_ether");
break;
#endif
#ifdef CONFIG_SPL_USB_SUPPORT
case BOOT_DEVICE_USB:
spl_usb_load_image();
break;
#endif
#ifdef CONFIG_SPL_SATA_SUPPORT
case BOOT_DEVICE_SATA:
spl_sata_load_image();
break;
#endif
#ifdef CONFIG_SPL_BOARD_LOAD_IMAGE
case BOOT_DEVICE_BOARD:
spl_board_load_image();
break;
#endif
default:
#if defined(CONFIG_SPL_SERIAL_SUPPORT) && defined(CONFIG_SPL_LIBCOMMON_SUPPORT)
puts("SPL: Unsupported Boot Device!\n");
#endif
hang();
}
必须实现spl_boot_device, 返回是从哪个外部设备启动的(NAND/SDCARD/NOR...). 可以厂商或者自己在BOARDDIR下面实现
将image从具体的外部设备中load到ram中. 这里暂时先不分析具体的load过程
switch (spl_image.os) {
case IH_OS_U_BOOT:
debug("Jumping to U-Boot\n");
break;
#ifdef CONFIG_SPL_OS_BOOT
case IH_OS_LINUX:
debug("Jumping to Linux\n");
spl_board_prepare_for_linux();
jump_to_image_linux((void *)CONFIG_SYS_SPL_ARGS_ADDR);
#endif
default:
debug("Unsupported OS image.. Jumping nevertheless..\n");
}
#if defined(CONFIG_SYS_MALLOC_F_LEN) && !defined(CONFIG_SYS_SPL_MALLOC_SIZE)
debug("SPL malloc() used %#lx bytes (%ld KB)\n", gd->malloc_ptr,
gd->malloc_ptr / 1024);
#endif
jump_to_image_no_args(&spl_image);
}
判断image的类型
如果是u-boot,则直接break, 去运行u-boot
如果是Linux,则启动Linux. 这里说明SPL也可以直接启动Linux Kernel
至此,SPL结束它的生命,控制权交于u-boot或Linux
4.3 u-boot启动流程分析
从u-boot编译系统一节可知, u-boot.bin的入口代码也是在arch/arm/lib/vectors.S中的_start函数.
下面我们就从它开始分析吧.
_start
代码路径: arch/arm/lib/vectors.S
与SPL中_start的执行流程基本一致, 不同的地方是, u-boot.bin阶段会负责处理异常中断.
这里就不具体分析了. 可以回过头看看SPL的_start一节.
_start最终会跳转到reset.
reset
代码路径: arch/arm/cpu/$(CPU)/start.S
与SPL中reset的执行流程一致. 这里就不具体分析了.
reset最终会跳转到_main
_main
代码路径: arch/arm/lib/crt0.S
先分析第一部分, 这一部分与SPL阶段一致, 重点在于最后调用的board_init_f这个函数.
/*
* entry point of crt0 sequence
*/
ENTRY(_main)
/*
* Set up initial C runtime environment and call board_init_f(0).
*/
#if defined(CONFIG_SPL_BUILD) && defined(CONFIG_SPL_STACK)
ldr sp, =(CONFIG_SPL_STACK)
#else
ldr sp, =(CONFIG_SYS_INIT_SP_ADDR)
#endif
…………
…………
/* mov r0, #0 not needed due to above code */
bl board_init_f
SPL阶段最终也会调用board_init_f, u-boot.bin阶段最终也会调用该函数, 有何不同?
不同之处非常大, 这两个阶段调用的函数名虽然一样, 但是实现文件不一样: SPL的board_init_f是在arch/arm/lib/spl.c中实现的; 而u-boot.bin阶段是在arch/arm/lib/board.c中实现的. 这个不同是由编译阶段决定的.
那么u-boot.bin阶段的board_init_f函数到底做了哪些事情呢? 下面一小节会详述.
然后分析第二部分, 这一部分是u-boot.bin阶段独有的:
#if ! defined(CONFIG_SPL_BUILD)
/*
* Set up intermediate environment (new sp and gd) and call
* relocate_code(addr_moni). Trick here is that we'll return
* 'here' but relocated.
*/
ldr sp, [r9, #GD_START_ADDR_SP] /* sp = gd->start_addr_sp */
#if defined(CONFIG_CPU_V7M) /* v7M forbids using SP as BIC destination */
mov r3, sp
bic r3, r3, #7
mov sp, r3
#else
bic sp, sp, #7 /* 8-byte alignment for ABI compliance */
#endif
ldr r9, [r9, #GD_BD] /* r9 = gd->bd */
sub r9, r9, #GD_SIZE /* new GD is below bd */
adr lr, here
ldr r0, [r9, #GD_RELOC_OFF] /* r0 = gd->reloc_off */
add lr, lr, r0
#if defined(CONFIG_CPU_V7M)
orr lr, #1 /* As required by Thumb-only */
#endif
ldr r0, [r9, #GD_RELOCADDR] /* r0 = gd->relocaddr */
b relocate_code
here:
/*
* now relocate vectors
*/
bl relocate_vectors
/* Set up final (full) environment */
bl c_runtime_cpu_setup /* we still call old routine here */
#endif
第二部分最主要的事情是做relocate code.
relocate code是什么意思?
u-boot.bin的链接地址是在编译阶段决定的, 假设这个链接地址是0x20000000; SPL在load u-boot.bin的时候, 需要把它load到这个地址; 然后Jump到这个地址运行.
注意u-boot.bin是地址相关的, 只有 link address, load address, run address这3者一致才可正常运行.当代码运行到b relocate_code这个位置时, 代表u-boot.bin已经被加载到0x20000000并正常运行至b relocate_code.
那么这里的relocate_code 到底什么意思呢? 它的意思是把u-boot.bin余下部分的code全部搬移到另外一个地址继续运行.
为什么要做relocate code?
要解答这个问题, 得看看board_init_f这个函数了. 在这个函数里面, 从DDRAM的最末尾位置开始, 预留了一些内存, 不允许u-boot的代码去touch它. 至于预留了哪些内存, 请查看board_init_f这个函数.
所以, 将u-boot.bin搬移到哪里, 也是由board_init_f决定的.
怎么实现relocate code?
具体的实现代码是在arch/arm/lib/relocate.S
具体的实现逻辑还有点复杂, 这里就不细看了, 网上有一篇介绍还不错, 只需要知道上面描述的前因后果即可
然后分析第三部分, 这一部分也是u-boot.bin阶段独有的:
......
......
#if ! defined(CONFIG_SPL_BUILD)
bl coloured_LED_init
bl red_led_on
#endif
/* call board_init_r(gd_t *id, ulong dest_addr) */
mov r0, r9 /* gd_t */
ldr r1, [r9, #GD_RELOCADDR] /* dest_addr */
/* call board_init_r */
ldr pc, =board_init_r /* this is auto-relocated! */
/* we should not return here. */
#endif
ENDPROC(_main)
ldr pc, =board_init_r: 调用board_init_r, 同样, 该函数实现的地方与SPL阶段的也不一样. 这里是在arch/arm/lib/board.c中实现的.
board_init_f
代码路径: arch/arm/lib/board.c
函数很长, 分段分析:
void board_init_f(ulong bootflag)
{
bd_t *bd;
init_fnc_t **init_fnc_ptr;
gd_t *id;
ulong addr, addr_sp;
#ifdef CONFIG_PRAM
ulong reg;
#endif
void *new_fdt = NULL;
size_t fdt_size = 0;
memset((void *)gd, 0, sizeof(gd_t));
gd的定义在board.c里面, 有一行: DECLARE_GLOBAL_DATA_PTR;
memset清零, 说明从这里开始, 重新对gd进行初始化.
gd->mon_len = (ulong)&__bss_end - (ulong)_start;
__bss_end是在链接脚本里面定义的
(ulong)_start代表获取_start所在的位置
整段代码的意思就是将u-boot.bin的 code, data & bss的大小存储在gd->mon_len中
后面的代码会用到mon_len
#ifdef CONFIG_OF_EMBED
/* Get a pointer to the FDT */
gd->fdt_blob = __dtb_dt_begin;
#elif defined CONFIG_OF_SEPARATE
/* FDT is at end of image */
gd->fdt_blob = &_end;
#endif
/* Allow the early environment to override the fdt address */
gd->fdt_blob = (void *)getenv_ulong("fdtcontroladdr", 16,
(uintptr_t)gd->fdt_blob);
Device tree机制相关, 暂不分析
for (init_fnc_ptr = init_sequence; *init_fnc_ptr; ++init_fnc_ptr) {
if ((*init_fnc_ptr)() != 0) {
hang ();
}
}
依次调用init_sequence里面的函数, 如果某个函数执行失败, 即返回值不为0, 就会hang
init_sequence也是在arch/arm/lib/board.c中定义的, 包括如下这些函数:
arch_cpu_init
一般厂商会实现, 根据实际需要初始化CPU相关的一些东西
如果没有其他实现, 就会用board.c中的weak实现.
mark_bootstage
Record the board_init_f() bootstage (after arch_cpu_init())
fdtdec_check_fdt : fdt相关, 暂不分析
board_early_init_f
一般在BOARDDIR下面实现, 初始化必要的硬件
timer_init
时钟初始化
board_postclk_init
get_clocks
env_init
以nandflash为例, 此时还没有对nandflash进行初始化, 所以这个函数的实现不会从nand中去读取实际保存的变量. 只是简单标记env_valid为1
env_common.c中的env_relocate() will do the real validation
init_baudrate
初始化gd->baudrate
gd->baudrate = getenv_ulong("baudrate", 10, CONFIG_BAUDRATE);
serial_init
uboot的serial子系统相关
console_init_f
stage 1 init of console
Called before relocation - use serial functions
print_cpuinfo
display cpu info (and speed)
checkboard
display board info
dram_init
初始化gd->ram_size
接下来的code会做内存分配, 需要用到这个值. 注意, 如果有多块DRAM, 起始地址不一样, 那这里的ram_size应该只是其中1块的大小. 因为CONFIG_SYS_SDRAM_BASE只可能被指定为其中一块DRAM的起始地址, 在访问DRAM的时候, 使用CONFIG_SYS_SDRAM_BASE + offset的方式访问的, 其中offset的最大值是ram_size.
比如BANK1: 0x20000000 64M; BANK2: 0x40000000 64M; 那这个ram_size应该是64M, 而不是128M. 否则就可能出现非法的内存访问错误.
#if defined(CONFIG_SYS_MEM_TOP_HIDE)
/*
* Subtract specified amount of memory to hide so that it won't
* get "touched" at all by U-Boot. By fixing up gd->ram_size
* the Linux kernel should now get passed the now "corrected"
* memory size and won't touch it either. This should work
* for arch/ppc and arch/powerpc. Only Linux board ports in
* arch/powerpc with bootwrapper support, that recalculate the
* memory size from the SDRAM controller setup will have to
* get fixed.
*/
gd->ram_size -= CONFIG_SYS_MEM_TOP_HIDE;
#endif
gd->ram_size在前面(dram_init中)已经被赋值为SDRAM的大小了, 比如一块SDRAM是256M, 则gd->ram_size就被赋值为256M
gd->ram_size -= CONFIG_SYS_MEM_TOP_HIDE 表示从SDRAM的顶端开始, 预留CONFIG_SYS_MEM_TOP_HIDE大小的空间
addr = CONFIG_SYS_SDRAM_BASE + get_effective_memsize();
get_effective_memsize()的返回值一般是gd->ram_size, 整句话的意思就是让addr指向SDRAM顶部的某个位置, 具体的位置是(顶部 - CONFIG_SYS_MEM_TOP_HIDE)
后续的代码都会从这个addr开始向下(就是从SDRAM顶部向起始位置的方向)预留其它空间
#ifdef CONFIG_LOGBUFFER
#ifndef CONFIG_ALT_LB_ADDR
/* reserve kernel log buffer */
addr -= (LOGBUFF_RESERVE);
debug("Reserving %dk for kernel logbuffer at %08lx\n", LOGBUFF_LEN,
addr);
#endif
#endif
根据条件编译开关, 预留LOGBUFF_RESERVE大小的空间
#ifdef CONFIG_PRAM
/*
* reserve protected RAM
*/
reg = getenv_ulong("pram", 10, CONFIG_PRAM);
addr -= (reg << 10); /* size is in kB */
debug("Reserving %ldk for protected RAM at %08lx\n", reg, addr);
#endif /* CONFIG_PRAM */
根据条件编译开关, 预留PRAM大小的空间, 注意PRAM可以通过环境变量”pram”获取, pram是以KB为单位的
#if !(defined(CONFIG_SYS_ICACHE_OFF) && defined(CONFIG_SYS_DCACHE_OFF))
/* reserve TLB table */
gd->arch.tlb_size = PGTABLE_SIZE;
addr -= gd->arch.tlb_size;
/* round down to next 64 kB limit */
addr &= ~(0x10000 - 1);
gd->arch.tlb_addr = addr;
debug("TLB table from %08lx to %08lx\n", addr, addr + gd->arch.tlb_size);
#endif
预留TLB大小的空间
#ifdef CONFIG_LCD
#ifdef CONFIG_FB_ADDR
gd->fb_base = CONFIG_FB_ADDR;
#else
/* reserve memory for LCD display (always full pages) */
addr = lcd_setmem(addr);
gd->fb_base = addr;
#endif /* CONFIG_FB_ADDR */
#endif /* CONFIG_LCD */
如果没有给fb_base指定专门的地址, 则预留一块空间给fb_base用
最终会初始化gd->fb_base
/*
* reserve memory for U-Boot code, data & bss
* round down to next 4 kB limit
*/
addr -= gd->mon_len;
addr &= ~(4096 - 1);
debug("Reserving %ldk for U-Boot at: %08lx\n", gd->mon_len >> 10, addr);
预留一段空间给U-Boot code, data, bss.
mon_len是在本函数最前面被初始化的.
注意addr的值在后续的代码中就没被更改过了, addr最终会被赋值给gd->relocaddr, 然后负责relocate uboot的代码会读取gd->relocaddr, 然后把u-boot搬移到该位置
#ifndef CONFIG_SPL_BUILD
/*
* reserve memory for malloc() arena
*/
addr_sp = addr - TOTAL_MALLOC_LEN;
debug("Reserving %dk for malloc() at: %08lx\n",
TOTAL_MALLOC_LEN >> 10, addr_sp);
/*
* (permanently) allocate a Board Info struct
* and a permanent copy of the "global" data
*/
addr_sp -= sizeof (bd_t);
bd = (bd_t *) addr_sp;
gd->bd = bd;
debug("Reserving %zu Bytes for Board Info at: %08lx\n",
sizeof (bd_t), addr_sp);
预留用于malloc的空间, 这里就是我们通常所说的堆栈中的堆空间
给board info 结构体预留一段空间, 并把这段空间赋值给gd->bd. 这里就相当于给gd->bd分配存储空间了
#ifdef CONFIG_MACH_TYPE
gd->bd->bi_arch_number = CONFIG_MACH_TYPE; /* board id for Linux */
#endif
给gd->bd->bi_arch_number赋值, 在没有使用dtb机制的时候, uboot和内核就是靠这个id来进行匹配的
addr_sp -= sizeof (gd_t);
id = (gd_t *) addr_sp;
debug("Reserving %zu Bytes for Global Data at: %08lx\n",
sizeof (gd_t), addr_sp);
给Global Data预留一段存储空间. 注意此时gd还是指向别的某个地址, 这里预留的目的是为了在本函数的后面, 把gd的内容全部memcopy到这个地址. copy之后, gd就该使用此处的新地址了.
#if defined(CONFIG_OF_SEPARATE) && defined(CONFIG_OF_CONTROL)
/*
* If the device tree is sitting immediate above our image then we
* must relocate it. If it is embedded in the data section, then it
* will be relocated with other data.
*/
if (gd->fdt_blob) {
fdt_size = ALIGN(fdt_totalsize(gd->fdt_blob) + 0x1000, 32);
addr_sp -= fdt_size;
new_fdt = (void *)addr_sp;
debug("Reserving %zu Bytes for FDT at: %08lx\n",
fdt_size, addr_sp);
}
#endif
根据情况决定是否要为fdt预留空间, 具体细节看注释
#ifndef CONFIG_ARM64
/* setup stackpointer for exeptions */
gd->irq_sp = addr_sp;
#ifdef CONFIG_USE_IRQ
addr_sp -= (CONFIG_STACKSIZE_IRQ+CONFIG_STACKSIZE_FIQ);
debug("Reserving %zu Bytes for IRQ stack at: %08lx\n",
CONFIG_STACKSIZE_IRQ+CONFIG_STACKSIZE_FIQ, addr_sp);
#endif
/* leave 3 words for abort-stack */
addr_sp -= 12;
/* 8-byte alignment for ABI compliance */
addr_sp &= ~0x07;
#else /* CONFIG_ARM64 */
/* 16-byte alignment for ABI compliance */
addr_sp &= ~0x0f;
#endif /* CONFIG_ARM64 */
为中断预留空间
#ifndef CONFIG_SPL_BUILD
.....
.....
#else
addr_sp += 128; /* leave 32 words for abort-stack */
gd->irq_sp = addr_sp;
#endif
这里的逻辑很奇怪, 按照之前的分析, 如果是定义了SPL_BUILD, 不会执行这个C代码中的board_init_f. 所以这#else部分理论上不会运行
/* Ram ist board specific, so move it to board code ... */
dram_init_banksize();
初始化板载的SDRAM硬件状态, 初始化gd->bd->bi_dram[n].start和gd->bd->bi_dram[n].size.
gd->bd->bi_dram[n]是一个数组, 每个元素代表一块SDRAM
如果有多块SDRAM, 则需要初始化多个bi_dram
display_dram_config(); /* and display it */
打印出有几块SDRAM, 每块SDRAM的大小等信息
gd->relocaddr = addr;
gd->start_addr_sp = addr_sp;
gd->reloc_off = addr - (ulong)&_start;
debug("relocation Offset is: %08lx\n", gd->reloc_off);
gd->relocaddr : 还记得我们在_main函数中介绍过, 当执行完board_init_f函数后, 紧接着就会relocate u-boot.
relocate到哪里是由board_init_f决定的, 这里就是决定relocate地址的. 负责relocate的代码会读取gd->relocaddr, 然后把u-boot重定向到这个地方.
gd->start_addr_sp指向堆和栈的起始位置
gd->reloc_off 偏移量, 这个偏移量有什么用呢?
让我们从头来想一想: 首先u-boot.bin被link到某个地址, 然后load到该地址, 并在该地址运行.
u-boot.bin运行了一段时间后, 会被relocate到addr处继续运行. 但是u-boot.bin里面有一些调用是基于绝对地址的, 当u-boot.bin被搬移到另外一个地址之后, 这些基于绝对地址的调用就不正确了. 怎么解决这个问题呢? 很简单, 把这些绝对地址加上gd->reloc_off这个偏移量, 然后再去调用, 就不会出问题了.
if (new_fdt) {
memcpy(new_fdt, gd->fdt_blob, fdt_size);
gd->fdt_blob = new_fdt;
}
如果重新分配了fdt的空间, 则把原fdt的内容memcpy到这个新的地址空间
memcpy(id, (void *)gd, sizeof(gd_t));
将gd_t的内容memcpy到新的地址. id指向新分配的Global Data所在的空间.
这里你是否有一个疑问? 前面我们介绍过, gd的地址是存储的r9这个寄存器里面的, u-boot的所有代码都会从r9这个寄存器获取gd的地址. 现在gd换了新地址, 是否也该更新r9寄存器里面的值呢?
确实需要更新, _main函数首先会调用board_init_f, 随后会在接下来的代码中更新r9, 如下:
#if ! defined(CONFIG_SPL_BUILD)
......
......
ldr r9, [r9, #GD_BD] /* r9 = gd->bd */
sub r9, r9, #GD_SIZE /* new GD is below bd */
......
......
#endif
board_init_r
代码路径: arch/arm/lib/board.c
_main调用完board_init_f, 然后会relocate u-boot, 最后会调用board_init_r.
让我们以board_init_r的函数注释开始本节:
/************************************************************************
*
* This is the next part if the initialization sequence: we are now
* running from RAM and have a "normal" C environment, i. e. global
* data can be written, BSS has been cleared, the stack size in not
* that critical any more, etc.
*
************************************************************************
*/
注意红色字体, 不过多解释了
void board_init_r(gd_t *id, ulong dest_addr)
{
ulong malloc_start;
#if !defined(CONFIG_SYS_NO_FLASH)
ulong flash_size;
#endif
gd->flags |= GD_FLG_RELOC; /* tell others: relocation done */
bootstage_mark_name(BOOTSTAGE_ID_START_UBOOT_R, "board_init_r");
tell others: relocation done
bootstage_mark_name: 标记当前状态为board_init_r
/* Enable caches */
enable_caches();
使能I-Cache & D-Cache
board_init(); /* Setup chipselects */
调用各厂商自己在板级代码里面实现的board_init.
该函数里面做一些很简单的初始化动作, 例如初始化GPIO的引脚状态, 初始化开门狗等等.
除了这些最简单的初始化, 此函数还有一个重要功能, 设置gd->bd->bi_boot_params = CONFIG_SYS_SDRAM_BASE + 0x100; 该地址是用于存储传递给内核的参数的. 内核会从此地址获取bootargs等参数.
/*
* TODO: printing of the clock inforamtion of the board is now
* implemented as part of bdinfo command. Currently only support for
* davinci SOC's is added. Remove this check once all the board
* implement this.
*/
#ifdef CONFIG_CLOCKS
set_cpu_clk_info(); /* Setup clock information */
#endif
注意看此段代码的注释. set_cpu_clk_info的主要作用是根据现有的clk的状态, 初始化gd里面一些变量的值. 也就是说这里不会在硬件层面去配置clk, 只是在软件层面保存一些信息
serial_initialize();
初始化调试串口
#ifdef CONFIG_LOGBUFFER
logbuff_init_ptrs();
#endif
初始化log buffer
#ifdef CONFIG_POST
post_output_backlog();
#endif
POST相关, 暂不分析, 细节看代码
/* The Malloc area is immediately below the monitor copy in DRAM */
malloc_start = dest_addr - TOTAL_MALLOC_LEN;
mem_malloc_init (malloc_start, TOTAL_MALLOC_LEN);
初始化mem alloc池
#ifdef CONFIG_ARCH_EARLY_INIT_R
arch_early_init_r();
#endif
调用arch_early_init_r, 该函数一般是Arch相关的代码中实现的
power_init_board();
一般在厂商的板级文件里面定义此函数, 根据实际需要做某些事情.
#if !defined(CONFIG_SYS_NO_FLASH)
puts("Flash: ");
flash_size = flash_init();
if (flash_size > 0) {
# ifdef CONFIG_SYS_FLASH_CHECKSUM
print_size(flash_size, "");
/*
* Compute and print flash CRC if flashchecksum is set to 'y'
*
* NOTE: Maybe we should add some WATCHDOG_RESET()? XXX
*/
if (getenv_yesno("flashchecksum") == 1) {
printf(" CRC: %08X", crc32(0,
(const unsigned char *) CONFIG_SYS_FLASH_BASE,
flash_size));
}
putc('\n');
# else /* !CONFIG_SYS_FLASH_CHECKSUM */
print_size(flash_size, "\n");
# endif /* CONFIG_SYS_FLASH_CHECKSUM */
} else {
puts(failed);
hang();
}
#endif
如果有flash的话, 比如SPI flash, nor flash, 则会调用flash子系统, 获取flash的大小并显示出来
#if defined(CONFIG_CMD_NAND)
puts("NAND: ");
nand_init(); /* go init the NAND */
#endif
如果有nand, 则初始化nand子系统
#if defined(CONFIG_CMD_ONENAND)
onenand_init();
#endif
如果有onenand, 则初始化onenand子系统
#ifdef CONFIG_GENERIC_MMC
puts("MMC: ");
mmc_initialize(gd->bd);
#endif
如果有MMC, 则初始化MMC子系统
#ifdef CONFIG_CMD_SCSI
puts("SCSI: ");
scsi_init();
#endif
如果有SCSI, 则初始化SCSI子系统
#ifdef CONFIG_HAS_DATAFLASH
AT91F_DataflashInit();
dataflash_print_info();
#endif
atmel的芯片有data flash这一说, 如果板载有dataflash, 则初始化dataflash
/* initialize environment */
if (should_load_env())
env_relocate();
else
set_default_env(NULL);
根据需要, 选择重新加载环境变量或者设置其为NULL
#if defined(CONFIG_CMD_PCI) || defined(CONFIG_PCI)
arm_pci_init();
#endif
如果有PCI, 则初始化pci子系统
stdio_init(); /* get the devices list going. */
stdio子系统相关, 暂不清楚stdio到底是干什么用的
jumptable_init();
调用include/_exports.h中定义的各种通用的操作函数: get_version, getc, malloc, udelay等等
#if defined(CONFIG_API)
/* Initialize API */
api_init();
#endif
初始化一些API, 这些API可能是对外的一些接口函数
console_init_r(); /* fully init console as a device */
控制台最终初始化
#ifdef CONFIG_DISPLAY_BOARDINFO_LATE
# ifdef CONFIG_OF_CONTROL
/* Put this here so it appears on the LCD, now it is ready */
display_fdt_model(gd->fdt_blob);
# else
checkboard();
# endif
#endif
打印板子相关信息, checkboard一般是厂商在板级文件中实现的
#if defined(CONFIG_ARCH_MISC_INIT)
/* miscellaneous arch dependent initialisations */
arch_misc_init();
#endif
看注释就清楚是什么意思了
#if defined(CONFIG_MISC_INIT_R)
/* miscellaneous platform dependent initialisations */
misc_init_r();
#endif
看注释就清楚是什么意思了
/* set up exceptions */
interrupt_init();
/* enable exceptions */
enable_interrupts();
初始化并开启中断
/* Initialize from environment */
load_addr = getenv_ulong("loadaddr", 16, load_addr);
获取装载地址, 这个地址指的是将kernel拷贝到SDRAM的哪个位置
#ifdef CONFIG_BOARD_LATE_INIT
board_late_init();
#endif
可以选择性的在BOARDDIR中定义board_late_init, 做一些最后的初始化动作
#ifdef CONFIG_BITBANGMII
bb_miiphy_init();
#endif
#if defined(CONFIG_CMD_NET)
puts("Net: ");
eth_initialize();
#if defined(CONFIG_RESET_PHY_R)
debug("Reset Ethernet PHY\n");
reset_phy();
#endif
#endif
ethernet子系统相关
在BOARDDIR中实现eth_initialize, 初始化网络控制器, 并向uboot ethernet子系统注册
在BOARDDIR中实现reset_phy, 复位phy
#ifdef CONFIG_POST
post_run(NULL, POST_RAM | post_bootmode_get(0));
#endif
post相关, 暂不分析.
#if defined(CONFIG_PRAM) || defined(CONFIG_LOGBUFFER)
/*
* Export available size of memory for Linux,
* taking into account the protected RAM at top of memory
*/
{
ulong pram = 0;
uchar memsz[32];
#ifdef CONFIG_PRAM
pram = getenv_ulong("pram", 10, CONFIG_PRAM);
#endif
#ifdef CONFIG_LOGBUFFER
#ifndef CONFIG_ALT_LB_ADDR
/* Also take the logbuffer into account (pram is in kB) */
pram += (LOGBUFF_LEN + LOGBUFF_OVERHEAD) / 1024;
#endif
#endif
sprintf((char *)memsz, "%ldk", (gd->ram_size / 1024) - pram);
setenv("mem", (char *)memsz);
}
#endif
注意最后一条语句: setenv("mem", (char *)memsz);
要理解它的意思, 得先了解一点背景知识. uboot会向内核传递参数bootargs, bootargs里面有一项内容是mem=xxx, 意思是告诉内核可用的memory的空间是多少. 例如mem=512M, 就是告诉内核可用的memory空间有512M.
整段代码段的意思是, 在gd->ram_size的基础上, 减掉protected RAM和logbuffer所占用的空间, 再将余下的值赋值给”mem”, 最终通过bootargs告诉内核可用空间是多少. 注意gd->ram_size = (SDRAM的实际大小 - CONFIG_SYS_MEM_TOP_HIDE), 也就是说gd->ram_size已经减掉了CONFIG_SYS_MEM_TOP_HIDE所占用的空间.
/* main_loop() can return to retry autoboot, if so just run it again. */
for (;;) {
main_loop();
}
最精彩的部分就在这里了, 一个死循环.
这个循环中的逻辑就是: uboot会等待bootdelay秒, 如果这段时间内没有任何输入, 则运行bootcmd所指定的命令, 启动内核; 如果用户在这段时间内按下了按键, 则停止自动加载内核, 等待用户输入命令
下面我们用专门的一节来看看main_loop内部的逻辑
main_loop
代码路径: common/main.c
/* We come here after U-Boot is initialised and ready to process commands */
void main_loop(void)
{
const char *s;
bootstage_mark_name(BOOTSTAGE_ID_MAIN_LOOP, "main_loop");
设置当前状态为”main_loop”
modem_init();
在该函数的实现代码中, 有一个条件编译开关: #ifdef CONFIG_MODEM_SUPPORT
这里是进行modem初始化, 一般都没有用到这个功能, 即条件开关没有打开
#ifdef CONFIG_VERSION_VARIABLE
setenv("ver", version_string); /* set version variable */
#endif /* CONFIG_VERSION_VARIABLE */
设置环境变量ver, 在uboot命令行下, 可以用printenv ver打印出该变量
cli_init();
基本上没有做什么实质性的事情
run_preboot_environment_command();
运行环境变量” preboot”所定义的命令
#if defined(CONFIG_UPDATE_TFTP)
update_tftp(0UL);
#endif /* CONFIG_UPDATE_TFTP */
如果定义了CONFIG_UPDATE_TFTP, 则通过tftp下载”filename”到内存的某个地址, 然后在将其烧录到flash中. 具体细节看其实现代码.
有时候我们可以通过uboot的tftp机制, 把zImage下载到内存, 然后直接跳转到这个地址运行内核. 这种时候, 我们一般不是依靠这里的代码实现的, 而是在环境变量中定义一个变量(例如tftpkernel), 然后在bootcmd里面先run tftpkernel.
如果看不懂看上面这句话, 先忽略掉吧, 不影响.
s = bootdelay_process();
bootdelay_process的实现函数是在common/autoboot.c, 它所做的事情简单总结就是:
从环境变量bootdelay获取需要等待的多长时间, 将其保存在stored_bootdelay这个全局变量中
如果使能了CONFIG_OF_CONTROL, 并且dtb里面也指定了bootdelay这个参数, 则优先从dtb里面获取
从环境变量bootcmd获取需要执行的命令, 并将其return (这里假设没有使能CONFIG_BOOTCOUNT_LIMIT)
s保存的就是从bootcmd里面获取到的命令
if (cli_process_fdt(&s))
cli_secure_boot_cmd(s);
也可以在dtb中定义bootcmd, 这种情况下就会override uboot环境变量中的bootcmd.
如果dtb里面将bootsecure设置为enable状态, 则会直接运行调用cli_secure_boot_cmd来运行bootcmd里面所指定的命令. 而且cli_secure_boot_cmd不会返回, u-boot将不再运行后面的代码.
bootsecure到底有什么用呢? 它的目的就是让uboot直接运行bootcmd来启动内核, 从而用户就没有机会通过命令行来控制uboot了, 以此达到secure的效果
autoboot_command(s);
这里面实现的逻辑就是等待stored_bootdelay, 如果用户没有” Hit any key to stop autoboot”, 则直接运行s所指向的命令, 也就是bootcmd里面定义的命令. 并且此函数不会返回
如果用户Hit了any key, 则不执行s所指向的命令, 并返回, 继续运行下面的代码
cli_loop();
如果运行到这里, 说明用户Hit了any key, 此时uboot会停下, 等待用户输入命令并运行用户输入的命令
至此, 整个uboot的启动流程就已经分析完毕了.
5. u-boot命令执行机制
前一章最后提到uboot可以等待用户输入命令, 然后响应用户命令. 下面简单分析一下命令的执行流程.
当输入一个命令之后, uboot首先得先找到这个命令的实现代码, 然后才能运行这个命令. 如果uboot没有实现这个命令, 那输入的就是无效命令. 所以本章主要要弄清楚uboot是如何找到对应的命令的, 以及在uboot中如何定义一个命令.
5.1 命令执行流程
在uboot中有如下几种方式来运行命令, 它们最终都会调用cli_simple_run_command
它们的实现代码都是在: common/cli.c
run_command_list->cli_simple_run_command_list->cli_simple_run_command
run_command_list, 从字面意思就知道, 它是用于运行多条命令. 多条命令之间用”;”分割.
例如定义命令bootcmd=cmd1; cmd2; cmd3;
则run_command_list(bootcmd, …)就可以依次运行bootcmd中指定的多条命令
run_command_repeatable->cli_simple_run_command
直接调用cli_simple_run_command, 当uboot等待用户输入命令时, 如果你输入了一个命令, 就是靠run_command_repeatable来解析执行的
do_run->run_command->cli_simple_run_command
do_run是run命令的实现函数, 当uboot等待用户输入命令时, 我们也可以在命令行通过run cmd1的方式来运行命令cmd1.
没有很深入的研究 输入run cmd1和直接输入cmd1有何不同, 看上去是一样的, 都能运行cmd1这个命令
cli_simple_run_command
上述的几种调用流程, 最终都会调用cli_simple_run_command这个函数, 我们分析一下该函数.
代码路径: common/cli_simple.c
我们只看这个函数中几个关键的地方
int cli_simple_run_command(const char *cmd, int flag)
{
......
......
/* find macros in this token and replace them */
cli_simple_process_macros(token, finaltoken);
/* Extract arguments */
argc = cli_simple_parse_line(finaltoken, argv);
if (argc == 0) {
rc = -1; /* no command at all */
continue;
}
if (cmd_process(flag, argc, argv, &repeatable, NULL))
rc = -1;
......
}
cli_simple_process_macros : 替换命令中的宏. 这里表明我们可以在命令中使用一些宏.
例如用一个宏 ALL_CMD=cmd2; cmd3; cmd4;代表一个命令集合, 然后编写命令 bootcmd=cmd1;ALL_CMD; 那么宏替换完毕之后, bootcmd=cmd1;cmd2;cmd3;cmd4
cli_simple_parse_line: 解析出每个命令的参数.
例如 setenv bootargs console=ttyS0; setenv是命令, 后面的是参数.
cmd_process : 运行这个命令
cmd_process
代码路径: common/command.c
enum command_ret_t cmd_process(int flag, int argc, char * const argv[],
int *repeatable, ulong *ticks)
{
enum command_ret_t rc = CMD_RET_SUCCESS;
cmd_tbl_t *cmdtp;
/* Look up command in command table */
cmdtp = find_cmd(argv[0]);
if (cmdtp == NULL) {
printf("Unknown command '%s' - try 'help'\n", argv[0]);
return 1;
}
find_cmd, 查找这个命令是否有实现.
这里的机制是: uboot里面所有的命令实现都会被链接到某个固定的section里面, 这里就是从这个section里面查找命令. 具体细节请看《定义一个命令》一节.
/* found - check max args */
if (argc > cmdtp->maxargs)
rc = CMD_RET_USAGE;
每一个命令在实现的时候, 都会定义自己能接受的最大参数个数(cmdtp->maxargs), 这里判断实际的参数个数是否正确
/* If OK so far, then do the command */
if (!rc) {
if (ticks)
*ticks = get_timer(0);
rc = cmd_call(cmdtp, flag, argc, argv);
if (ticks)
*ticks = get_timer(*ticks);
*repeatable &= cmdtp->repeatable;
}
每一个命令在实现的时候, 都会定义自己的调用接口(cmdtp->cmd).
cmd_call就是通过调用cmdtp->cmd来运行这个命令的
if (rc == CMD_RET_USAGE)
rc = cmd_usage(cmdtp);
每一个命令在实现的时候, 都会定义一个usage接口(cmdtp->help)
这里的意思是如果命令执行不成功, 则调用cmdtp->help, 把本命令的帮助信息打印出来
至此, 命令的执行流程就分析完毕了
5.2 定义一个命令
在uboot中, 该如何定义一个命令呢? 弄清楚这个问题后, 我们也能知道uboot已经定义了哪些命令.
命令的定义用的是一个宏: U_BOOT_CMD
每个命令用一个结构体来描述: cmd_tbl_t
cmd_tbl_t
代码路径: include/command.h
/*
* Monitor Command Table
*/
struct cmd_tbl_s {
char *name; /* Command Name */
int maxargs; /* maximum number of arguments */
int repeatable; /* autorepeat allowed? */
/* Implementation function */
int (*cmd)(struct cmd_tbl_s *, int, int, char * const []);
char *usage; /* Usage message (short) */
#ifdef CONFIG_SYS_LONGHELP
char *help; /* Help message (long) */
#endif
#ifdef CONFIG_AUTO_COMPLETE
/* do auto completion on the arguments */
int (*complete)(int argc, char * const argv[], char last_char, int maxv, char *cmdv[]);
#endif
};
typedef struct cmd_tbl_s cmd_tbl_t;
注释已经相当清晰了, 不解释.
U_BOOT_CMD
代码路径: include/command.h
#ifdef CONFIG_AUTO_COMPLETE
# define _CMD_COMPLETE(x) x,
#else
# define _CMD_COMPLETE(x)
#endif
#ifdef CONFIG_SYS_LONGHELP
# define _CMD_HELP(x) x,
#else
# define _CMD_HELP(x)
#endif
#define U_BOOT_CMD_MKENT_COMPLETE(_name, _maxargs, _rep, _cmd, \
_usage, _help, _comp) \
{ #_name, _maxargs, _rep, _cmd, _usage, \
_CMD_HELP(_help) _CMD_COMPLETE(_comp) }
#define U_BOOT_CMD_COMPLETE(_name, _maxargs, _rep, _cmd, _usage, _help, _comp) \
ll_entry_declare(cmd_tbl_t, _name, cmd) = \
U_BOOT_CMD_MKENT_COMPLETE(_name, _maxargs, _rep, _cmd, \
_usage, _help, _comp);
#define U_BOOT_CMD(_name, _maxargs, _rep, _cmd, _usage, _help) \
U_BOOT_CMD_COMPLETE(_name, _maxargs, _rep, _cmd, _usage, _help, NULL)
U_BOOT_CMD -> U_BOOT_CMD_COMPLETE, 在U_BOOT_CMD_COMPLETE这个宏的实现里面, 会调用ll_entry_declare, 这个函数会把命令链接到某个固定的section.
举个例子, 比如common/cmd_echo.c里面就实现了echo这个命令:
echo, CONFIG_SYS_MAXARGS, 1, do_echo,
"echo args to console",
"[args..]\n"
" - echo args to console; \\c suppresses newline"
);
如果想知道uboot里面到底实现了哪些命令, 在整个源码里面搜索一下U_BOOT_CMD就知道了.
5.3 官网介绍的命令
U-BOOT官网也介绍了一些命令及其使用方法: http://www.denx.de/wiki/DULG/Manual
5.9. U-Boot Command Line Interface
6. FAQs
6.1 uboot启动kernel的流程
《4.3 main_loop》里面说到uboot在等待n秒的延时后, 会自动执行bootcmd中定义的命令.
bootcmd里面一般会用bootm命令去装载并启动kernel.
bootm的实现代码是arch/arm/lib/bootm.c.
bootm在执行过程中会打印” Starting kernel ...” , 这是uboot启动kernel前打印的最后一句话.

浙公网安备 33010602011771号