【uboot】Uboot的启动流程

引言

在驱动岗位上,每一位新员工刚入职期间都需要理解和掌握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,将ARCHCPUBOARD等信息写入

     * >: 定向输出到文件,若文件不存在创建空文件

     * >>: 追加内容到指定的文件末尾

     */ 

    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  

    #include  

    #include  

    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    

    #include igs/Hi3536_demo.h>   

    #include  

./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)的作用:

  1. 确定ARCHCPUBOARD等变量的值,并存到./include/config.mk文件中
  2. 建立板级相关的 ./include/config.h文件
  3. 建立指向其他文件的软链接

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格式,*/

/* 32ARM指令,小端 */

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    --关闭MMUcache */

在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内存使用情况了,如下图所示:

【uboot】Uboot的启动流程_第1张图片

这段代码的意思是清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代码 */

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

  1. 设置异常向量表
  2. 设置特权管理模式
  3. 初始化PLLDDRMUX…
  4. MMU,关CACHE
  5. 判断代码在RAM还是FLASH,将FLASH代码复制至RAM
  6. 设置堆栈、清空bss
  7. 跳转至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,  /* 初始化IIChard:真正iicsoft: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 : "");

    /* 倒数读秒,如果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 ("\n");

                 else

                         rc = run_command(lastcommand, flag);  /* 执行命令 */

                         lastcommand[0] = 0;  /* 将命令置无效命令令其不可重复 */

        }

}

总结,C语言第二阶段代码可以分为以下部分:

  1. gdbd数据结构分配地址,并清零
  2. 执行 init_fnc_ptr 函数指针数组中的各个初始化函数

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

  1. gd , bd 数据结构赋值初始化
  2. 各种设备初始化
  3. NAND Flash初始化(nand_init)、MMC初始化(mmc_initializemmc_flash_init)、网络初始化(eth_initialize)、初始化串口(serial_initconsole_init_r)、初始化其他外设(stdio_init)、杂项设备初始化(misc_init_r
  4. 环境变量代码重定位(env_relocate
  5. 使能中断(enable_interrupts
  6. 进入主循环(main_loop

总结

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

  1. 创建到目标板相关文件的链接
  2. 创建include/config.mk文件,内容如下:
  3. 创建与目标板相关的头文件include/config.h
  4. 后续执行编译的时候,到哪些路径下面找文件都是在配置时确定的。

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

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

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

  1. 初始化本阶段要使用到的硬件设备
  2. 为加载Bootloader的第二阶段代码准备RAM空间
  3. 复制Bootloader的第二阶段到RAM空间
  4. 设置好堆栈
  5. 跳转到第二阶段的C入口点

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

  1. 初始化本阶段要使用到的硬件设备
  2. 配置系统内存映射,
  3. 将内核映像和根文件系统映像从Flash上读到RAM空间中
  4. 为内核设置启动参数

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