Uboot详解

1.   文档结构介绍

首先简介uboot, 给出uboot的官网.

 

然后介绍uboot的编译系统, 让你能了解到SPLu-boot.bin是如何编译出来的, 哪些C代码会被编译进SPLu-boot.bin.

 

接着会介绍uboot的启动流程, 从第一行汇编代码开始, 梳理一遍代码的运行流程.

 

最后一章会介绍uboot里面命令的执行流程, 如何定义一个命令, 以及uboot当前已经支持的命令.

2.   u-boot 简介

U-boot全称UniversalBootLoader, 即通用bootloader.

它是德国DENX小组的开发用于多种嵌入式CPUbootloader程序, UBoot不仅仅支持嵌入式Linux系统的引导, 它还支持NetBSD,VxWorks,QNX,RTEMS,ARTOS,LynxOS嵌入式操作系统。UBoot除了支持PowerPC系列的处理器外,还能支持MIPSx86ARMNIOSXScale等诸多常用系列的处理器

 

官网:

         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-stage1image-stage22段代码.

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, 那么你将得到SPLu-boot.bin这两个image.

 

那么问题来了:

哪些代码将被编译进SPL, SPL的入口函数是谁?

哪些代码将被编译进u-boot.bin?

make xxx_defconfig又干了些什么事情呢?

 

接下来, 让我们深入进去看看u-boot的编译系统. 要了解它的编译系统, 你得先有点make & Makefile的基础知识.

3.2             make & Makefile

NOTE: 下面内容是对<跟我一起写Makefile-陈皓.pdf>一文的缩写, 主要列出了u-bootMakefile中用到的一些语法. 如果你想全面了解Makefile, 建议你在百度里面找到这篇文章.

make

一般我们在编译某个代码时, 会进入到代码所在目录, 然后敲一个make命令. 之后, make命令就会在当前目录下寻找Makefile或者makefile文件, 解析并执行该文件.

 

当然, make命令后面可以跟一些参数, 例如:

         make -C DIRECTORY -f FILE

进入到DIRECTORY目录, 找到FILE文件, 解析并执行该文件.

如果没有-f参数, FILE默认就是Makefilemakefile

         make target

Makefile中找到target目标, 并执行该目标. 关于target的概念, 我们随后就会介绍.

如果make命令后面没有显示指明target, 那么make会将Makefile中的第一个target做为默认目标.

对于非默认目标, 则只能通过make target这种显示指定目标的方式使其执行.

Makefile

target

target的基本语法格式如下:

targets : [ prerequisites ] [; command]

[command1]

[command2]

[……]

         [ ]里面的内容是可选的

         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. 假设这个targetMakefile中的第一个目标, 那么每次敲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.cmain.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命令

         buildscripts/Kbuild.include文件中被定义为build := -f $(srctree)/scripts/Makefile.build obj, 顶层Makefileincludescripts/Kbuild.include

         $@代表目标, 也就是xxx_defconfig.

整条语句相当于

make -f scripts/Makefile.build obj=scripts/kconfig xxx_defconfig

 

scripts/Makefile.build中并没有定义xxx_defconfig这个目标, 不过它includescripts/kconfig/Makefile. 代码如下, 逻辑就细说了

 

scripts/kconfig/Makefile中定义了xxx_defconfig这个目标, 如下:

         obj在之前被设置过了, scripts/kconfig

         %_defconfig这个目标依赖$(obj)/conf, 也就是依赖scripts/kconfig/conf.

但是你在scripts/kconfig/Makefile中找不到目标$(obj)/conf的定义. 不过没关系, Makefile可以自动推导(参见 跟我一起写Makefile-陈皓.pdf).

自动推导的结果就是:

$(obj)/conf : conf.c

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下新增一块板的支持, 只需要如下步骤:

    1. configs/下创建一个配置文件xxx_defconfig. 在配置文件里面选中某个TARGET, : CONFIG_TARGET_MYOWNTARGET=y
  1. 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

    1. include/configs/目录下创建一个名为MYCONFIGNAME.h的文件. 在这个文件中存放特定的一些配置.
  1. board/MYVENDOR/MYBOARD/目录下创建板级文件myboardfile.cMakefile.

如果你在板级文件中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

 

了解了上述逻辑之后, 接下来我们分别分析SPLu-boot.bin是如何生成的.

SPL

先分析一下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的定义如下:

它依赖toolsprepare这两个目标, 这两个目标是做一些准备工作, 细看了.

重点在于下面的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_BUILDCONFIG_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有没有定义链接脚本的路径, 如果没有, 依次去BOARDDIRCPUDIR$(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又是如何生成出来的呢?

 

来看看scripts/Makefile.spl中的这一行:

这个顶层目录下的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.lds3个目标的定义都在顶层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, 等等)中加载SPLCPU内部SRAM

         SPL的主要目的是初始化板载的DDRAM, 然后将u-boot搬移到DDRAM

         u-boot就可以做很多事情了, 最主要的目的就是加载Linux KernelDDRAM

 

SPL一般是地址无关的, 设计成地址无关的主要目的是为了保证SPL被搬移到任何地方都能运行. 这样设计的目的是我们不清楚RomBoot会把SPL搬移到哪个地址, 也许是SRAM0地址, 也许是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

代码路径: arch/arm/lib/vectors.S

         直接跳转到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, 主要目的是设置CPUPLL, 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

         确保sp8字节对齐

         gdata的地址存入到r9寄存器中

做上面这几件事情的主要目的就是为下面这个跳转做准备, 做完这些准备, 下面的跳转就可以调用C代码了.

         跳转到 s_init: 这个s_init就需要芯片厂商或者我们自己在板级文件里面C实现了. 它主要做的事情从代码注释中可以看出来.

另外, 从代码的注释中可以看到, s_init里面做不做事情无所谓, 因为随后就会调用board_init_f, board_init_f中在做这些初始化的事情也可以.

 

为什么新版本的uboot, lowlevel_init会放CPUDIR?

我想着应该是uboot架构的优化, CPUDIRlowlevel_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赋值, 确认sp8字对齐

         顶保留一个global_data的大小, 这个global_datauboot里面的一个全局数据, 很多地方都会用到. 俗称 gd_t

         确认更新后的sp8字对齐

         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, 则进行memorymalloc池初始化; 或者定义了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的初始化. 例如samsungCPU是在<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从具体的外部设备中loadram. 这里暂时先不分析具体的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-bootLinux

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

 

SPLreset的执行流程一致. 这里就不具体分析了.

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阶段最终也会调用该函数, 有何不同?

不同之处非常大, 这两个阶段调用的函数名虽然一样, 但是实现文件不一样: SPLboard_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; SPLload u-boot.bin的时候, 需要把它load到这个地址; 然后Jump到这个地址运行.

注意u-boot.bin是地址相关的, 只有 link address, load address, run address3者一致才可正常运行.当代码运行到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_valid1

env_common.c中的env_relocate() will do the real validation

          init_baudrate

初始化gd->baudrate

gd->baudrate = getenv_ulong("baudrate", 10, CONFIG_BAUDRATE);

          serial_init

ubootserial子系统相关

          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的大小了, 比如一块SDRAM256M, 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].startgd->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.binlink到某个地址, 然后load到该地址, 并在该地址运行.

u-boot.bin运行了一段时间后, 会被relocateaddr处继续运行. 但是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 RAMlogbuffer所占用的空间, 再将余下的值赋值给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. 具体细节看其实现代码.

         有时候我们可以通过uboottftp机制, 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里面定义的命令. 并且此函数不会返回

         如果用户Hitany key, 则不执行s所指向的命令, 并返回, 继续运行下面的代码

 

    cli_loop();

         如果运行到这里, 说明用户Hitany 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_runrun命令的实现函数, 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这个命令:

U_BOOT_CMD(

    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前打印的最后一句话.

posted @ 2020-12-13 17:29  johnliuxin  阅读(5516)  评论(21)    收藏  举报