U-Boot移植教程之一:U-Boot分析与启动过程

内容来自 韦东山《嵌入式Linux应用开发完全手册》

 

一、Bootloader的引出

        当系统上电时,并不是直接进入Linux系统的,而是需要先执行一段程序来把单片机的硬件外围初始化好,比如:看门狗、单片机时钟、存储控制器等。如果这段程序能将操作系统内核复制到内存中运行,无论是从本地(比如Flash)还是从远端(比如网络)就称这段程序位Bootloader。

        简单地说,Bootloader就是上电时就开始执行,初始化硬件设备为操作系统准备好执行环境,最后调用操作系统内核。

        Bootloader还可以具有通过串口、网络、USB等通信路径烧写操作系统内核的功能。

二、Bootloader的结构和启动过程

        一般嵌入式系统的存储结构如下:

U-Boot移植教程之一:U-Boot分析与启动过程_第1张图片

        最前面的是Bootloader,上电后首先执行它。

        第二段是参数,是内核执行需要的参数或Bootloader传递给内核的数据。Bootloader把数据保存到这个区域,然后进入内核后,内核去这个区域读,这样就完成了数据从Bootloader到内核的传递。

        第三段是内核映像,单片机由Bootloader进入这里,进入后就开始调度各种任务,执行应用程序,开始进入正常运行状态。

        第四段是文件系统,其中保存着内核运行需要的应用程序、库等数据。

三、Bootloader的两个阶段

        Bootloader的执行过程又分为两个阶段,第一阶段用汇编来实现,完成一些简单的功能,为第二阶段做准备。第二阶段用C语言来实现,这样可以实现一些复杂一点的功能,而且代码具有更好的可读性和移植性。而具体第一阶段和第二阶段各自执行那些功能,可以由开发者自己分配。

四、U-Boot的源码结构

        U-Boot是Bootloader的一种,且是开源的,其源码可以在这里下载到:ftp://ftp.denx.de/pub/u-boot/。现在已经更新到很新的版本了。但是本文依然用U-B00t-1.1.6来介绍,因为此版本资料最多,更新版本的U-Boot只是增加了更多的开发板和CPU支持而已,因此选择的时候不用太纠结于U-Boot的版本,能选择到支持自己开发板或CPU的U-Boot最好,没有就只能自己移植了。

        下载后,打开文件夹,目录结构如下:

U-Boot移植教程之一:U-Boot分析与启动过程_第2张图片

        一般移植U-BOOT会修改绿色部分的代码,这些文件夹也有4个层次:

U-Boot移植教程之一:U-Boot分析与启动过程_第3张图片

       最上层是Lib和common,其中是一些库函数、各种命令。调用关系是从上往下的。比如common里的cmd_nand.c文件中提供了操作NAND Flash的各种命令,当执行这些命令,命令会调用drivers文件夹里的nand/nand_base.c中的擦除、读写函数来实现。nand/nand_base.c中的函数只是针对NAND Flash的共性做了封装,与硬件平台相关的代码都用宏或外部函数来代替,这样可以保证所有的CPU,nand/nand_base.c中的代码都是相同的。nand/nand_base.c中调用宏或外部函数又在cpu或board文件夹中实现。

五、U-Boot的使用方法

        从网上下载了一个版本的U-Boot后,我们去board文件夹中找我们的开发板名字,board文件夹中的的每一个子文件夹都是一种开发板,如果在其中找到了,那就不用移植了,直接使用即可。

U-Boot移植教程之一:U-Boot分析与启动过程_第4张图片

        当我们在board文件夹中找到自己的开发板后,把名字记下来,比如“smdk2410”。然后在根目录下执行

make _config

命令,其中的“board_name”即为刚才记下的名字,因此这里是执行:

make smdk2410_config

然后执行

make all

即可完成U-Boot的编译,编译后会生成3个文件:

U-Boot.bin:二进制可执行文件,它就是可以直接烧入ROM、NORFlash的文件。
U-Boot:ELF格式的可执行文件。
U-Boot.srec:Motorola S-Record格式的可执行文件。

        我们把上面生成的U-Boot.bin烧录到CPU的Flash中,让CPU上电执行它即可,这样关于U-Boot的工作就完成了。

        如果没有现成的,那就只能自己移植了,那我们就来开始分析U-Boot吧。

 

六、make smdk2410_config命令的解析

        上面我们执行了两条命令:

make smdk2410_config
make all

        执行这两条命令后,make工具会去根目录下的Makefile中查询到底要做哪些事情。因此我们打开Makefile,然后搜索“smdk2410_config”,看一下这条命令的内容是什么(可以先大概看下Makefile的语法:链接):

smdk2410_config	:	unconfig
	@$(MKCONFIG) $(@:_config=) arm arm920t smdk2410 NULL s3c24x0

        命令的名字(目标)是:smdk2410_config,依赖是:unconfig,命令是:@$(MKCONFIG) $(@:_config=) arm arm920t smdk2410 NULL s3c24x0。根据Makefile的语法,我们先来看看依赖是不是文件,搜索unconfig,发现:

unconfig:
	@rm -f $(obj)include/config.h $(obj)include/config.mk \
		$(obj)board/*/config.tmp $(obj)board/*/*/config.tmp

        说明unconfig是命令,它的作用是删除以前编译过程中产生的文件。再来看命令内容,“@$(MKCONFIG)”最前面的”@“的作用是不输出命令内容到命令行中,”$(MKCONFIG)”是引用参数值,搜索“MKCONFIG”:

MKCONFIG	:= $(SRCTREE)/mkconfig

        搜索“SRCTREE”

SRCTREE		:= $(CURDIR)

        CURDIR是make的内嵌变量,其值为当前目录,也就是U-Boot的根目录。所以“@$(MKCONFIG)”就等于“mkconfig”。 ”$(@:_config=)“中“$(@)”的作用与“$@”相同,所以可以变为:“$@:_config=”。“$@”等于目标文件的完整名称。

所以“$@:_config=”即为“smdk2410_config:_config=”,意思是让“smdk2410_config”中的“_config”等于“”,那就变为了“smdk2410”。

        因此

	@$(MKCONFIG) $(@:_config=) arm arm920t smdk2410 NULL s3c24x0

        就变为了:

	mkconfig smdk2410 arm arm920t smdk2410 NULL s3c24x0

        因此我们执行“make smdk2410_config”,就相当于在命令行中执行“mkconfig smdk2410 arm arm920t smdk2410 NULL s3c24x0”命令。

       mkconfig是shell脚本文件,后面的smdk2410 arm arm920t smdk2410 NULL s3c24x0都是传入其中的参数。打开mkconfig,看到第六行:

# Parameters:  Target  Architecture  CPU  Board [VENDOR] [SOC]

        说明了mkconfig文件的传入参数的意义。即:

smdk2410 (目标) arm(架构) arm920t(cpu) smdk2410 (开发板选型) NULL(供应商) s3c24x0(片上系统/芯片)

        下面分成几个部分来说明mkconfig文件中代码的作用。

        1、确定开发板的名称,即确定BOARD_NAME变量的值

U-Boot移植教程之一:U-Boot分析与启动过程_第5张图片

       11、12行是定义两个变量。

        14到21行是while循环,循环条件是“$# -gt 0”,“$#”是传入的参数个数,“-gt”是大于的意思,因此只要传入参数的个数大于0,while循环就会执行。

        15到20行是case语句,与c语言switch类似。“$1”是指传入的第一个参数。shift是作用是把传入的参数左移1次,比如传入了4个参数,执行shift后,传入参数就剩下了3个,第一个参数被丢弃了,同时原来的第二个参数变成了第一个参数。

        16行的意思就是判断第一个参数是否等于“--”,如果等于就执行shift。

        17行的意思是判断第一个参数是否等于“-a”,如果等于就执行shift,并且把“APPEND”的值改为“yes”。

        23行,“||”是逻辑“或”,表示在它前面的语句执行失败了,才执行它后面的语句。现在BOARD_NAME仍然为空,因此会执行“||”后面的语句,即BOARD_NAME=smdk2410。

         2、创建到平台/开发板相关的头文件的链接。

U-Boot移植教程之一:U-Boot分析与启动过程_第6张图片

        33行判断U-Boot的源代码目录和我们编译的目标文件目录是否相同。编译的目录可以放在其他地方,这样可以使源代码目录保持干净,他们的值在Makefile中修改。一般默认他们是相同的,所以执行else分支。

        46到48行,先进入根目录下的include文件夹,然后删除其中名字为asm的文件,然后再次建立asm文件,并令它链接向asm-$2目录,即asm-arm。

U-Boot移植教程之一:U-Boot分析与启动过程_第7张图片

        51行删除asm-$2/arch目录,即asm-arm/arch。

        53行,“-z”判断字符串是否为空。“-o”是逻辑或。因为$6为s3c24x0,既不为空,也不为NULL,于是执行else分支。

        56行中,LNPREFIX为空,所以命令实际为“ln -s arch-s3c24x0 asm-arm/arch”,即令asm-arm/arch链接向arch-s3c24x0

        第60、61行重新建立asm-arm/proc文件,并让它链接向proc-armv目录。

        3、创建顶层Makefile包含的文件include/config.mk

U-Boot移植教程之一:U-Boot分析与启动过程_第8张图片

对于“./mkconfig smdk2410 arm arm920t smdk2410 NULL s3c24x0”命令,上面几行代码创建的config.mk文件内容如下:

ARCH = arm
CPU = arm920t
BOARD = smdk2410
SOC = s3c24x0

        4、创建开发板相关的头文件include/config.h

U-Boot移植教程之一:U-Boot分析与启动过程_第9张图片

APPEND维持原值“no”,所以config.h被重新建立,它的内容如下:

/* Automatically generated - do not edit */
#include "

 

        现在总结一下,配置命令“make smdk2410_config”,实际的作用就是执行“./mkconfig smdk2410 arm arm920t smdk2410 NULL s3c24x0”命令。假设执行“./mkconfig $1 $2 $3 $4 $5 $6”命令,则将产生如下结果:
(1)开发板名称BOARD_NAME等于$1;
(2)创建到平台/开发板相关的头文件的链接:

ln -s asm-$2 asm
ln -s arch-$6 asm-$2/arch
ln -s proc-armv asm-$2/proc# 如果$2不是arm的话,此行没有

(3) 创建顶层Makefile包含的文件include/config.mk。

ARCH = $2
CPU = $3
BOARD = $4
VENDOR = $5# $5为空,或者是NULL的话,此行没有
SOC = $6# $6为空,或者是NULL的话,此行没有

(4)创建开发板相关的头文件include/config.h。

/* Automatically generated - do not edit */
#include 

 

        从上面知道了,如果我们要编译smdk2410开发板的U-Boot,先要执行“make smdk2410_config”,执行这个命令相当于进行了4个步骤,前面3个步骤都是建立软链接和新建新的文件,不牵扯到源代码中已有的文件。只有第4个步骤,#include ,包含了include/configs中已有开发板的头文件。

U-Boot移植教程之一:U-Boot分析与启动过程_第10张图片

        在这个文件中对对应开发板的U-Boot进行裁剪和配置,比如这个smdk240开发板不需要某个功能,就在include/configs/smdk2410.h文件中把该功能的宏开关关闭。我们打开smdk2410.h发现其中有两类宏:

(1)一类是选项(Options),前缀为"CONFIG_",它们用于选择CPU、SOC、开发板类型,设置系统时钟、选择设备驱动等。比如:

#define CONFIG_ARM920T        1/* This is an ARM920T Core*/
#define CONFIG_S3C2410        1/* in a SAMSUNG S3C2410 SoC */
#define CONFIG_SMDK2410       1/* on a SAMSUNG SMDK2410 Board */
#define CONFIG_SYS_CLK_FREQ   12000000/* the SMDK2410 has 12MHz input clock */
#define CONFIG_DRIVER_CS8900  1/* we have a CS8900 on-board */

(2)另一类是参数(Setting),前缀为“CFG_”,它们用于设置malloc缓冲池的大小、U-Boot的提示符、U-Boot下载文件时的默认加载地址、Flash的起始地址等。比如:

#define CFG_MALLOC_LEN (CFG_ENV_SIZE + 128*1024)
#define CFG_PROMPT     "100ASK> "/* Monitor Command Prompt*/
#define CFG_LOAD_ADDR  0x33000000/* default load address*/
#define PHYS_FLASH_1   0x00000000 /* Flash Bank #1 */

        从下面的编译、连接过程可知,U-Boot中几乎每个文件都被编译和连接,但是这些文件是否包含有效的代码,则由宏开关来设置。比如对于网卡驱动drivers/cs8900.c,它的格式为:

#include /* 将包含配置文件include/config/.h */
……
#ifdef CONFIG_DRIVER_CS8900
/* 实际的代码 */
……
#endif/* CONFIG_DRIVER_CS8900 */

        如果定义了宏CONFIG_DRIVER_CS8900,则文件中包含有效的代码;否则,文件被注释为空。

        可以这样粗糙地认为,“CONFIG_”除了设置一些参数外,主要用来设置U-Boot的功能、选择使用文件中的哪一部分;而“CFG_”用来设置更细节的参数。

七、make all命令的解析

        执行make smdk2410_config只是完成对开发板的配置,相当于做了一些编译前的准备工作,接着执行make all才是真正的开始编译。

        分析make all之前,先看下根目录Makefile中包含进了哪些文件:

117 include $(OBJTREE)/include/config.mk
118 export ARCH CPU BOARD VENDOR SOC
119
……
127 ifeq ($(ARCH),arm)
128 CROSS_COMPILE = arm-linux-
129 endif
……
163 # load other configuration
164 include $(TOPDIR)/config.mk
165

        第117、164行用于包含其他的config.mk文件,第117行所要包含文件的就是在上面的配置过程中制作出来的 include/config.mk文件,其中定义了ARCH、CPU、BOARD、SOC等4个变量的值为arm、arm920t、smdk2410、 s3c24x0。

        第164行包含顶层目录的config.mk文件,它根据上面4个变量的值确定了编译器、编译选项等。打开顶层目录的config.mk文件,其中对我们理解编译过程有帮助的是BOARDDIR、LDFLAGS的值,config.mk中:

88 BOARDDIR = $(BOARD)
……
91 sinclude $(TOPDIR)/board/$(BOARDDIR)/config.mk# include board specific rules
……
143 LDSCRIPT := $(TOPDIR)/board/$(BOARDDIR)/u-boot.lds
……
189 LDFLAGS += -Bstatic -T $(LDSCRIPT) -Ttext $(TEXT_BASE) $(PLATFORM_LDFLAGS)

        所以BOARDDIR值为smdk2410。

        在board/smdk2410/config.mk中,定义了“TEXT_BASE = 0x33F80000”。所以LDFLAGS的值包含有:

“-T board/smdk2410/u-boot.lds -Ttext 0x33F80000”。

        继续往下看Makefile:

166
##################################################################### ####
167 # U-Boot objects....order is important (i.e. start must be first)
168
169 OBJS = cpu/$(CPU)/start.o
……
193 LIBS = lib_generic/libgeneric.a
194 LIBS += board/$(BOARDDIR)/lib$(BOARD).a
195 LIBS += cpu/$(CPU)/lib$(CPU).a
……
199 LIBS += lib_$(ARCH)/lib$(ARCH).a
200 LIBS += fs/cramfs/libcramfs.a fs/fat/libfat.a fs/fdos/libfdos.a fs/jffs2/libjffs2.a \
201 fs/reiserfs/libreiserfs.a fs/ext2/libext2fs.a
202 LIBS += net/libnet.a
……
212 LIBS += $(BOARDLIBS)
213

        从第169行得知,OBJS的值为“cpu/$(CPU)/start.o”,即“cpu/arm920t/start.o”。

        第193~213行指定了LIBS变量就是平台/开发板相关的各个目录、通用目录下相应的库,比如:lib_generic /libgeneric.a、board/smdk2410/libsmdk2410.a、cpu/arm920t/libarm920t.a、 lib_arm/libarm.a、fs/cramfs/libcramfs.a fs/fat/libfat.a等。

        OBJS、LIBS所代表的.o、.a文件就是U-Boot的构成文件,它们通过如下命令由相应的源文件(或相应子目录下的文件)编译得到。

268 $(OBJS):
269 $(MAKE) -C cpu/$(CPU) $(if $(REMOTE_BUILD),$@,$(notdir $@))
270
271 $(LIBS):
272 $(MAKE) -C $(dir $(subst $(obj),,$@))
273
274 $(SUBDIRS):
275 $(MAKE) -C $@ all
276

        第268、269两行的规则表示,对于OBJS中的每个成员,都将进入cpu/$(CPU)目录(即cpu/arm920t)编译它们。现在OBJS为cpu/arm920t/start.o,它将由cpu/arm920t/start.S编译得到。

        第271、272两行的规则表示,对于LIBS中的每个成员,都将进入相应的子目录执行“make”命令。这些子目录中Makefile结构相似,它们将Makefle中指定的文件编译、连接成一个库文件。

        当所有的OBJS、LIBS所表示的.o和.a文件都生成后,就剩最后的连接了,这对应Makefile中如下几行:

246 $(obj)u-boot.srec:$(obj)u-boot
247 $(OBJCOPY) ${OBJCFLAGS} -O srec $< $@
249 $(obj)u-boot.bin:$(obj)u-boot
250 $(OBJCOPY) ${OBJCFLAGS} -O binary $< $@
251
……
262 $(obj)u-boot:depend version $(SUBDIRS) $(OBJS) $(LIBS) $(LDSCRIPT)
263 UNDEF_SYM=`$(OBJDUMP) -x $(LIBS) |sed -n -e 's/.*\(__u_boot_cmd_.*\)/-u\1/p'|sort|uniq`;\
264 cd $(LNDIR) && $(LD) $(LDFLAGS) $$UNDEF_SYM $(__OBJS) \
265 --start-group $(__LIBS) --end-group $(PLATFORM_LIBS) \
266 -Map u-boot.map -o u-boot
267

        先使用第262~266的规则连接得到ELF格式的u-boot,最后转换为二进制格式的u-boot.bin、S-Record格式的u- boot.srec。          LDFLAGS确定了连接方式,其中的“-T board/smdk2410/u-boot.lds -Ttext 0x33F80000”字样指定了程序的布局、地址。          board/smdk2410/u-boot.lds文件如下:

28 SECTIONS
29 {
30     . = 0x00000000;
31
32     . = ALIGN(4);
33     .text :
34     {
35         cpu/arm920t/start.o(.text)
36         *(.text)
37     }
38
39     . = ALIGN(4);
40     .rodata : { *(.rodata) }
41
42     . = ALIGN(4);
43     .data : { *(.data) }
44
45     . = ALIGN(4);
46     .got : { *(.got) }
47
48     . = .;
49     __u_boot_cmd_start = .;
50     .u_boot_cmd : { *(.u_boot_cmd) }
51     __u_boot_cmd_end = .;
52
53     . = ALIGN(4);
54     __bss_start = .;
55     .bss : { *(.bss) }
56     _end = .;
57 }

        从第35行可知,cpu/arm920t/start.o被放在程序的最前面,所以U-Boot的入口点在cpu/arm920t/start.S中。

        现在来总结一下U-Boot的编译流程:

(1)首先编译cpu/$(CPU)/start.S,对于不同的CPU,还可能编译cpu/$(CPU)下的其他文件。

(2)然后,对于平台/开发板相关的每个目录、每个通用目录,都使用它们各自的Makefile生成相应的库。

(3)将1、2步骤生成的.o、.a文件按照board/$(BOARDDIR)/config.mk文件中指定的代码段起始地址、board/$(BOARDDIR)/u-boot.lds连接脚本进行连接。

(4)第3步得到的是ELF格式的U-Boot,后面Makefile还会将它转换为二进制格式、S-Record格式。

八、U-Boot的启动过程

        首先强调,本书使用的U-Boot从NOR Flash启动,下面以开发板smdk2410的U-Boot为例。

        U-Boot属于两阶段的Bootloader,第一阶段的文件为cpu/arm920t/start.S和board/smdk2410/lowlevel_init.S,前者是平台相关,后者是开发板相关。

1、U-Boot第一阶段代码分析 

(1)硬件设备初始化。

        依次完成如下设置:将CPU的工作模式设为管理模式(svc),关闭WATCHDOG,设置FCLK、HCLK、PCLK的比例(即设置CLKDIVN寄存器),关闭MMU、CACHE。

        代码都在cpu/arm920t/start.S中,注释也比较完善。

(2)为加载Bootloader的第二阶段代码准备RAM空间。

        所谓准备RAM空间,就是初始化内存芯片,使它可用。对于S3C2410/S3C2440,通过在start.S中调用lowlevel_init函数来设置存储控制器,使得外接的SDRAM可用。代码在board/smdk2410/lowlevel_init.S中。

        注意:lowlevel_init.S文件是开发板相关的,这表示如果外接的设备不一样,可以修改lowlevel_init.S文件中的相关宏。

        lowlevel_init函数并不复杂,只是要注意这时的代码、数据都只保存在NOR Flash上,内存中还没有,所以读取数据时要变换地址。代码如下:

129 _TEXT_BASE:
130 .wordTEXT_BASE
131
132 .globl lowlevel_init
133 lowlevel_init:
134     /* memory control configuration */
135     /* make r0 relative the current location so that it */
136     /* reads SMRDATA out of FLASH rather than memory ! */
137     ldr r0, =SMRDATA
138     ldr r1, _TEXT_BASE
139     sub r0, r0, r1
140     ldr r1, =BWSCON/* Bus Width Status Controller */
141     add r2, r0, #13*4
142 0:
143     ldr r3, [r0], #4
144     str r3, [r1], #4
145     cmp r2, r0
146     bne 0b
147
148     /* everything is fine now */
149     mov pc, lr
150
151     .ltorg
152 /* the literal pools origin */
153
154 SMRDATA:/* 13个寄存器的值 */
155     .word ……
156     .word ……

        第137~139行进行地址变换,因为这时候内存中还没有数据,不能使用连接程序时确定的地址来读取数据。

        第137行中SMRDATA 表示这13个寄存器的值存放的开始地址(连接地址),值为0x33F8xxxx,处于内存中。

        第138行获得代码段的起始地址,它就是第130行中的“TEXT_BASE”,其值在board/smdk2410/config.mk中定义:“TEXT_BASE = 0x33F80000”。

        第139行将0x33F8xxxx与0x33F80000相减,这就是13个寄存器值在NOR Flash上存放的开始地址。

(3)拷贝Bootloader的第二阶段代码到 RAM 空间中。

        这里将整个U-Boot的代码(包括第一、第二阶段)都复制到SDRAM中,这在cpu/arm920t/start.S中实现:

164 relocate:                /* 将U-Boot复制到RAM中 */
165     adrr0, _start        /* r0 = 当前代码的开始地址 */
166     ldrr1, _TEXT_BASE    /* r1 = 代码段的连接地址 */
167     cmp r0, r1           /* 测试现在是在Flash中还是在RAM中 */
168     beq stack_setup      /* 如果已经在RAM中(这通常是调试时,直接下载到RAM中),
                              * 则不需要复制
                              */
169
170     ldrr2, _armboot_start /* _armboot_start在前面定义,是第一条指令的运行地址 */
171     ldrr3, _bss_start     /* 在连接脚本u-boot.lds中定义,是代码段的结束地址 */
172     subr2, r3, r2         /* r2 = 代码段长度 */
173     addr2, r0, r2         /* r2 = NOR Flash上代码段的结束地址 */
174
175 copy_loop:
176     ldmiar0!, {r3-r10}    /* 从地址[r0]处获得数据 */
177     stmiar1!, {r3-r10}    /* 复制到地址[r1]处 */
178     cmpr0, r2             /* 判断是否复制完毕 */
179     blecopy_loop          /* 没复制完,则继续 */

(4)设置好栈。

栈的设置灵活性很大,只要让sp寄存器指向一段没有使用的内存即可。

182 /* Set up the stack */
183 stack_setup:
184     ldr r0, _TEXT_BASE             /* _TEXT_BASE为代码段的开始地址,值为0x33F80000 */
185     sub r0, r0, #CFG_MALLOC_LEN    /* 代码段下面,留出一段内存以实现malloc */
186     sub r0, r0, #CFG_GBL_DATA_SIZE /* 再留出一段内存,存一些全局参数 */
187 #ifdef CONFIG_USE_IRQ
188     sub r0, r0, #(CONFIG_STACKSIZE_IRQ+CONFIG_STACKSIZE_FIQ) /* IRQ、FIQ模式的栈 */
189 #endif
190     sub sp, r0, #12 /* 最后,留出12字节的内存给abort异常,
                         * 往下的内存就都是栈了
                         */
191

        到了这一步,读者可以知道内存的使用情况了,如下图所示(图中与上面的划分稍有不同,这是因为在cpu/arm920t/cpu.c中的cpu_init函数中才真正为IRQ、FIQ模式划分了栈):

U-Boot移植教程之一:U-Boot分析与启动过程_第11张图片
图15.3 U-Boot内存使用情况

(5)跳转到第二阶段代码的C入口点。

在跳转之前,还要清除BSS段(初始值为0、无初始值的全局变量、静态变量放在BSS段),代码如下:

192 clear_bss:
193     ldr r0, _bss_start  /* BSS段的开始地址,它的值在连接脚本u-boot.lds中确定 */
194     ldr r1, _bss_end    /* BSS段的结束地址,它的值在连接脚本u-boot.lds中确定 */
195     mov r2, #0x00000000
196
197 clbss_l:str r2, [r0]    /* 往BSS段中写入0值 */
198     add r0, r0, #4
199     cmp r0, r1
200     ble clbss_l
201

        现在,C函数的运行环境已经完全准备好,通过如下命令直接跳转(这之后,程序才在内存中执行),它将调用lib_arm/board.c中的start_armboot函数,这是第二阶段的入口点:

223      ldr pc, _start_armboot
224
225 _start_armboot:.word start_armboot
226

2、U-Boot第二阶段代码分析 

        它与15.1.2节中描述的Bootloader第二阶段所完成的功能基本上一致,不过顺序有点小差别。另外,U-Boot在启动内核之前可以让用户决定是否进入下载模式,即进入U-Boot的控制界面。

        第二阶段从lib_arm/board.c中的start_armboot函数开始,先看从这个函数开始的程序流程图,图15.3所示。

U-Boot移植教程之一:U-Boot分析与启动过程_第12张图片

        移植U-Boot的主要工作在于对硬件的初始化、驱动,所以下面讲解时将重点放在硬件的操作上。

(1)初始化本阶段要使用到的硬件设备。

        最主要的是设置系统时钟、初始化串口,只要这两个设置好了,就可以从串口看到打印信息。

        board_init函数设置MPLL、改变系统时钟,它是开发板相关的函数,在board/smdk2410/smdk2410.c中实现。值得注意的是,board_init函数中还保存了机器类型ID,这将在调用内核时传给内核,代码如下:

/* arch number of SMDK2410-Board */
gd->bd->bi_arch_number = MACH_TYPE_SMDK2410; /* 值为193 */

        串口的初始化函数主要是serial_init,它设置UART控制器,是CPU相关的函数,在cpu/arm920t/s3c24x0/serial.c中实现。

(2)检测系统内存映射(memory map)。

        对于特定的开发板,其内存的分布是明确的,所以可以直接设置。board/smdk2410/smdk2410.c中的dram_init函数指定了本开发板的内存起始地址为0x30000000,大小为0x4000000。代码如下:

int dram_init (void)
{
    gd->bd->bi_dram[0].start = PHYS_SDRAM_1;    /* 即0x300000000 */
    gd->bd->bi_dram[0].size = PHYS_SDRAM_1_SIZE;/* 即0x4000000 */
    return 0;
}

这些设置的参数,将在后面向内核传递参数时用到。

(3)U-Boot命令的格式。

        从图15.3可以知道,即使是内核的启动,也是通过U-Boot命令来实现的。U-Boot中每个命令都通过U_BOOT_CMD宏来定义,格式如下:

U_BOOT_CMD(name,maxargs,repeatable,command,"usage","help")

各项参数的意义为:

① name:命令的名字,注意,它不是一个字符串(不要用双引号括起来)。

② maxargs:最大的参数个数

③ repeatable:命令是否可重复,可重复是指运行一个命令后,下次敲回车即可再次运行。

④ command:对应的函数指针,类型为(*cmd)(struct cmd_tbl_s *, int, int, char *[])。

⑤ usage:简短的使用说明,这是个字符串。

⑥ help:较详细的使用说明,这是个字符串。

宏U_BOOT_CMD在include/command.h中定义:

#define U_BOOT_CMD(name,maxargs,rep,cmd,usage,help) /
cmd_tbl_t __u_boot_cmd_##name Struct_Section = {#name, maxargs, rep, cmd, usage, help}

Struct_Section也是在include/command.h中定义:

#define Struct_Section __attribute__ ((unused,section (".u_boot_cmd")))

比如对于bootm命令,它如此定义:

U_BOOT_CMD(
    bootm,CFG_MAXARGS,1,do_bootm,
    “string1”,
    “string2”
);

宏U_BOOT_CMD扩展开后就是:

cmd_tbl_t __u_boot_cmd_bootm __attribute__ ((unused,section (".u_boot_cmd"))) = {“bootm”, CFG_MAXARGS, 1, do_bootm, “string1”, “string2”};

        对于每个使用U_BOOT_CMD宏来定义的命令,其实都是在".u_boot_cmd"段中定义一个cmd_tbl_t结构。连接脚本u-boot.lds中有如下代码:

__u_boot_cmd_start = .;
.u_boot_cmd : { *(.u_boot_cmd) }
__u_boot_cmd_end = .;

        程序中就是根据命令的名字在内存段__u_boot_cmd_start~__u_boot_cmd_end找到它的cmd_tbl_t结构,然后调用它的函数(请参考common/command.c中的find_cmd函数)。
        内核的复制和启动,可以通过如下命令来完成:bootm从内存、ROM、NOR Flash中启动内核,bootp则通过网络来启动,而nboot从NAND Flash启动内核。它们都是先将内核映像从各种媒介中读出,存放在指定的位置;然后设置标记列表以给内核传递参数;最后跳到内核的入口点去执行。具体实现的细节不再描述,有兴趣的读者可以阅读common/cmd_boot.c、common/cmd_net.c、common/cmd_nand.c来了解它们的实现。
(4)为内核设置启动参数。

        与15.1.2小节中《Bootloader与内核的交互》所描述的一样,U-Boot也是通过标记列表向内核传递参数。并且,15.1.2小节中内存标记、命令行标记的示例代码就是取自U-Boot中的setup_memory_tags、setup_commandline_tag函数,它们都是在lib_arm/armlinux.c中定义。一般而言,设置这两个标记就可以了,在配置文件include/configs/smdk2410.h中增加如下两个配置项即可:

#define CONFIG_SETUP_MEMORY_TAGS 1
#define CONFIG_CMDLINE_TAG       1

        对于ARM架构的CPU,都是通过lib_arm/armlinux.c中的do_bootm_linux函数来启动内核。这个函数中,设置标记列表,最后通过“theKernel (0, bd->bi_arch_number, bd->bi_boot_params)”调用内核。其中,theKernel指向内核存放的地址(对于ARM架构的CPU,通常是0x30008000),bd->bi_arch_number就是前面board_init函数设置的机器类型ID,而bd->bi_boot_params就是标记列表的开始地址。

 

 

你可能感兴趣的:(嵌入式Linux)