引言
在驱动岗位上,每一位新员工刚入职期间都需要理解和掌握uboot,但深入的理解代码往往需要耗费大量的时间去反复阅读。本文希望对uboot进行尽可能详细的解析,帮助其他人更快的掌握和理解uboot源码。
准备工作
uboot源码
本文是基于Hi3536_demo开发板对应的uboot源码进行分析,可以通过下面面的tag信息使用git下载Hi3536_demo的uboot源码。
project path="packages/linux_lsp/boot/u-boot-2010.06", name="sysdev/packages/linux_lsp/boot/u-boot-2010.06", revision="sysdev_v1.x-201703" |
代码阅读软件source insight
可以想象uboot源码包有10000多个文件,每个文件都有几百行甚至上千行代码。需要专业的代码阅读器查找函数原型,根据需求去查找阅读,完全没必要重头读到尾。
Hi3536_demo开发板和对应的数据手册
在uboot源码中常常直接对一块地址进行操作,看的人云里雾里,通过查阅数据手册可以协助我们理解那些语句的作用。
开发环境
我们还需要一个linux环境去编译uboot代码,还需要在linux下对uboot镜像进行反汇编。
注意:编译uboot镜像前需要配置交叉编译工具链。
U-Boot源码分析
uboot的配置过程
在拿到uboot代码后的第一步,我们需要做什么?执行make Hi3536_demo_config。
那为什么要执行make Hi3536_demo_config? 我们可以通过查看源代码去理解执行make Hi3536_demo_config的深层次原因。
通过顶层Makefile可以看到,在执行make Hi3536_demo_config的时候,实质上调用了如下部分:
#### u-boot-2010.06/Makefile #### Hi3536_config: unconfig @$(MKCONFIG) $(@:_config=) arm hi3536 Hi3536 NULL hi3536 #### 注意:$(@:_config=) 就是将Hi3536_config中的_config替换为空!得到Hi3536; #### #### 注意:每段代码段的第一行指明了代码存在的目录 #### |
首先,确定下变量的值,这里以Hi3536_demo板为例:
#### 在顶层Makefile中会涉及到如下变量 #### $1 = Hi3536_demo “BOARD_NAME” $2 = arm “ARCH” $3 = hi3536 “CPU” $4 = Hi3536_demo “BOARD” $5 = NULL “VENDOR” $6 = hi3536 “SOC”
#OBJTREE = ./ //输出目录 #SRCTREE = ./ //源码目录 #TOPDIR = ./ //顶层目录 #LNDIR = ./ //连接目录 MKCONFIG= $(SRCTREE)/mkconfig = ./mkconfig
BOARD_NAME = "$1" = Hi3536_demo ARCH= arm OBJTREE= $(if $(BUILD_DIR),$(BUILD_DIR),$(CURDIR)) = ./ LNPREFIX = 空 BOARDDIR = $4 = Hi3536_demo |
通过上面的代码可以推导出:@$(MKCONFIG) $(@:_config=) arm hi3536 Hi3536_demo NULL hi3536 等于 ./mkconfig Hi3536_demo arm hi3536 Hi3536_demo NULL hi3536
推导出:”make Hi3536_demo_config” 实际执行 “./mkconfig Hi3536_demo arm hi3536 Hi3536_demo NULL hi3536”
上面那这段代码具体干了什么事情呢?咱们继续向下分析。
mkconfig实际上就是顶层目录下的一个文件。那么,就来研究下顶层目录下的mkconfig文件:
#### u-boot-2010.06/mkconfig #### /* $#: ./mkconfig Hi3536_demo arm hi3536 Hi3536_demo NULL hi3536命令行参数的个数 * $0 $1 $2 $3 $4 $5 $6 */ /* 创建include目录,将相关文件软连接 */ if [ "$SRCTREE" != "$OBJTREE" ] ; then mkdir -p ${OBJTREE}/include … cd ../include ln -s ${SRCTREE}/arch/$2/include/asm asm else cd ./include … ln -s ../arch/$2/include/asm asm fi
/* 即/arch/$2/include/asm/arch-$6,为执行make Hi3536_demo_config产生的连接文件, arch/arm/include/asm/arch-hi3536 */ rm -f asm/arch
/* -z STRING: 判断字符串STRING是否为0,若STRING为空字符串,则为true * -o: or或的意思 */ if [ -z "$6" -o "$6" = "NULL" ] ; then /* 软连接 */ ln -s ${LNPREFIX}arch-$3 asm/arch else /* arch->arch/arm/include/asm/arch-hi3536 */ ln -s ${LNPREFIX}arch-$6 asm/arch fi
if [ "$2" = "arm" ] ; then /* 软连接 */ /* proc->arch/arm/include/asm/proc-armv */ rm -f asm/proc ln -s ${LNPREFIX}proc-armv asm/proc fi
/* 创建include/config.mk,将ARCH、CPU、BOARD等信息写入 * >: 定向输出到文件,若文件不存在创建空文件 * >>: 追加内容到指定的文件末尾 */ echo "ARCH = $2" > config.mk echo "CPU = $3" >> config.mk echo "BOARD = $4" >> config.mk [ "$5" ] && [ "$5" != "NULL" ] && echo "VENDOR = $5" >> config.mk [ "$6" ] && [ "$6" != "NULL" ] && echo "SOC = $6" >> config.mk … /* 将config.mk中的数据写入到 config.h 并添加相关信息*/ for i in ${TARGETS} ; do echo "#define CONFIG_MK_${i} 1" >>config.h ; done cat << EOF >> config.h #define CONFIG_BOARDDIR board/$BOARDDIR #include <config_defaults.h> #include <configs/$1.h> #include <asm/config.h> EOF exit 0 |
./include/config.h文件内容:
#### u-boot-2010.06/include/config.h #### /* Automatically generated - do not edit */ #define CONFIG_BOARDDIR board/Hi3536_demo #include <config_defaults.h> #include <configs/Hi3536_demo.h> #include <asm/config.h> |
./include/config.mk文件内容:
#### u-boot-2010.06/include/config.mk #### ARCH = arm CPU = hi3536 BOARD = Hi3536_demo VENDOR = 空 SOC = Hi3536_demo |
综上,总结下mkconfig文件(或者叫make Hi3536_demo_config)的作用:
- 确定ARCH、CPU、BOARD等变量的值,并存到./include/config.mk文件中
- 建立板级相关的 ./include/config.h文件
- 建立指向其他文件的软链接
uboot的编译与链接过程
说完配置我们再回到Makefile中来看看编译与链接,面对Makefile的时候首先我们就会想到最后的目标文件u-boot.bin是怎样产生的:
#### u-boot-2010.06/Makefile #### /* CROSS_COMPILE = #指定编译器种类 */ /* OBJCOPY = $(CROSS_COMPILE)objcopy #转换目标文件格式 */ /* OBJCFLAGS += --gap-fill=0xff #段之间的空隙用0xff填充 */ |
$(obj)u-boot.bin: $(obj)u-boot $(OBJCOPY) ${OBJCFLAGS} -O binary $< $@ #### 注意:/* */这些是注释 #### |
从上段代码可以看到u-boot.bin 是用$(OBJCOPY) 从u-boot生成的,u-boot是elf格式的文件,不能直接在裸机上运行,所以需要用$(OBJCOPY) 把u-boot转换成二进制u-boot.bin文件。
#### u-boot-2010.06/Makefile #### $(obj)u-boot: ddr_training depend $(SUBDIRS) $(OBJS) $(LIBBOARD) $(LIBS) $(LDSCRIPT) $(obj)u-boot.lds $(GEN_UBOOT) ifeq ($(CONFIG_KALLSYMS),y) smap=`$(call SYSTEM_MAP,u-boot) | \ awk '$$2 ~ /[tTwW]/ {printf $$1 $$3 "\\\\000"}'` ; \ $(CC) $(CFLAGS) -DSYSTEM_MAP="\"$${smap}\"" \ -c common/system_map.c -o $(obj)common/system_map.o $(GEN_UBOOT) $(obj)common/system_map.o endif |
通过上面代码可以分析出:u-boot的产生依赖于depend,$(SUBDIRS),$(OBJS),$(LIBBOARD),$(LIBS),$(LDSCRIPT)。这里介绍一下这几个依赖目标(其中涉及到很多变量,均在顶层config.mk中):
$(SUBDIRS):进入各个子目录中执行make
SUBDIRS = tools \ examples/standalone \ examples/api .PHONY : $(SUBDIRS) ... $(SUBDIRS): depend $(MAKE) -C $@ all |
$(OBJS):OBJS = $(CPUDIR)/start.o, 即为'arch/arm/cpu/hi3536/start.o',而要产生start.o需要进入$(CPUDIR)进行编译。
CPUDIR=arch/$(ARCH)/cpu/$(CPU) #### u-boot-2010.06\config.mk #### OBJS = $(CPUDIR)/start.o … $(OBJS): depend $(MAKE) -C $(CPUDIR) $(if $(REMOTE_BUILD),$@,$(notdir $@)) |
$(LIBBOARD):这个也很好理解就是产生board/$(BOARDDIR)/lib$(BOARD).a,对Hi3536_demo来说,LIBBOARD = board /Hi3536_demo/libHi3536_demo.a
BOARD = Hi3536_demo #### u-boot-2010.06\include\config.mk #### BOARDDIR = $(BOARD) #### u-boot-2010.06\config.mk #### LIBBOARD = board/$(BOARDDIR)/lib$(BOARD).a LIBBOARD := $(addprefix $(obj),$(LIBBOARD)) $(LIBBOARD): depend $(LIBS) $(MAKE) -C $(dir $(subst $(obj),,$@)) |
$(LIBS):LIBS包括的目标非常多,都是将子目录的源码编成*.a库文件,通过执行每个目录的Makefile来实现。
LIBS = lib/libgeneric.a LIBS += lib/lzma/liblzma.a … $(LIBS): depend $(SUBDIRS) $(MAKE) -C $(dir $(subst $(obj),,$@)) |
$(LDSCRIPT):这里其实就是执行链接所需要的链接脚本,这里我需要特别强调链接脚本,链接脚本是程序链接的依据,它规定了可执行文件中的程序的输出格式是大端还是小端,程序如何来布局(第一条指令是那一条,各个依赖文件是如何组成最后的目标文件的),程序的入口是那里(只对elf文件有用)。
CURDIR = ./ SRCTREE := $(CURDIR) TOPDIR := $(SRCTREE) LDSCRIPT := $(TOPDIR)/board/$(BOARDDIR)/u-boot.lds $(LDSCRIPT): depend $(MAKE) -C $(dir $@) $(notdir $@) |
总结:u-boot的产生其实简单来说就进入各个目录下执行make,将指定目录下的.c文件编译生成.o文件,将指定目录下源码编成*.a库,最后再将这些文件按照链接脚本组合成最后的目标文件。
还有一点,通常放到板子上运行的镜像为u-boot.bin而不是u-boot,是因为u-boot虽然是一个可执行镜像,但里面包含了大量的调试信息,文件也非常的大。而u-boot.bin是将u-boot镜像通过objcopy转换为二进制,去掉了其中调试信息,代码非常紧凑,文件小很多,适合作为镜像放板子上运行。
uboot第一阶段解析
接下来正式开始uboot源码之旅,分析代码当然要从上电后执行的第一条指令开始看起咯,那第一条指令在哪呢? 还是以Hi3536_demo为例,首先我们来看一下它的链接脚本,通过它我们可以知道它整个程序的各个段是怎么存放的(uboot运行的第一段代码在arch/arm/cpu/hi3536/start.S文件中)。
#### u-boot-2010.06\arch\arm\cpu\hi3536\ u-boot.lds #### OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm") /*指定输出可执行文件是elf格式,*/ /* 32位ARM指令,小端 */ OUTPUT_ARCH(arm) /*指定输出可执行文件的平台为ARM*/ ENTRY(_start) /*指定输出可执行文件的起始代码段为_start*/ SECTIONS { /*指定可执行image文件的全局入口点,通常这个地址都放在ROM(flash)0x0位置。*/ /*必须使编译器知道这个地址,通常都是修改此处来完成*/ . = 0x00000000; /*;从0x0位置开始运行*/ . = ALIGN(4); /*代码以4字节对齐*/ .text : { __text_start = .; arch/arm/cpu/hi3536/start.o (.text) /* 代码段的起始部分就是最开始运行代码的地方, */ /* 因此uboot运行的第一条指令在arch/arm/cpu/hi3536/start.S文件 */ drivers/ddr/ddr_training_impl.o (.text) drivers/ddr/ddr_training_ctl.o (.text) drivers/ddr/ddr_training_boot.o (.text) drivers/ddr/ddr_training_custom.o (.text) __init_end = .; ASSERT(((__init_end - __text_start) < 0x16000), "init sections too big!"); *(.text) /*下面依次为各个text段函数*/ } . = ALIGN(4); /*代码以4字节对齐*/ .rodata : { *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.rodata*))) } /*指定只读数据段*/ . = ALIGN(4); .data : { *(.data) } . = ALIGN(4); .got : { *(.got) } /*指定got段, got段是uboot自定义的一个段, 非标准段*/ __u_boot_cmd_start = .; /*把__u_boot_cmd_start赋值为当前位置, 即起始位置*/ .u_boot_cmd : { *(.u_boot_cmd) } /*指定u_boot_cmd段, uboot把所有的uboot命令放在该段.*/ __u_boot_cmd_end = .; /*把__u_boot_cmd_end赋值为当前位置,即结束位置*/ . = ALIGN(4); __bss_start = .; /*把__bss_start赋值为当前位置,即bss段的开始位置*/ .bss : { *(.bss) } /*指定bss段 */ _end = .; /*把_end赋值为当前位置,即bss段的结束位置*/ } |
现在知道uboot的第一行代码在哪里运行了吗?(在arch/arm/cpu/hi3536/start.S中运行)下面我们来分析start.S汇编代码。
#### u-boot-2010.06\arch\arm\cpu\hi3536\start.S #### .globl _start _start: b reset ldr pc, _undefined_instruction ldr pc, _software_interrupt ldr pc, _prefetch_abort ldr pc, _data_abort ldr pc, _not_used ldr pc, _irq ldr pc, _fiq |
在这里我们终于看到了第一条运行指令是_start:b reset,呵呵!看到这段代码的时候许多人都认为_start的值是0x00000000,为什么是这个地址呢? 因为连接脚本上指定了。真的是这样吗?我们来看看我们编译好之后,在u-boot目录下有个System.map,这里面有各个变量的值。
#### u-boot-2010.06\System.map #### 40c00000 T __text_start 40c00000 T _start 40c00020 t _undefined_instruction 40c00024 t _software_interrupt 40c00028 t _prefetch_abort 40c0002c t _data_abort 40c00030 t _not_used 40c00034 t _irq 40c00038 t _fiq |
哈哈,_start的值怎么会是40c00000?这是因为在顶层的Makefile里面我们指定了它的连接地址。
#### u-boot-2010.06\Makefile #### GEN_UBOOT = \ UNDEF_SYM=`$(OBJDUMP) -x $(LIBBOARD) $(LIBS) | \ sed -n -e 's/.*\($(SYM_PREFIX)__u_boot_cmd_.*\)/-u\1/p'|sort|uniq`;\ cd $(LNDIR) && $(LD) $(LDFLAGS) $$UNDEF_SYM $(__OBJS) \ --start-group $(__LIBS) --end-group $(PLATFORM_LIBS) \ -Map u-boot.map -o u-boot $(obj)u-boot: ddr_training depend $(SUBDIRS) $(OBJS) $(LIBBOARD) $(LIBS) $(LDSCRIPT) $(obj)u-boot.lds $(GEN_UBOOT) |
看到那个LDFLAGS变量了吗?它是什么呢,我们继续往下面看:
#### u-boot-2010.06\config.mk #### LDFLAGS += -Bstatic -T $(obj)u-boot.lds $(PLATFORM_LDFLAGS) ifneq ($(TEXT_BASE),) LDFLAGS += -Ttext $(TEXT_BASE) /* 如果有TEXT_BASE变量,那LDFLAGS重新赋值 */ endif |
看到了没有,LDFLAGS先等于链接脚本中的地址,再判断TEXT_BASE是否等于空,如果TEXT_BASE不为空,LDFLAGS会被重新赋值。TEXT_BASE的值是多少呢?我们可以在u-boot-2010.06\board\Hi3536_demo\config.mk里面找到定义,它的值为0x40c00000。这样我就可以知道为什么System.map的起始地址0x40c00000。
#### u-boot-2010.06\board\Hi3536_demo\config.mk #### TEXT_BASE = 0x40c00000 |
下面我们继续来看第一条汇编指令b reset,初始化相关硬件操作。
Reset处代码有点不按照常理出牌,和网上通用的汇编起始代码有点不一样,它先判断部分寄存器中的值,再跳转到不同标志处运行。其中,“after_ziju”标志处代码执行初始化PLL/DDRC/pin mux/…等命令;
#### u-boot-2010.06\arch\arm\cpu\hi3536\start.S #### reset: /* uboot刚进来就进行的初始化操作 */ … beq after_ziju /* 若REG_SC_GEN2寄存器值 == 魔数,跳转到after_ziju标志处运行 */ … bne normal_start_flow /* 若REG_SC_GEN20寄存器值 !=魔数,跳转到 normal_start_flow 标志处运行 */ … after_ziju: /* 初始化 PLL/DDRC/pin mux/... */ … beq pcie_slave_addr /* 跳转到 pcie_slave_addr 处执行(PCIE相关初始化操作) */ … b ziju_ddr_init /* 跳转到 ziju_ddr_init 处运行(初始化PLL/DDRC/pin mux/...) */ pcie_slave_addr: /* PCIE相关初始化操作 */ … ziju_ddr_init: /*初始化PLL/DDRC/pin mux/... */ … bl init_registers /*跳转到 init_registers函数处运行,初始化PLL/DDRC/... */ … bl start_ddr_training /*跳转到 start_ddr_training函数处运行,DDR training */ … beq pcie_slave_hold /* 跳转到 pcie_slave_hold标志处运行,通常代码不会跑到这里 */ … mov pc, r1 /* 将pc指针返回到 bootrom */ pcie_slave_hold: /* pcie 出错保持,通常代码不对跑到这里 */ … b . /* bug here */ |
若满足“bne normal_start_flow”条件,运行“normal_start_flow”标志处的代码,这部分代码是普通uboot最开始启动时执行的命令。重要部分看注释。
这段汇编代码很好理解,就是设置CPU为管理模式、将cache置无效、关闭MMU和cache。这边抛出一个问题:
在汇编代码中,Invalidate cache、disable cache、flash cache分别表示什么含义?
Invalidate cache表示当前cache内的数据无效,所有cpu获取数据只能重新读取;flash cache表示清空cache中的数据;disable cache表示关闭cache。
#### u-boot-2010.06\arch\arm\cpu\hi3536\start.S #### normal_start_flow: mrs r0, cpsr /* set the cpu to SVC32 mode 设置管理模式 */ … mov r0, #0 /* Invalidate L1 I/D -- 置无效 I/D cache */ … mrc p15, 0, r0, c1, c0, 0 /* disable MMU stuff and caches --关闭MMU和cache */ … |
在normal_start_flow标志处代码执行到尾部,都没有跳转这一类指令,因此pc指针继续向下执行main_core标志处代码。
此处代码内容为找到对应的存储介质,将其中的代码拷贝到DDR中运行。
#### u-boot-2010.06\arch\arm\cpu\hi3536\start.S #### main_core: … bne check_bootrom_type /*检测是否需要跳转,PC的高八位如果不为0(已经在ram中运行了)则跳转,不等于则跳转*/ #ifndef CONFIG_HI3536_A7 /* 找到对应的存储介质 */ … cmp r6, #0 ldreq pc, _clr_remap_spi_entry /* SPI存储 */ cmp r6, #1 ldreq pc, _clr_remap_spi_nand_entry /* SPI_NAND 存储 */ cmp r6, #2 ldreq pc, _clr_remap_nand_entry /* NAND 存储 */ cmp r6, #3 ldreq pc, _clr_remap_ddr_entry /* DDR 存储 */ ldr pc, _clr_remap_nand_entry /* 所有其他情况,默认采用 NAND 存储 */ #endif check_bootrom_type: /* 将bootrom中的u-boot.bin 拷贝到RAM(0x4010c00) */ … ldreq pc, _clr_remap_ram_entry /* 根据不同的存储介质,传不同参数 */ do_clr_remap: /*清除remap */ #ifndef CONFIG_HI3536_A7 … /*清除remap */ #endif #ifdef CONFIG_ARCH_HI3536 … /* 如果使用Hi3536板卡,那就需要使能I/D cache */ #endif … /* 使能 Cache 操作 */ … bne ddr_init /* DDR初始化 */ … b copy_to_ddr /* 将u-boot.bin 拷贝到DDR */ ddr_init: /* DDR初始化相关 */ #ifndef CONFIG_HI3536_A7 … bl init_registers /* 初始化寄存器 */ #endif #ifdef CONFIG_DDR_TRAINING_V2 …. bl start_ddr_training /* DDR training */ #endif #ifndef CONFIG_HI3536_A7 … bne copy_flash_to_ddr /* 拷贝镜像到DDR */ #ifdef CONFIG_EMMC_SUPPORT emmc_boot: /* 初始化emmc,跳转到 jump_to_ddr */ bl emmc_boot_read /* 拷贝镜像 */ b jump_to_ddr /*跳转到 jump_to_ddr */ #endif copy_flash_to_ddr: /* 从NAND中拷贝镜像,跳转到 copy_to_ddr */ .. bne spi_nor_copy /* 拷贝镜像 */ … b copy_to_ddr /* 跳转到copy_to_ddr */ spi_nor_copy: /* 从SPI_NOR中拷贝镜像,跳转到 copy_to_ddr */ … bne spi_nand_copy /* 拷贝镜像 */ … b copy_to_ddr /* 跳转到copy_to_ddr */ spi_nand_copy: /* 从SPI_NAND中拷贝镜像,跳转到 copy_to_ddr */ … b copy_to_ddr /* 跳转到copy_to_ddr */ #endif copy_to_ddr: /* 将指定存储内的数据拷贝到DDR */ … beq copy_abort_code /* 拷贝操作 */ … bl memcpy /* 拷贝操作 */ jump_to_ddr: … ldr pc, _copy_abort_code /* 拷贝操作 */ copy_abort_code: … bl memcpy /* 拷贝操作 */ |
又到了熟悉的部分,如果要在C语言环境下执行代码,必须先初始化堆栈。
这段代码的意思是设置一些堆栈。
#### u-boot-2010.06\arch\arm\cpu\hi3536\start.S #### stack_setup: /* 设置栈指针 */ ldr r0, _TEXT_BASE @ upper 128 KiB: relocated uboot sub r0, r0, #CONFIG_SYS_MALLOC_LEN @ malloc area sub r0, r0, #CONFIG_SYS_GBL_DATA_SIZE @ bdinfo sub sp, r0, #12 @ leave 3 words for abort-stack and sp, sp, #~7 @ 8 byte alinged for (ldr/str)d |
只要将sp指针指向一段没有被使用的内存就完成栈的设置了。根据上面的代码可以知道U-Boot内存使用情况了,如下图所示:
这段代码的意思是清bss段。
#### u-boot-2010.06\arch\arm\cpu\hi3536\start.S #### clear_bss: /* 清除bss段 */ ldr r0, _bss_start /* r0 = bss段的起始位置 */ ldr r1, _bss_end @ stop here /* r1 = bss段结束位置 */ mov r2, #0x0 @ clear value /* r2 = 0 */ clbss_l: str r2, [r0] @ clear BSS location /* 先将r2,即0x0,存到地址为r0的内存中去 */ cmp r0, r1 @ are we at the end yet /* 比较r0地址和r1地址,即比较当前地址是否到了bss段的结束位置 */ add r0, r0, #4 @ increment clear index pointer /* 然后r0地址加上4 */ bne clbss_l @ keep clearing till at end /* 如果不等于,那么就跳到clbss_l,即接着这几个步骤,直到地址超过了bss的_end位置,即实现了将整个bss段,都清零。*/ |
这个时候,pc指针开始跳到RAM里面执行代码,这也就到了第二阶段(C语言阶段),后面的代码都是用C语言写的。
#### u-boot-2010.06\arch\arm\cpu\hi3536\start.S #### ldr pc, _start_armboot /* start_armboot,赋值给PC,即调用start_armboot函数 */ _start_armboot: .word start_armboot /* start_armboot函数,在C文件中,即跳转执行c代码 */ |
总结:汇编第一阶段的代码主要可以分为以下部分:
- 设置异常向量表
- 设置特权管理模式
- 初始化PLL、DDR、MUX…
- 关MMU,关CACHE
- 判断代码在RAM还是FLASH,将FLASH代码复制至RAM中
- 设置堆栈、清空bss段
- 跳转至C语言处,进入第二阶段
uboot第二阶段解析
在uboot第一阶段启动完成后将会调用start_armboot开始第二阶段的启动流程,这个阶段的代码由c语言编写,代码位于u-boot-2010.06\arch\arm\lib\board.c。
基础数据结构
第二阶段主要用到了两个数据结构即 gd_t 和 bd_t,其定义如下:
这两个类型变量记录了刚启动时的信息,还将记录作为引导内核和文件系统的参数,如 bootargs 等,并且将来还会在启动内核时,由 uboot 交由 kernel 时会有所用。
#### u-boot-2010.06\arch\arm\include\asm\global_data.h #### /* U-Boot使用了一个存储在寄存器中的指针gd来记录全局数据区的地址,这个指针存放在指定的寄存器r8中 */ typedef struct global_data { /* 全局数据结构 */ bd_t *bd; /* 指向板级信息结构 */ unsigned long flags; /* 标记位 */ unsigned long baudrate; /* 串口波特率 */ unsigned long have_console; /* serial_init() was called */ unsigned long env_addr; /* 环境参数地址 */ unsigned long env_valid; /* 环境参数 CRC 校验有效标志 */ unsigned long fb_base; /* fb 起始地址 */ #ifdef CONFIG_VFD unsigned char vfd_type; /* 显示器类型(VFD代指真空荧光屏) */ #endif #ifdef CONFIG_FSL_ESDHC /* 宏未定义 */ unsigned long sdhc_clk; #endif #if 0 /* 未定义 */ unsigned long cpu_clk; /* cpu 频率*/ unsigned long bus_clk; /* bus 频率 */ phys_size_t ram_size; /* ram 大小 */ unsigned long reset_status; /* reset status register at boot */ #endif void **jt; /* 跳转函数表 */ } gd_t; typedef struct bd_info { /* 板级信息结构 */ int bi_baudrate; /* 波特率 */ unsigned long bi_ip_addr; /* IP地址 */ struct environment_s *bi_env; /* 板子的环境变量 */ ulong bi_arch_number; /* 板子的 id */ ulong bi_boot_params; /* 板子的启动参数 */ struct /* RAM 配置 */ { ulong start; ulong size; } bi_dram[CONFIG_NR_DRAM_BANKS]; } bd_t; |
启动流程
start_armboot 首先为全局数据结构和板级信息结构分配内存,代码如下:
可以看到 bd_t 、gd_t 以及 MALLOC 区域是紧挨着的。
#### u-boot-2010.06\arch\arm\lib\board.c #### gd = (gd_t*)(_armboot_start - CONFIG_SYS_MALLOC_LEN - sizeof(gd_t)); /* compiler optimization barrier needed for GCC >= 3.4 */ __asm__ __volatile__("": : :"memory"); /* 内存屏障,防止编译器优化 */ memset ((void*)gd, 0, sizeof (gd_t)); /* 将指定的内存地址清零( 将全局数据清零 ) */ gd->bd = (bd_t*)((char*)gd - sizeof(bd_t)); /* gd->bd指向一块地址( 取得板级信息数据结构的起始地址 ) */ memset (gd->bd, 0, sizeof (bd_t)); /* gd->db指向地址中的内容清零( 将板级信息清零 ) */ gd->flags |= GD_FLG_RELOC; /* 标记为代码已经转移到 RAM */ |
然后依次调用 init_sequence数组中的函数指针完成各部分的初始化,代码如下:
#### u-boot-2010.06\arch\arm\lib\board.c #### init_fnc_t *init_sequence[] = { #if defined(CONFIG_ARCH_CPU_INIT) arch_cpu_init, /* 基本的处理器相关配置 -- basic arch cpu dependent setup */ #endif timer_init, /* 初始化定时器 -- initialize timer before usb init */ board_init, /* 板级特殊设备初始化(很重要) -- basic board dependent setup */ #if defined(CONFIG_USE_IRQ) interrupt_init, /* 中断初始化 -- set up exceptions */ #endif // timer_init, /* 初始化定时器 */ #ifdef CONFIG_FSL_ESDHC get_clocks, #endif env_init, /* 初始化环境变量(默认的环境变量) -- initialize environment */ init_baudrate, /* 初始化波特率设置 -- initialze baudrate settings */ serial_init, /* 初始化串口 */ console_init_f, /* 控制台初始化 -- stage 1 init of console */ display_banner, /* 打印uboot版本信息 -- say that we are here */ #if defined(CONFIG_DISPLAY_CPUINFO) print_cpuinfo, /* 显示cpu信息 -- display cpu info (and speed) */ #endif #if defined(CONFIG_DISPLAY_BOARDINFO) checkboard, /* 显示板级信息 -- display board info */ #endif #if defined(CONFIG_HARD_I2C) || defined(CONFIG_SOFT_I2C) init_func_i2c, /* 初始化IIC,hard:真正iic,soft:gpio模拟iic */ #endif dram_init, /* 配置可用RAM -- configure available RAM banks */ #if defined(CONFIG_CMD_PCI) || defined (CONFIG_PCI) arm_pci_init, /* 初始化pci */ #endif NULL, }; /* 函数指针,执行指针数组中的内容(实际内容为函数指针),初始化cpu、总线、设备等等*/ for (init_fnc_ptr = init_sequence; *init_fnc_ptr; ++init_fnc_ptr) { if ((*init_fnc_ptr)() != 0) { hang (); } } void hang (void) { puts ("### ERROR ### Please RESET the board ###\n"); for (;;); } |
在我们平台比较重要的初始化函数有 board_init 以及 env_init,代码如下:
#### u-boot-2010.06\board\Hi3536_demo\board.c #### int board_init(void) { unsigned long reg; /* set uart clk from apb bus */ reg = readl(CRG_REG_BASE + PERI_CRG57); /* 设置串口时钟 */ reg &= ~UART_CKSEL_APB; writel(reg, CRG_REG_BASE + PERI_CRG57); DECLARE_GLOBAL_DATA_PTR; gd->bd->bi_arch_number = MACH_TYPE_HI3536; gd->bd->bi_boot_params = CFG_BOOT_PARAMS; gd->flags = 0; boot_flag_init(); add_board_partition(&pri_board_part, FLASH_TYPE_EMMC); return 0; } #### u-boot-2010.06\common\env_common_func.c #### /* 初始化环境变量 */ int env_init(void) { #ifdef CONFIG_HI3536_A7 env_cmn_func = &nw_env_cmn_func; #else switch (get_boot_media()) { default: env_cmn_func = NULL; break; case BOOT_MEDIA_NAND: env_cmn_func = &nand_env_cmn_func; break; case BOOT_MEDIA_SPIFLASH: env_cmn_func = &sf_env_cmn_func; break; case BOOT_MEDIA_EMMC: env_cmn_func = &emmc_env_cmn_func; break; case BOOT_MEDIA_DDR: env_cmn_func = &nw_env_cmn_func; break; } #endif if (env_cmn_func && !env_cmn_func->env_name_spec) env_cmn_func = NULL; /* unknow start media */ if (!env_cmn_func) return -1; env_cmn_func->env_init(); env_name_spec = env_cmn_func->env_name_spec; return 0; } |
在环境变量 default_environment 中我们设置了很多参数,列表如下:
我们可以在 uboot 命令行模式下输入 printenv 命令查看当前的环境变量值。
#### u-boot-2010.06\tools\env\fw_env.c #### static char default_environment[] = { #if defined(CONFIG_BOOTARGS) "bootargs=" CONFIG_BOOTARGS "\0" #endif #if defined(CONFIG_BOOTCOMMAND) "bootcmd=" CONFIG_BOOTCOMMAND "\0" #endif … }; |
start_armboot 在接下来的流程中还做了如下操作:
#### u-boot-2010.06\arch\arm\lib\board.c #### void start_armboot (void) { … nand_init(); /* 初始化 NAND */ … mmc_initialize(0); /* 初始化MMC */ mmc_flash_init(0); env_relocate () /* 重定位环境变量,将其从 NAND 拷贝到内存中 */ … gd->bd->bi_ip_addr = getenv_IPaddr ("ipaddr"); /* 设置IP地址 */ stdio_init (); /* 初始化外设 */ jumptable_init (); /* 初始化跳转函数表 */ … console_init_r (); /* 控制台初始化第二阶段 */ … misc_init_r (); /* 杂项设备初始化, eg:battery */ … enable_interrupts (); /* 使能中断 */ #ifdef CONFIG_KEDACOM_E2PROM extern int kd_set_ethaddr(); kd_set_ethaddr(); #endif … /* 如果存在则从环境变量中读取装载地址,其默认为 ulong load_addr = CONFIG_SYS_LOAD_ADDR; */ if ((s = getenv ("loadaddr")) != NULL) { load_addr = simple_strtoul (s, NULL, 16); } #if defined(CONFIG_CMD_NET) if ((s = getenv ("bootfile")) != NULL) { copy_filename (BootFile, s, sizeof (BootFile)); } #endif … #if defined(CONFIG_CMD_NET) … eth_initialize(gd->bd); /* 网络初始化 */ … #endif #if defined(CONFIG_BOOTROM_SUPPORT) extern void download_boot(const int (*handle)(void)); download_boot(NULL); #endif product_control(); … #ifdef CONFIG_PARTTAB_ON_FLASH partition_check_update_flags(); #endif /* main_loop() can return to retry autoboot, if so just run it again. */ for (;;) { main_loop (); /* 进入主循环 common/main.c */ } } |
start_armboot 最终进入 main_loop 函数,首先判断用户选择的启动模式,如果是命令模式则等待输入命令然后执行,代码如下:
#### u-boot-2010.06\arch\arm\lib\board.c #### void main_loop (void) { … setenv ("ver", version_string); /* 设置版本信息 */ … update_tftp (); …. #if defined(CONFIG_BOOTDELAY) && (CONFIG_BOOTDELAY >= 0) s = getenv ("bootdelay"); /* 获取bootdelay环境变量的值 */ bootdelay = s ? (int)simple_strtol(s, NULL, 10) : CONFIG_BOOTDELAY; /* 将字符串转换为long类型变量 */ debug ("### main_loop entered: bootdelay=%d\n\n", bootdelay); debug ("### main_loop: bootcmd=\"%s\"\n", s ? s : "<UNDEFINED>"); /* 倒数读秒,如果delay时间内没有操作,执行run_command命令 */ if (bootdelay >= 0 && s && !abortboot (bootdelay)) { run_command (s, 0); } #endif /* CONFIG_BOOTDELAY */ … for (;;) { … len = readline (CONFIG_SYS_PROMPT); /* 读取输入 */ flag = 0; /* assume no special flags for now */ if (len > 0) strcpy (lastcommand, console_buffer); /* 将输入保存到历史记录中 */ else if (len == 0) flag |= CMD_FLAG_REPEAT; /* 如果没有输入则重复上次 */ … if (len == -1) puts ("<INTERRUPT>\n"); else rc = run_command(lastcommand, flag); /* 执行命令 */ lastcommand[0] = 0; /* 将命令置无效命令令其不可重复 */ } } |
总结,C语言第二阶段代码可以分为以下部分:
- 为gd、bd数据结构分配地址,并清零
- 执行 init_fnc_ptr 函数指针数组中的各个初始化函数
板级特殊设备初始化(board_init)、时钟初始化(timer_init)、初始化环境变量(env_init)、串口控制台初始化(init_baudrate、console_init_f)、打印U-Boot信息(display_banner、print_cpuinfo、checkboard)、配置可用RAM大小(dram_init)
- 对gd , bd 数据结构赋值初始化
- 各种设备初始化
- NAND Flash初始化(nand_init)、MMC初始化(mmc_initialize、mmc_flash_init)、网络初始化(eth_initialize)、初始化串口(serial_init、console_init_r)、初始化其他外设(stdio_init)、杂项设备初始化(misc_init_r)
- 环境变量代码重定位(env_relocate)
- 使能中断(enable_interrupts)
- 进入主循环(main_loop)
总结
u-boot的配置过程,可以简单描述为:
- 创建到目标板相关文件的链接
- 创建include/config.mk文件,内容如下:
- 创建与目标板相关的头文件include/config.h
- 后续执行编译的时候,到哪些路径下面找文件都是在配置时确定的。
uboot的编译和链接过程,可以简单描述为:
- 将所有需要的.c文件编译生成.o文件,将需要的部分文件编成.a库,最后再将这些文件按照链接脚本组合成最后的目标文件。
第一阶段代码,可以简单描述为:
- 初始化本阶段要使用到的硬件设备
- 为加载Bootloader的第二阶段代码准备RAM空间
- 复制Bootloader的第二阶段到RAM空间
- 设置好堆栈
- 跳转到第二阶段的C入口点
第二阶段代码,可以简单描述为:
- 初始化本阶段要使用到的硬件设备
- 配置系统内存映射,
- 将内核映像和根文件系统映像从Flash上读到RAM空间中
- 为内核设置启动参数