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源码分析
在拿到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
#include
#include
EOF
exit 0
./include/config.h文件内容:
#### u-boot-2010.06/include/config.h ####
/* Automatically generated - do not edit */
#define CONFIG_BOARDDIR board/hdu5
#include
#include
#include
./include/config.mk文件内容:
#### u-boot-2010.06/include/config.mk ####
ARCH = arm
CPU = hi3536
BOARD = hdu5
VENDOR = 空
SOC = hdu5
综上,总结下mkconfig文件(或者叫make hdu5_config)的作用:
l 确定ARCH、CPU、BOARD等变量的值,并存到./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 设置异常向量表
l 设置特权管理模式
l 初始化PLL、DDR、MUX…
l 关MMU,关CACHE
l 判断代码在RAM还是FLASH,将FLASH代码复制至RAM中
l 设置堆栈、清空bss段
l 跳转至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 : "");
/* 倒数读秒,如果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语言第二阶段代码可以分为以下部分:
l 为gd、bd数据结构分配地址,并清零
l 执行 init_fnc_ptr 函数指针数组中的各个初始化函数
板级特殊设备初始化(board_init)、时钟初始化(timer_init)、初始化环境变量(env_init)、串口控制台初始化(init_baudrate、console_init_f)、打印U-Boot信息(display_banner、print_cpuinfo、checkboard)、配置可用RAM大小(dram_init)
l 对gd, bd 数据结构赋值初始化
l 各种设备初始化
l NAND Flash初始化 (nand_init) 、MMC初始化 (mmc_initialize、mmc_flash_init) 、网络初始化 (eth_initialize)、初始化串口(serial_init、console_init_r) 、初始化其他外设(stdio_init)、杂项设备初始化(misc_init_r)
l 环境变量代码重定位(env_relocate)
l 使能中断(enable_interrupts)
l 进入主循环(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 为内核设置启动参数