【UBoot】uboot启动过程分析

1. 准备工作

    1.1 uboot源码

本文是基于hdu5开发板对应的uboot源码进行分析。

 1.2 代码阅读软件source insight

可以想象uboot源码包有10000多个文件,每个文件都有几百行甚至上千行代码。需要专业的代码阅读器查找函数原型,根据需求去查找阅读,完全没必要重头读到尾。

    1.3 hdu5开发板和对应的数据手册

在uboot源码中常常直接对一块地址进行操作,看的人云里雾里,通过查阅数据手册可以协助我们理解那些语句的作用。

    1.4 开发环境

我们还需要一个linux环境去编译uboot代码,还需要在linux下对uboot镜像进行反汇编。

注意:编译uboot镜像前需要配置交叉编译工具链。


2. U-Boot源码分析

    2.1 uboot的配置过

        在拿到uboot代码后的第一步,我们需要做什么?执行make hdu5_config。

        那为什么要执行make hdu5_config? 我们可以通过查看源代码去理解执行make hdu5_config的深层次原因。

        通过顶层Makefile可以看到,在执行make hdu5_config的时候,实质上调用了如下部分:

#### u-boot-2010.06/Makefile ####
hdu5_config: unconfig
        @$(MKCONFIG) $(@:_config=) arm hi3536 hdu5 NULL hi3536
#### 注意:$(@:_config=) 就是将hdu5_config中的_config替换为空!得到hdu5; ####
#### 注意:每段代码段的第一行指明了代码存在的目录 ####

        首先,确定下变量的值,这里以hdu5板为例:

#### 在顶层Makefile中会涉及到如下变量 ####
$1 = hdu5 
    $2 = arm  
    $3 = hi3536 
    $4 = hdu5 
    $5 = NULL
    $6 = hi3536  
      
    CURDIR  = ./  
    SRCTREE = ./  
    TOPDIR  = ./  
    MKCONFIG= $(SRCTREE)/mkconfig = ./mkconfig  
      
    BOARD_NAME = "$1" = hdu5  
    ARCH= arm  
    OBJTREE= $(if $(BUILD_DIR),$(BUILD_DIR),$(CURDIR))  = ./  
    LNPREFIX = 空  
    BOARDDIR = $4 = hdu5

        通过上面的代码可以推导出:@$(MKCONFIG) $(@:_config=) arm hi3536 hdu5 NULL hi3536 等于 ./mkconfig hdu5 arm hi3536 hdu5NULL hi3536

        推导出:”makehdu5_config” 实际执行 “./mkconfig hdu5 arm hi3536 hdu5 NULLhi3536”

        上面那这段代码具体干了什么事情呢?咱们继续向下分析。

        mkconfig实际上就是顶层目录下的一个文件。那么,就来研究下顶层目录下的mkconfig文件:

#### u-boot-2010.06/mkconfig ####
注: mkconfig文件注释符改为/* 注释内容 */ 
    /* Default: Create new config file */  
    APPEND=no  
    /* Name to print in make output */  
    BOARD_NAME=""     
    TARGETS=""  
    
    /* $#: ./mkconfig hdu5 arm hi3536 hdu5 NULL hi3536命令行参数的个数 
     *     $0         $1   $2  $3     $4   $5   $6 
     * $符号总结: 
     * $#: 代表后接的参数个数,以上为例这里为[6] 
     * $@: 代表["$1" "$2" "$3" "$4"]之意,每个变量是独立的(用双引号括起来) 
     * $*: 代表["$1c$2c$3c$4"],其中c为分割字符,默认为空格键,所以本例中代表["$1 $2 $3 $4"] 
     * 
     * -gt: great than; -lt: less than 
     */  
    while [ $# -gt 0 ] ; do  
        case "$1" in  
        /* shift命令:  
         * 变量号码偏移功能,简单来说就是移动变量,即转到下一个变量$2,$3... 
         * 见[鸟哥的linux私房菜] 
         */  
        --) shift ; break ;;  
        -a) shift ; APPEND=yes ;;  
        -n) shift ; BOARD_NAME="${1%%_config}" ; shift ;;  
        -t) shift ; TARGETS="`echo $1 | sed 's:_: :g'` ${TARGETS}" ; shift ;;  
        *)  break ;;  
        /* case条件不满足,故本循环中不做任何事 */  
        esac  
    done  
      
    /* BOARD_NAME = hdu5 */  
    [ "${BOARD_NAME}" ] || BOARD_NAME="$1"  
      
    /* 参数检查,不满足直接退出 */  
    [ $# -lt 4 ] && exit 1  
    [ $# -gt 6 ] && exit 1  
      
    if [ "${ARCH}" -a "${ARCH}" != "$2" ]; then  
        echo "Failed: \$ARCH=${ARCH}, should be '$2' for ${BOARD_NAME}" 1>&2  
        exit 1  
    fi  
      
    echo "Configuring for ${BOARD_NAME} board..."  
      
    /* Create link to architecture specific headers */  
    if [ "$SRCTREE" != "$OBJTREE" ] ; then  
        /* 在指定的${OBJTREE}目录下编译,可以保持源代码目录的干净,不执行该分支 */  
        mkdir -p ${OBJTREE}/include  
        mkdir -p ${OBJTREE}/include2  
        cd ${OBJTREE}/include2  
        rm -f asm  
        ln -s ${SRCTREE}/arch/$2/include/asm asm  
        LNPREFIX=${SRCTREE}/arch/$2/include/asm/  
        cd ../include  
        rm -f asm  
        ln -s ${SRCTREE}/arch/$2/include/asm asm  
    else  
        cd ./include  
        /* -f: 删除是不显示提示信息,对于不存在的文件,会忽略掉  
         * asm: 上次配置过程中建立的连接文件 
         */  
        rm -f asm  
        /* -s: make symbolic links instead of hard links 
         * asm -> /arch/arm/include/asm 
         */  
        ln -s ../arch/$2/include/asm asm  
    fi  
      
    /* 即/arch/$2/include/asm/arch-$6,为执行make hdu5_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  
      
    /* Create include file for Make 
     * >: 定向输出到文件,若文件不存在创建空文件 
     * >>: 追加内容到指定的文件末尾 
     */  
    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  
      
    /* Assign board directory to BOARDIR variable */  
    if [ -z "$5" -o "$5" = "NULL" ] ; then  

   /* BOARDDIR = hdu5 */ 
      BOARDDIR=$4  
    else   
        BOARDDIR=$5/$4  
    fi  
      
    /* Create board specific header file 
     */  
    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  
        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/hdu5   
    #include <config_defaults.h>    
    #include <configs/hdu5.h>    
    #include <asm/config.h>  

        ./include/config.mk文件内容:

#### u-boot-2010.06/include/config.mk ####
    ARCH    = arm  
    CPU     = hi3536
    BOARD   = hdu5
    VENDOR  = 空 
    SOC     = hdu5

        综上,总结下mkconfig文件(或者叫make hdu5_config)的作用:

            l  确定ARCHCPUBOARD等变量的值,并存到./include/config.mk文件中

            l  建立板级相关的 ./include/config.h文件

            l  建立指向其他文件的软链接


    2.2 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中):

            l  $(SUBDIRS):进入各个子目录中执行make

SUBDIRS	= tools \
	  examples/standalone \
	  examples/api
.PHONY : $(SUBDIRS)
...
$(SUBDIRS):	depend
		$(MAKE) -C $@ all

            l  $(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 $@))

            l  $(LIBBOARD):这个也很好理解就是产生board/$(BOARDDIR)/lib$(BOARD).a,对hdu5来说,LIBBOARD =board /hdu5/libhdu5.a

BOARD    = hdu5          #### 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),,$@))

            l  $(LIBS):LIBS包括的目标非常多,都是将子目录的源码编成*.a库文件,通过执行每个目录的Makefile来实现。

LIBS  = lib/libgeneric.a
LIBS += lib/lzma/liblzma.a
…
$(LIBS):	depend $(SUBDIRS)
		$(MAKE) -C $(dir $(subst $(obj),,$@))

            l  $(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转换为二进制,去掉了其中调试信息,代码非常紧凑,文件小很多,适合作为镜像放板子上运行。


    2.3 uboot第一阶段解析

        接下来正式开始uboot源码之旅,分析代码当然要从上电后执行的第一条指令开始看起咯,那第一条指令在哪呢?还是以hdu5为例,首先我们来看一下它的链接脚本,通过它我们可以知道它整个程序的各个段是怎么存放的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汇编代码。

.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\hdu5\config.mk里面找到定义,它的值为0x40c00000。这样我就可以知道为什么System.map的起始地址0x40c00000。

#### u-boot-2010.06\board\hdu5\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代码 */

        总结:汇编第一阶段的代码主要可以分为以下部分:

            l  设置异常向量表

            设置特权管理模式

            初始化PLLDDRMUX…

            MMU,关CACHE

            判断代码在RAM还是FLASH,将FLASH代码复制至RAM

            设置堆栈、清空bss

            跳转至C语言处,进入第二阶段


   3.4 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 (;;);
}

        在hdu5平台比较重要的初始化函数有 board_init 以及 env_init,代码如下:

#### u-boot-2010.06\board\hdu5\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语言第二阶段代码可以分为以下部分:

            l  gdbd数据结构分配地址,并清零

            执行 init_fnc_ptr 函数指针数组中的各个初始化函数

            板级特殊设备初始化(board_init)、时钟初始化(timer_init)、初始化环境变量(env_init)、串口控制台初始化(init_baudrateconsole_init_f)、打印U-Boot信息(display_bannerprint_cpuinfocheckboard)、配置可用RAM大小(dram_init

            gd, bd 数据结构赋值初始化

            各种设备初始化

         NAND Flash初始化 (nand_init) 、MMC初始化 (mmc_initializemmc_flash_init) 、网络初始化          (eth_initialize)、初始化串口(serial_initconsole_init_r) 、初始化其他外设(stdio_init)、杂项设备初始化(misc_init_r

            环境变量代码重定位(env_relocate

            使能中断(enable_interrupts

            进入主循环(main_loop


4. 总结

    u-boot的配置过程,可以简单描述为:

        l  创建到目标板相关文件的链接

       l  创建include/config.mk文件,内容如下:

       l  创建与目标板相关的头文件include/config.h

       l  后续执行编译的时候,到哪些路径下面找文件都是在配置时确定的。

    uboot的编译和链接过程,可以简单描述为:

       l  将所有需要的.c文件编译生成.o文件,将需要的部分文件编成.a库,最后再将这些文件按照链接脚本组合成最后的目标文件。

    第一阶段代码,可以简单描述为:

       l  初始化本阶段要使用到的硬件设备

       l  为加载Bootloader的第二阶段代码准备RAM空间

       l  复制Bootloader的第二阶段到RAM空间

       l  设置好堆栈

       l  跳转到第二阶段的C入口点

    第二阶段代码,可以简单描述为:

       l 初始化本阶段要使用到的硬件设备

       l  配置系统内存映射,

       l  将内核映像和根文件系统映像从Flash上读到RAM空间中

       l  为内核设置启动参数

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 技术黑板 设计师:CSDN官方博客 返回首页