1.源码结构分析
首先一个问题,老版本的u-boot是没有SPL这个文件的,新版u-boot开始包含SPL文件,原来u-boot启动比如放到nand中,在cpu内部有一个stepping stone,可以拷贝nand中的u-boot到ram中运行,然后u-boot自己再启动第二阶段在对应内存中好到系统的image启动。现在加了这个SPL之后,我的理解这是一个u-boot的loader。及cpu上电后,首先运行这个spl,然后通过这个spl再将u-boot放到对应的位置运行,之后的操作就和老版本基本一样了。至于为什么这样做,暂时还不明白,后期再研究下。
编译成功的u-boot-2013.10共有19个子目录,大约15个有用的文件,其中各个子目录和重要文件功能见下表:
名称 |
类型 |
功能说明 |
api |
通用 |
U-boot提供的一些接口函数 |
arch |
平台相关 |
当前U-BOOT重要的目录,arch下每个子目录代表一种处理器类型 |
board |
平台相关 |
里面有很多支持的开发板型号,这里关注具体开发板和config.mk |
common |
通用 |
主要跟U-BOOT的命令有关,cmd_xxx.c以及环境变量的处理代码env_xxx.c |
spl |
平台相关 |
u-boot的第一阶段相关,搬运第二阶段代码到内存中 |
disk |
通用 |
磁盘驱动的分区处理代码 |
doc |
说明文档 |
可以用来做配置参考 |
drivers |
通用 |
设备的驱动程序,每种类型一个子目录包括网卡,USB,LCD等 |
dts |
通用 |
设备树的控制,主要是由于LINUX 3.X中去除了很多冗余的代码,引入device tree,许多硬件细节可以直接传递给LINUX,是新的东西 |
examples |
通用 |
一些示例程序 |
fs |
通用 |
文件系统支持 |
include |
通用 |
头文件和开发板的配制,configs子目录重要 |
lib |
通用 |
通用的库文件 |
Licenses |
通用 |
可以忽略。。。 |
nand_spl |
平台相关 |
支持了部分平台的nand启动 |
net |
通用 |
网络相关的代码,小型的协议栈 |
post |
通用 |
加电自检程序 |
scripts |
通用 |
脚本程序 |
test |
通用 |
测试程序 |
tools |
通用 |
工具,mkimage就在这里 |
boards.cfg |
文件,平台相关 |
修改添加开发板配置现在在此处 |
Makefile MAKEALL config.mk rules.mk mkconfig |
文件,通用 |
整个U-BOOT编译过程的规则文件 |
kbuild mkconfig |
文件,通用 |
对Makefile功能的补充,使得编译更加高效 |
其余 |
文件,通用 |
介绍文档以及其他 |
移植工作主要集中在一些编译规则文件,还有board和arch目录下。
2 Makefile分析
u-boot的README里面其实讲的很清楚u-boot的移植过程,翻译过来如下:
第一步:在boards.cfg里面添加自己的开发板,必须按照现有的规则添加。
第二步:为自己的开发板建立一个目录,在目录下添加你需要的文件,这目录下必须要有以下几个文件,Makefile,<board>.c,flash.c和u-boot.lds
第三步:为你的建立一个新的配置文件“include/configs/<board>.h”
第四步:输入“make<board>_config”
第五步:make
第六步:调试并解决出现的问题(当然,这一步远比听起来的难很多)
Makefile的分析可以了解整个U-boot的代码结构是怎样的,文件是如何编译、链接的。
2.1分析配置过程第一步
在编译的时候第一步是输入:make wandboard_config,当输入这个指令的时候,Makefile就会调用以下语句:
%_config:: unconfig
@$(MKCONFIG) -A $(@:_config=)
%通配符匹配到执行xxx_config的时候,就调用下面的@$(MKCONFIG),这个MKCONFIG可以搜索在以下定义:
MKCONFIG := $(SRCTREE)/mkconfig
export MKCONFIG
2.2分析mkconfig文件
mkconfig为$(SRCTREE)/目录下的mkconfig文件,就是我们源码目录下的mkconfig文件,也就是说,我们输入了make wandboard_config之后,就执行了mkconfig。
mkconfig里面其实就是根据输入的板子的型号,这里是wandboard,调用boards.cfg文件,将arch cpu soc vender board等信息全部读出来,然后解析这些信息,进行通用头文件和库文件的自动配置,比如arm平台,很多lib库和头文件都是可以共用的。就在这一步生成头文件和很多宏,并将我们的板子的宏配置进去,如下所示为boards.cfg和Imx6平台相关的内容:
boards.cfg文件与imx6相关配置 展开原码
expand source?
cat << EOF >> config.h #define CONFIG_BOARDDIR board/$BOARDDIR #include <config_cmd_defaults.h> #include <config_defaults.h> #include <configs/${CONFIG_NAME}.h> #include <asm/config.h> #include <config_fallbacks.h> #include <config_uncmd_spl.h> |
这里调用了boards.cfg文件,切进去查看该文件,这个文件里面其实定义了u-boot可以支持的所有开发板,如下图:
2.3分析建立软连接过程
mkconfig建立软连接 展开原码
expand source?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
if [ "$SRCTREE" != "$OBJTREE" ] ; then mkdir -p ${OBJTREE}/include mkdir -p ${OBJTREE}/include2 cd ${OBJTREE}/include2 rm -f asm ln -s ${SRCTREE}/arch/${arch}/include/asm asm LNPREFIX=${SRCTREE}/arch/${arch}/include/asm/ cd ../include mkdir -p asm else cd ./include rm -f asm ln -s ../arch/${arch}/include/asm asm fi rm -f asm/arch if [ -z "${soc}" ] ; then ln -s ${LNPREFIX}arch-${cpu} asm/arch else ln -s ${LNPREFIX}arch-${soc} asm/arch fi if [ "${arch}" = "arm" ] ; then rm -f asm/proc ln -s ${LNPREFIX}proc-armv asm/proc fi |
以上是建立软连接的过程,if [ "$SRCTREE" != "$OBJTREE" ] ; then 表示判断源码目录是不是我们目标文件生产的目录,显然是的,我们生成的目标文件是在u-boot源码目录下的,所以直接跳到else后面,执行下面的语句:
cd ./include
rm -f asm
ln -s ../arch/${arch}/include/asm asm
切换到源码目录的include目录下,删除asm软连接,然后将上一级目录下arch/arm/include/asm目录链接到这个目录来,这是建立了第一个软连接。可以看得到:
接着rm -f asm/arch删除asm目录下的arch软连接,下面的代码:
建立软连接2
?
1 2 3 4 5 6 7 8 9 |
if [ -z "${soc}" ] ; then ln -s ${LNPREFIX}arch-${cpu} asm/arch else ln -s ${LNPREFIX}arch-${soc} asm/arch fi if [ "${arch}" = "arm" ] ; then rm -f asm/proc ln -s ${LNPREFIX}proc-armv asm/proc fi |
首先判断soc是否为空,这里soc显然不为空,执行ln -s ${LNPREFIX}arch-${soc} asm/arch ,这里LNPREFIX为空,所以这句其实就是ln -s ./arch-mx6 asm/arch,下面的同样是将arm相关的proc链接进去。
结果可以通过ls -l来查看:
2.4生成config.mk和头文件
生成config.mk文件
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
( echo "ARCH = ${arch}" if [ ! -z "$spl_cpu" ] ; then echo 'ifeq ($(CONFIG_SPL_BUILD),y)' echo "CPU = ${spl_cpu}" echo "else" echo "CPU = ${cpu}" echo "endif" else echo "CPU = ${cpu}" fi echo "BOARD = ${board}" [ "${vendor}" ] && echo "VENDOR = ${vendor}" [ "${soc}" ] && echo "SOC = ${soc}" exit 0 ) > config.mk |
上面的代码其实就是判断有没有定义spl_cpu如果定义了那就将spl_cpu信息输出到CPU,这里没有定义,因此依次就是确定了CPU.BOARD.SOC这些信息,最后一句> config.mk将以上信息输出到config.mk后退出。可以切换到./include/目录下,看到一个config.mk文件,打开看到如下内容:
接着mkconfig文件还做了自动生成头文件的工作,这部分代码如下:
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
if [ "$APPEND" = "yes" ] # Append to existing config file then echo >> config.h else > config.h # Create new config file fi echo "/* Automatically generated - do not edit */" >>config.h for i in ${TARGETS} ; do i= "`echo ${i} | sed '/=/ {s/=/ /;q; } ; { s/$/ 1/; }'`" echo "#define CONFIG_${i}" >>config.h ; done echo "#define CONFIG_SYS_ARCH \"${arch}\"" >> config.h echo "#define CONFIG_SYS_CPU \"${cpu}\"" >> config.h echo "#define CONFIG_SYS_BOARD \"${board}\"" >> config.h [ "${vendor}" ] && echo "#define CONFIG_SYS_VENDOR \"${vendor}\"" >> config.h [ "${soc}" ] && echo "#define CONFIG_SYS_SOC \"${soc}\"" >> config.h cat << EOF >> config.h #define CONFIG_BOARDDIR board/$BOARDDIR #include <config_cmd_defaults.h> #include <config_defaults.h> #include <configs/${CONFIG_NAME}.h> #include <asm/config.h> #include <config_fallbacks.h> #include <config_uncmd_spl.h> EOF exit 0 |
首先检查config.h存在否,如果不存在就建立一个config,h,然后依次定义宏到config.h中,最后加入一些arm平台下通用的头文件,最后保存退出。打开config.h文件,可以清晰看到如下内容:
这里并未定义文件里面的前四行内容,应该是手动添加进去的,确定mxl是否是有SPL启动,具体是哪个型号,然后根据具体型号再做一个配置,这里写到imx6image.cfg文件里查看。
imx6img.cfg
?
1 2 3 4 5 6 7 8 9 |
/* image version */ IMAGE_VERSION 2 /* * Boot Device : one of * spi, sd (the board has no nand neither onenand) */ BOOT_FROM sd #define __ASSEMBLY__ #include <config.h> #include "asm/arch/iomux.h" #include "asm/arch/crm_regs.h" #include "clocks.cfg" |
这个文件加入了另外几个头文件,猜测这个文件是和启动方式有关的配置文件,这里又加入了clocks.cfg文件,配置了启动时候的时钟,这部分代码后面分析启动过程的时候再分析。
2.5编译
这部分代码比较多,但是主要完成了以下几个剩余的工作:
1.u-boot版本号确认及语言环境确认
2.解析make后面传入的参数,例如make -v=1之类的,这里我们没有
3.指定源码目录和目标目录
4.获取machine号
5.确定交叉编译工具链,制定了我们的shell名称:/bin/bash,编译器套件名称:gcc,以及一些编译参数,-Wall表示要提示所有的warning。
6.设置头文件包含路径,输出目标制定目录,添加平台相关的头文件到指定目录。
7.根据配置执行make以及depend的依赖关系分别调用各子目录,生成所有的obj文件。
8.交代了u-boot是如何组装起来的,组装规则是u-boot.lds这个文件,把start.o和各个子目录makefile生成的库文件按照LDFLAGS连接在一起,生成ELF文件u-boot 和连接时内存分配图文件u-boot.map。这里,我们的u-boot.bin文件我理解的是从u-boot.elf拷贝过来的。
3.u-boot.lds
u-boot的代码是根据u-boot.lds组装起来的,由于u-boot.lds的代码比较晦涩,不过不要紧,只要能找到每一个阶段的入口就可以了,该文件内容如下:
第一启动阶段代码入口
?
1 2 3 4 5 6 7 8 9 10 11 12 |
OUTPUT_FORMAT( "elf32-littlearm" , "elf32-littlearm" , "elf32-littlearm" ) OUTPUT_ARCH(arm) ENTRY(_start) SECTIONS { . = 0x00000000; . = ALIGN(4); .text : { *(.__image_copy_start) //这里指定了影响文件复制的起始地址 arch/arm/cpu/armv7/start.o (.text*) //指明了启动第一阶段的文件为制定目录下的start.s *(.text*) } |
下面开始分析start.s和具体上电后的操作。
4.启动分析
start.S
?
22 23 |
.globl _start _start: b reset |
这里声明一个连接入口_start,上电后或者复位后第一句就跳转到reset,切过去:
start.S
?
110 111 112 113 114 115 116 117 118 |
reset: bl save_boot_params mrs r0, cpsr //将当前状态寄存器的值读到r0 and r1, r0, #0x1f //将r0的低五位状态赋值r1,也就是cpsr的低五位状态 teq r1, #0x1a //比较CPSR的低五位状态是否等于0x1a,该状态说明对应HYP模式,一种非安全状态行运行的新模式。 bicne r0, r0, #0x1f //如果不等于那就清楚低五位 orrne r0, r0, #0x13 //同样设置低五位为0x13,也就是10011,对应的是ARM的SVC管理模式 orr r0, r0, #0xc0 //11000000,禁止IRQ和FIQ msr cpsr,r0 //将r0的值读到CPSR,这时候am谁svc模式,中断被禁止。 |
CPSR寄存器
这里先跳转到save_boot_params,bl跳转后会返回回来继续执行,还是切过去看save_boot_params做了什么。
save_boot_params
?
179 180 181 182 |
ENTRY(save_boot_params) bx lr @ back to my caller ENDPROC(save_boot_params) .weak save_boot_params |
这里bx lr就直接返回跳转来之前的地址,也就是什么都不做,下面的.weak关键字作用是如果其他地方定义了save_boot_params那就调用,如果没有定义,这就是个空函数。
接着上面的代码段,具体做了什么已经注释的比较清楚了,接下来:
start.S
?
129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 |
#if !(defined(CONFIG_OMAP44XX) && defined(CONFIG_SPL_BUILD)) /* Set V=0 in CP15 SCTRL register - for VBAR to point to vector */ mrc p15, 0, r0, c1, c0, 0 @ Read CP15 SCTRL Register bic r0, #CR_V @ V = 0 mcr p15, 0, r0, c1, c0, 0 @ Write CP15 SCTRL Register /* Set vector address in CP15 VBAR register */ ldr r0, =_start mcr p15, 0, r0, c12, c0, 0 @Set VBAR #endif #ifndef CONFIG_SKIP_LOWLEVEL_INIT bl cpu_init_cp15 bl cpu_init_crit #endif bl _main |
这里我们没有定义CONFIG_OMAP44XX和CONFIG_SPL_BUILD,因此执行
mrc p15, 0, r0, c1, c0, 0,这是协处理器操作,只有mrc和mcr才能对arm的协处理器进行操作:
MRC {条件}协处理器编码,协处理器操作码1,目的寄存器,源寄存器1,源寄存器2,{协处理器操作码2}
MCR {条件}协处理器编码,协处理器操作码1,源寄存器,目的寄存器1,目的寄存器2,{协处理器操作码2}
这两个指令一般是成对使用,读出来在写进去,设置CP15协处理器的C1寄存器V位为0,查看寄存器手册:
设置地段一场中断向量0x0~0x1c。
然后将_start的地址给r0,再将该地址写到c12寄存器,也就是设置异常向量的基地址:
紧接着,这里没有定义skip_lowlevel_init,跳入cpu_init_cp15 ,顾名思义还是对cp15协处理器的设置。代码如下:
cpu_init_cp15 展开原码
expand source?
184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 |
ENTRY(cpu_init_cp15) /* * Invalidate L1 I/D * 使无效整个数据和指令TLB,然后使无效整个指令cache,清空整个跳转目标的cache,清空预取缓冲区,清空写缓冲区 */ mov r0, #0 @ set up for MCR mcr p15, 0, r0, c8, c7, 0 @ invalidate TLBs mcr p15, 0, r0, c7, c5, 0 @ invalidate icache mcr p15, 0, r0, c7, c5, 6 @ invalidate BP array mcr p15, 0, r0, c7, c10, 4 @ DSB mcr p15, 0, r0, c7, c5, 4 @ ISB /* * disable MMU stuff and caches * 设置低端异常中断向量,禁止MMU,禁止地址对齐检查,禁止数据Cache,前面已经禁止了指令cache。紧接着使能地址对齐检查,使能跳转预测功能 */ mrc p15, 0, r0, c1, c0, 0 bic r0, r0, #0x00002000 @ clear bits 13 (--V-) bic r0, r0, #0x00000007 @ clear bits 2:0 (-CAM) orr r0, r0, #0x00000002 @ set bit 1 (--A-) Align orr r0, r0, #0x00000800 @ set bit 11 (Z---) BTB #ifdef CONFIG_SYS_ICACHE_OFF bic r0, r0, #0x00001000 @ clear bit 12 (I) I-cache #else orr r0, r0, #0x00001000 @ set bit 12 (I) I-cache //这里没有定义ICACHE_OFF,因此这里使能指令Cache #endif mcr p15, 0, r0, c1, c0, 0 #ifdef CONFIG_ARM_ERRATA_716044 //没有定义该号码的宏,跳过 mrc p15, 0, r0, c1, c0, 0 @ read system control register orr r0, r0, #1 << 11 @ set bit #11 mcr p15, 0, r0, c1, c0, 0 @ write system control register #endif /* * 这里是对CP15的C15寄存器进行了操作,这里叫做诊断寄存器,然后将4,6,15都置位,这里我没找到c15寄存器的手册说明。 */ #ifdef CONFIG_ARM_ERRATA_742230 mrc p15, 0, r0, c15, c0, 1 @ read diagnostic register orr r0, r0, #1 << 4 @ set bit #4 mcr p15, 0, r0, c15, c0, 1 @ write diagnostic register #endif #ifdef CONFIG_ARM_ERRATA_743622 mrc p15, 0, r0, c15, c0, 1 @ read diagnostic register orr r0, r0, #1 << 6 @ set bit #6 mcr p15, 0, r0, c15, c0, 1 @ write diagnostic register #endif #ifdef CONFIG_ARM_ERRATA_751472 mrc p15, 0, r0, c15, c0, 1 @ read diagnostic register orr r0, r0, #1 << 11 @ set bit #11 mcr p15, 0, r0, c15, c0, 1 @ write diagnostic register #endif mov pc, lr @ back to my caller ENDPROC(cpu_init_cp15) |
这部分首先对r0清零,然后使无效整个数据和指令TLB,然后使无效整个指令cache,清空整个跳转目标的cache,清空预取缓冲区,清空写缓冲区, 设置低端异常中断向量,禁止MMU,禁止地址对齐检查,禁止数据Cache,前面已经禁止了指令cache。紧接着使能地址对齐检查,使能跳转预测功能 。然后后面有三个勘误宏,这里定义了三个,分别作了以下事情:对CP15的C15寄存器进行了操作,这里叫做诊断寄存器,然后将4,6,15都置位,这里我没找到c15寄存器的手册说明,具体意义不明,不过应该不影响后面的启动过程。
具体CP15的C0到C15寄存器信息参考下面的链接。
http://blog.sina.com.cn/s/blog_858820890102v1gc.html
完了之后,跳回子函数,然后顺序执行到函数cpu_init_crit:
cpu_init_crit 展开原码
expand source?
254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 |
#ifndef CONFIG_SKIP_LOWLEVEL_INIT /************************************************************************* * * CPU_init_critical registers * * setup important registers * setup memory timing * *************************************************************************/ ENTRY(cpu_init_crit) /* * Jump to board specific initialization... * The Mask ROM will have already initialized * basic memory. Go here to bump up clock rate and handle * wake up conditions. */ b lowlevel_init @ go setup pll,mux,memory ENDPROC(cpu_init_crit) #endif |
未定义SKIP_LOWLEVEL_INIT这部分代码其实就是跳转到lowlevel_init去了,lowlevel_init的作用就是引导加载c函数做进一步的初始化,切过去。
low_level_init.S
?
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
ENTRY(lowlevel_init) /* * Setup a temporary stack */ ldr sp, =CONFIG_SYS_INIT_SP_ADDR bic sp, sp, #7 /* 8-byte alignment for ABI compliance */ #ifdef CONFIG_SPL_BUILD ldr r9, =gdata #else sub sp, #GD_SIZE bic sp, sp, #7 mov r9, sp #endif /* * Save the old lr(passed in ip) and the current lr to stack */ push {ip, lr} /* * go setup pll, mux, memory */ bl s_init pop {ip, pc} ENDPROC(lowlevel_init) |
这里首先设置了一个临时的堆空间,将CONFIG_SYS_INIT_SP_ADDR的地址送到SP,这个地址=(CONFIG_SYS_INIT_RAM_ADDR + CONFIG_SYS_INIT_SP_OFFSET),CONFIG_SYS_INIT_RAM_ADDR 在
imx-regs.h里面定义了0x00900000,后面的CONFIG_SYS_INIT_SP_OFFSET未找到,忽略。
设置SP八个字节对齐之后,这里定义了CONFIG_SPL_BUILD。将gdata赋值给r9,跳转到s_init函数中去。
s_init在arch\arm\cpu\armv7\mx6的soc.c中,s_init主要是对IMX6的PFDs进行了板级设置。
在调用结束s_init之后,程序跳转到到_main函数里面,搜索定位该感受在arch/arm/lib/crt0.S下,这里是main函数的入口,主要做了以下工作:
- 重新对SP赋值, 确认sp是8字对齐
- 在栈顶保留一个global_data的大小, 这个global_data是uboot里面的一个全局数据, 很多地方都会用到. 俗称 gd_t
- 确认更新后的sp是8字对齐
- r9指向global_data, 后面别的地方想用global_data时候, 可以直接从r9里面获取地址.
- r0赋值0
- bl board_init_f: 跳转到board_init_f. 在编译SPL时, 分析Makefile可以看出, 该函数的实现是在<arch/arm/lib/spl.c>.
board_init_f在arch/arm/lib/spl.c中,主要做了以下事情:
- 对BSS段进行清零操作
- gd = &gdata;
- gd的定义在DECLARE_GLOBAL_DATA_PTR <arch/arm/include/asm/global_data.h>
- #define DECLARE_GLOBAL_DATA_PTR register volatile gd_t *gd asm ("r9")
- gdata的定义在本文件中: gd_t gdata __attribute__ ((section(".data")));
- 它是一个 gd_t 也就是global_data类型的变量
- __attribute__表示这个变量会被放到".data"这个输入段中. 连接器会把输入段按照链接脚本(u-boot-spl.lds)里面指定的规则存放到输出段
接着跳转到board_init_r,在common/spl/spl.c下面,主要做了以下事情:
对memory,timer初始化,选择在什么介质启动,最后判断image的类型,是u-boot还是linux。
5.总结
(reset) <arch/arm/cpu/armv7/start.S-> (b lowlevel_init: arch/arm/cpu/armv7/lowlevel_init.S) (b _main) --> <arch/arm/lib/crt0.S> (bl board_init_f) --> <arch/arm/lib/spl.c> (board_init_r) --> <common/spl/spl.c> (jump_to_image_no_args去启动u-boot)