您的位置:首页 > 其它

Bootloader介绍和Uboot源码结构

2015-06-08 10:57 204 查看
本文是对《嵌入式Linux应用开发完全手册》的一个自我总结!

一. Bootloader介绍

1.Bootload引入的原因

Bootloader的作用是在系统启动的时候初始化必要的硬件设备,引导内核镜像文件,传递参数给内核,然后将控制权交给内核以结束自己的使命。总的来说就是为了引导操作系统。

Bootloader非常依赖具体的硬件,对每一个板子都有一个独一无二的Bootloader。

2.Bootloader启动方式

Bootloader有两种启动方式,分别对应着成熟的产品和开发中的产品,分别是:启动加载模式下载模式

启动加载模式

在系统启动时,运行Bootloader,当Bootloader准备好加载内核的环境后就直接从FLASH中拷贝内核镜像到内存,并且运行内核。

下载模式

该模式适合开发者。在开发过程中,所有东西都没有定型,需要经常性的修改。所以一般不会将内核放到目标板上面,而是存在在宿主机上,通过串口、USB、网络将内核传递到目标板上。串口传输协议有xmodem、ymodem、zmodem;网络传输有tftp、nfs。一般采用TFTP传输,因为网络传输速度快很多。

像Uboot这种强大的Bootloader可以同时支持两种模式。在默认情况下使用启动加载模式,但是中间会有几秒时间供开发者进入下载模式。

Bootloader的结构和启动过程

嵌入式Linux软件分为4类:

引导加载程序

分为固化在固件中的boot(可选)和Bootloader两大部分。PC机上的BIOS就是boot中的一种,对于大多数嵌入式设备只有Bootloader。

Linux内核

包含特制的Linux内核代码和内核启动参数。内核启动参数可以是Bootloader传递的,或者是编译内核时选定的默认参数。

文件系统

包含根文件系统和建立在FLASH内存设备上的文件系统,其他文件系统时挂载在根文件系统之上的。

用户应用程序

存放在文件系统里面。

boot parameters中存放的参数有IP地址等网络相关参数串口波特率命令行参数等等。这些参数会存放在与内核约定好的内存地址上。

Bootloader启动的两个过程

Bootloader的启动分为多阶段和单阶段两种方式。多阶段启动的Bootloader有更好的移植性,而且能实现更复杂的功能。第一阶段用汇编语言实现,第二阶段用C语言实现。

第一阶段

1.硬件设备初始化

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

3.复制Bootloader的第二段代码到RAM空间中

4.设置好栈

5.跳转到第二阶段代码的C入口点

在第一阶段的硬件初始化包括关闭watchdog、设置为SVC模式、关闭中断、设置CPU的速度和时钟频率、RAM初始化等。这些并不都是必须的,比如设置CPU的速度和时钟频率可以放在第二阶段。

其实将第二阶段拷贝到RAM中也不是必须的,对于NOR Flash可以直接在上面运行代码,但是效率将会降低。对于NAND FLASH必须要进行第二段代码的拷贝,因为一般Bootloader的二进制文件大于100K,而S3C2440内部自带的RAM才4K,不足以存放整个Bootloader。

第二阶段

1.初始化本阶段要使用的硬件设备(串口、网卡、NAND FLASH对于下载模式是必须的)

2.检测系统内存映射(确定板上使用了多少内存、它们的地址空间)

3.将内核镜像和根文件系统镜像从FLASH上读到RAM空间(对于不在FLASH上的内核和文件系统不执行)

4.为内核设置启动参数

与内核的交互

Bootloader和内核的交互是单向的,只能从Bootloader向内核传递。与内核相互可以分为两种:以内核要求的形式设置硬件,以内核要求的地方存放数据。

1.设置硬件

(1)CPU寄存器设置

R0 = 0

R1 = 机器类型ID;可参见/linux/arch/arm/tools/mach-types(在启动内核时首先就是检测CPU类型和mach类型是否正确)

R2 = 启动参数标记列表在RAM中起始基地址

(2)CPU工作模式

必须禁止中断(IRQs和FIQs)

CPU必须是SVC模式

(3)Cache和MMU的设置

MMU必须关闭

指令Cache可以打开也可以关闭

数据Cache必须关闭

2.存放数据

必须约定好参数的存放地址(通过R2寄存器传递),同时必须规范数据的结构。在Linux 2.4之后,内核期望以标记列表的形式来传递参数。标记列表以ATAG_CORE开始,以ATAG_NONE作为结束。

struct tag {
struct tag_header hdr;
union {
struct tag_core     core;
struct tag_mem32    mem;
struct tag_videotext    videotext;
struct tag_ramdisk  ramdisk;
struct tag_initrd   initrd;
struct tag_serialnr serialnr;
struct tag_revision revision;
struct tag_videolfb videolfb;
struct tag_cmdline  cmdline;

/*
* Acorn specific
*/
struct tag_acorn    acorn;

/*
* DC21285 specific
*/
struct tag_memclk   memclk;
} u;
};


上述代码是标记的结构体

params = (struct tag *) bd->bi_boot_params;

params->hdr.tag = ATAG_CORE;
params->hdr.size = tag_size (tag_core);

params->u.core.flags = 0;
params->u.core.pagesize = 0;
params->u.core.rootdev = 0;

params = tag_next (params);


上述代码是设置标记ATAG_CORE

#define tag_next(t) ((struct tag *)((u32 *)(t) + (t)->hdr.size))


tag_next(t)用来移动指针

常用Bootloader

对于x86常见的Bootloader是LILO、GRUB等,对于ARM架构的CPU,有U-Boot和Vivi。

Vivi是Mizi公司针对SAMSUNG的ARM架构CPU专门设计的,基本上可以直接使用。不过初始版本只支持串口下载,速度慢。网上有各种改进版本,支持网络功能、USB功能、烧写YAFFS文件系统映像等。

U-boot则支持大多数的CPU,支持的功能强大,不过它的使用复杂。但是可以用来更方便地调试程序。

二、Uboot源码结构

Uboot特性

U-boot有如下特性:

开放源码

支持多种内核:Linux、NetBSD、Vxworks、QNX、RTEMS、ARTOS、LynxOS

支持多个处理器系列:PowerPC、ARM、x86、MIPS、XScale

高度灵活的功能设置,适合U-Boot调试、操作系统不同引导要求、产品发布

丰富的设备驱动源码

较为丰富的开发调试文档与强大的网络支持

支持NFS挂载、RAMDISK形式的根文件系统

支持NFS挂载、从Flash中引导压缩或非压缩系统内核

可灵活设置、传递多个关键参数给操作系统、对Linux支持较为强劲

支持目标板环境变量多种存储方式,如Flash、NVRAM、EEPROM

CRC32校验,可检查Flash中内核、RAMDISK镜像文件是否完好

上电自检功能:SDRAM、FLASH大小自动检测

特殊功能:XIP内核引导

U-boot下载地址

U-boot源码结构

接下来的分析都是对于U-boot-2010-03

U-boot的源码目录可以分为4类:

平台相关或开发板相关目录

通用函数

驱动程序

U-Boot工具、示例程序、文档

U-Boot顶层目录说明

目录特性说明
board开发板相关对应不同的电路板(即使CPU相同),比如smdk2410、sbc2410x
cpu平台相关对应不同的CPU,比如arm920T、i386等,他们的子目录可以进一步细分,比如arm920t下有at91rm9200、s3c24x0
lib_i386类似平台相关某一架构下通用的文件
include通用函数头文件和开发板配置文件,开发板的配置文件都放在include/configs目录下,U-Boot没有图形化配置菜单,需要手动修改配置文件中的宏定义
lib_generic通用函数通用的库函数,比如printf等
comment通用函数通用的函数,对下一层驱动程序的进一步封装
disk驱动程序硬盘接口程序
drivers驱动程序各类具体设备的驱动程序,基本上可以通用,他们通过宏从外面引入平台/开发板相关的函数
dtt驱动程序数字温度测量器或者传感器的驱动
fs驱动程序文件系统
nand_spl驱动程序NAND驱动程序
net驱动程序各种网络协议
post驱动程序上电自检程序
rtc驱动程序实时时钟的驱动
doc文档开发、使用文档
examples示例程序一些测试程序,可以使用U-Boot下载后运行
tools工具制作S-Recond、U-Boot格式映像的工具,比如mkimage
U-boot的配置、编译、连接过程

编译U-boot只需要两步:

make <board_name>_config
make all


之后就会在源码根目录下生成三个文件:

U-Boot.bin : 二进制可执行文件,可以直接烧入ROM、NOR Flash

U-Boot : ELF格式的可执行文件

U-Boot.srec : Motorola S-Record格式的可执行文件

在编译完成之后会在tools子目录下生成一些工具,比如mkimage,可以用来生成U_boot格式的内核镜像uImage

U-Boot的配置过程

顶层makefile文件中代码

...
MKCONFIG    := $(SRCTREE)/mkconfig
...
SRCTREE     := $(CURDIR)
...
smdk2410_config :   unconfig
@$(MKCONFIG) $(@:_config=) arm arm920t smdk2410 samsung s3c24x0


$(@:_config=)


将目标中的config后缀去掉,结果为smdk2410

实际上上述代码等价于:

./mkconfig smdk2410 arm arm920t smdk2410 samsung s3c24x0


mkconfig是位于根目录下的脚本文件,接下来的很大一部分内容由它完成

mkconfig解析

# Parameters:  Target  Architecture  CPU  Board [VENDOR] [SOC]


说明了mkconfig中各个参数的含义

(1)确定开发板名称BOARD_NAME

APPEND=no   # Default: Create new config file
BOARD_NAME=""   # Name to print in make output
TARGETS=""
while [ $# -gt 0 ] ; do
case "$1" in
--) shift ; break ;;
-a) shift ; APPEND=yes ;;
-n) shift ; BOARD_NAME="${1%%_config}" ; shift ;;
-t) shift ; TARGETS="`echo $1 | sed 's:_: :g'` ${TARGETS}" ; shift ;;
*)  break ;;
esac
done

[ "${BOARD_NAME}" ] || BOARD_NAME="$1"


因为在命令中没有-a、-n等符号,所以while开始的语句没有执行。

执行完上述程序后BOARD_NAME = smdk2410

(2)创建头文件之间的连接

if [ "$SRCTREE" != "$OBJTREE" ] ; then
...
else
cd ./include
rm -f asm
ln -s asm-$2 asm
fi

rm -f asm-$2/arch

if [ -z "$6" -o "$6" = "NULL" ] ; then
ln -s ${LNPREFIX}arch-$3 asm-$2/arch
else
ln -s ${LNPREFIX}arch-$6 asm-$2/arch
fi

if [ "$2" = "arm" ] ; then
rm -f asm-$2/proc
ln -s ${LNPREFIX}proc-armv asm-$2/proc
fi


一般我们会在源码根目录进行编译,所以SRCTREE=OBJTREE,执行else部分的代码。

删除原有的/include/asm,建立新的连接

ln -s /include/asm-arm /include/asm


$6!=NULL,所以执行else语句,删除原有的/include/asm-arm/arm,建立新的连接,LNPREFIX为空

ln -s /include/asm-arm/arch-s3c24x0 /include/asm-arm/arch


建立另一个连接

ln -s /include/asm-arm/proc-armv  /include/asm-arm/proc


(3)为顶层makefile创建config.mk

下面代码摘自顶层makefile

include $(obj)include/config.mk
export  ARCH CPU BOARD VENDOR SOC


接下来看mkconfig为config.mk中写了什么内容

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=$4
else
BOARDDIR=$5/$4
fi


创建/config.mk,输出如下内容

ARCH = arm
CPU = arm920t
BOARD = smdk2410
VENDOR = samsung
SOC = s3c24x0
BORADDIR = samsung/smdk2410


(4)创建目标板相关头文件

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


APPEND=no,所以在include目录下创建config.h

for i in ${TARGETS} ; do
echo "#define CONFIG_MK_${i} 1" >>config.h ;
done


该语句不知道什么意思,但是它没有执行

所以最后会生成/include/config.h,其内容如下:

/* Automatically generated - do not edit */
#define CONFIG_BOARDDIR board/samsung/smdk2410
#include <config_defaults.h>
#include <configs/smdk2410.h>
#include <asm/config.h>


可以看出我们需要在/include/config目录下创建和我们的目标板相应的头文件。需要在/board目录下创建属于开发板的目录。

总结上述过程:

(1)BOARD_NAME=smdk2410

(2)创建头文件之间的软连接

ln -s /include/asm-arm /include/asm
ln -s /include/asm-arm/arch-s3c24x0 /include/asm-arm/arch
ln -s /include/asm-arm/proc-armv  /include/asm-arm/proc


(3)为顶层makefile创建/include/config.mk

ARCH = arm
CPU = arm920t
BOARD = smdk2410
VENDOR = samsung
SOC = s3c24x0
BORADDIR = samsung/smdk2410


(4)创建/include/config.h

/* Automatically generated - do not edit */
#define CONFIG_BOARDDIR board/samsung/smdk2410
#include <config_defaults.h>
#include <configs/smdk2410.h>
#include <asm/config.h>


配置U-Boot

U-Boot并没有图形化配置界面,所以需要手动配置,配置的文件是/include/configs/board_name.h

以/include/configs/smdk2410.h为例讲解如何配置U-Boot,并且配置是如何影响编译过程的。

配置文件中有两类宏:

定义宏的存在

#define CONFIG_ARM920T  1
#define CONFIG_S3C24X0  1
#define CONFIG_S3C2410  1
#define CONFIG_SMDK2410 1


这类宏能决定编译文件中的哪一部分

对宏进行赋值

#define CONFIG_SYS_CLK_FREQ 12000000
#define CONFIG_CS8900_BASE  0x19000300
#define CONFIG_BAUDRATE     115200


宏定义可以影响makefile的编译具体文件中的某一段的编译

COBJS-$(CONFIG_RTC_S3C24X0) += s3c24x0_rtc.o


上述代码取自/driver/rtc/Makefile

#ifdef CONFIG_S3C2410
return (readl(&gpio->GPEDAT) & 0x8000) >> 15;
#endif
#ifdef CONFIG_S3C2400
return (readl(&gpio->PGDAT) & 0x0020) >> 5;
#endif


上述代码取自/driver/i2c/s3c24x0_i2c.c

U-Boot的编译、连接过程

接下来讲解的是执行make all时编译和连接的过程。从makefile中可以了解到U-Boot使用了哪些文件、哪些文件首先执行、可执行文件占用内存的情况。

确定makefile中与ARM相关的部分

include $(obj)include/config.mk
export  ARCH CPU BOARD VENDOR SOC
...
ifeq ($(HOSTARCH),$(ARCH))
CROSS_COMPILE ?=
endif


我们编译的HOSTARCH为x86,而ARCH为arm,所以上述代码的CROSS_COMPILE ?= 不执行,所以我们要在make的时候加上CROSS_COMPILE = arm-linux-

在/lib-arm/config.mk文件中定义了

CROSS_COMPILE ?= arm-linux-


但为了保险起见,还是在编译时加上CROSS_COMPILE = arm-linux-

include $(TOPDIR)/config.mk


在makefile中还包含了根目录下在config.mk

/config.mk分析

config.mk根据ARCH CPU BOARD VENDOR SOC定义了编译器和编译选项。

下列的代码摘自/config.mk

ifdef   VENDOR
BOARDDIR = $(VENDOR)/$(BOARD)
else
BOARDDIR = $(BOARD)
endif


其实这一段代码是多余的,因为在/include/config.mk已经对BORADDIR赋了相同的值。

ifdef   ARCH
sinclude $(TOPDIR)/lib_$(ARCH)/config.mk
endif
ifdef   CPU
sinclude $(TOPDIR)/cpu/$(CPU)/config.mk
endif
ifdef   SOC
sinclude $(TOPDIR)/cpu/$(CPU)/$(SOC)/config.mk
endif
ifdef   BOARD
sinclude $(TOPDIR)/board/$(BOARDDIR)/config.mk


包含了各目录下的config.mk文件

其中/board/samsung/smdk2410/config.mk中包含了一个和连接有关的重要参数

TEXT_BASE = 0x33F80000


该文件中只有这一行代码

PLATFORM_LDFLAGS =
...
LDFLAGS += -Bstatic -T $(obj)u-boot.lds $(PLATFORM_LDFLAGS)
ifneq ($(TEXT_BASE),)
LDFLAGS += -Ttext $(TEXT_BASE)
endif


上述语句相当于下列语句:

LDFLAGS = -T u-boot.lds -Ttext 0x33F80000


其中的obj为空

makefile中具体编译内容

makefile中的目标文件只有两种OBJS、LIBS,其中OBJS是.o文件,LIBS是.a文件。

OBJS  = cpu/$(CPU)/start.o
ifeq ($(CPU),i386)
OBJS += cpu/$(CPU)/start16.o
OBJS += cpu/$(CPU)/resetvec.o
endif
ifeq ($(CPU),ppc4xx)
OBJS += cpu/$(CPU)/resetvec.o
endif
ifeq ($(CPU),mpc85xx)
OBJS += cpu/$(CPU)/resetvec.o
endif


上述是所有和OBJS有关源文件,可以看出只有一个原文件/cpu/arm920t/start.o,除了这个文件,源码树中的所有其它文件都被编译成了静态库。

OBJS := $(addprefix $(obj),$(OBJS))


为OBJS加上前缀

LIBS  = lib_generic/libgeneric.a
LIBS += lib_generic/lzma/liblzma.a
LIBS += lib_generic/lzo/liblzo.a
...


和LIBS有关的原文件太多了

$(OBJS):   depend
$(MAKE) -C cpu/$(CPU) $(if $(REMOTE_BUILD),$@,$(notdir $@))

$(LIBS):	depend $(SUBDIRS)
$(MAKE) -C $(dir $(subst $(obj),,$@))


上述代码显示进入相应的目录后调用子目录中的makefile,生成相应的目标文件。

$(obj)u-boot:	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

$(obj)u-boot.srec:	$(obj)u-boot
$(OBJCOPY) -O srec $< $@

$(obj)u-boot.bin:	$(obj)u-boot
$(OBJCOPY) ${OBJCFLAGS} -O binary $< $@


可见u-boot.bin是由u-boot经过转换而成的,主要考虑u-boot的编译过程。

连接方式由LDFLAGS决定

LDFLAGS = -T u-boot.lds -Ttext 0x33F80000


下面分析u-boot.lds文件

$(obj)u-boot.lds: $(LDSCRIPT)
$(CPP) $(CPPFLAGS) $(LDPPFLAGS) -ansi -D__ASSEMBLY__ -P - <$^ >$@


在源码的根目录中会生成我们需要的u-boot.lds,但是它是从/cpu/arm920t/u-boot.lds拷贝的,中间的过程不知道。

OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
OUTPUT_ARCH(arm)
ENTRY(_start)
SECTIONS
{
. = 0x00000000;

. = ALIGN(4);
.text :
{
cpu/arm920t/start.o (.text)
*(.text)
}

. = ALIGN(4);
.rodata : { *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.rodata*))) }

. = ALIGN(4);
.data : { *(.data) }

. = ALIGN(4);
.got : { *(.got) }

. = .;
__u_boot_cmd_start = .;
.u_boot_cmd : { *(.u_boot_cmd) }
__u_boot_cmd_end = .;

. = ALIGN(4);
__bss_start = .;
.bss (NOLOAD) : { *(.bss) . = ALIGN(4); }
_end = .;
}


OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
OUTPUT_ARCH(arm)


这两句在执行file命令时,会显示

ENTRY(_start)


定义了入口点为_start,它存在在start.S中。

.globl _start
_start: b   start_code


后面的一大段可以看出cpu/arm920t/start.o 在最前面的,所以它最先执行,从该文件的_start标号处开始执行。

U-Boot启动过程源代码分析

1.第一阶段代码分析

第一阶段的启动代码包含两个文件:/cpu/arm920t/start.S/board/samsung/smdk2410/lowlevel_init .S

其中一个是和平台相关的、另一个是和开发板相关的。

(1)硬件设备初始化

以此完成如下设置:将CPU设置为SVC管理模式、关闭WATCHDOG、设置FCLK、HCLK、PCLK的比例(即设置CLKDIVN寄存器)、关闭MMU、CACHE。

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

所谓准备RAM空间,对于s3c2410通过在调用start.S中调用lowlevel_init函数来设置存储控制器。

(3)复制Bootloader的第二阶段代码到RAM空间

(4)设置好栈

(5)清零BBS段、跳转到第二阶段代码的C入口点start_amrboot函数



2.第二阶段代码分析

第二阶段调用的函数位于文件/lib-arm/board.c

大致可以分为3个阶段:

init_sequence中的函数

start_armboot函数后续调用的初始化函数

调用main_loop(),位于/commen/main.c

U_Boot命令格式

U_BOOT_CMD(name,maxargs,rep,cmd,usage,help)


name : 命令的名字,它不是一个字符串(不要用双引号括起来)

maxargs : 最大的参数个数

repeatable : 命令是否可重复,可重复是指运行一个命令后,下次敲回车即可再次运行

command : 对应的函数指针

usage : 简短的使用说明,这是个字符串

help : 较详细的使用说明,这是个字符串

U_BOOT_CMD宏在include/command.h中定义

比如bootm命令,它如下定义:

U_BOOT_CMD
{
bootm, CFG_MAXARGS,1,do_bootm,"string1","string2"
};


宏U_BOOT_CMD扩展后如下所示:

cmd_tbl_t _u_boot_cmd_bootm  __attribute__ ((unused,section(".u_boot_cmd"))) =

{"bootm",CFG_MAXARGS,1,do_bootm,"string1","string2"};


对于每个使用U_BOOT_CMD宏来定义的命令,其实都是在“.u_boot_cmd”段中定义一个cmd_tbl_t结构。

__u_boot_cmd_start = .;
.u_boot_cmd : { *(.u_boot_cmd) }
__u_boot_cmd_end = .;


程序中就是根据命令的名字在段__u_boot_cmd_start和__u_boot_cmd_end 之间找到相应的cmd_tbl_t结构,然后调用它的函数(请参考*command/command.c中的find_cmd函数)

为内核设置启动参数

U_Boot是通过标记列表向内核传递参数的。一般而言,设置内存标志和命令行标记就可以了,在配置文件中include/configs/smdk2410.h 中添加如下两个配置项即可:

#define CONFIG_SETUP_MEMORY_TAGS 1
#define CONFIG_CMDLINE_TAG   1
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: