本文是对《嵌入式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;
struct tag_acorn acorn;
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 _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
BOARD_NAME=""
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
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" ]
then
echo >> config.h
else
> config.h
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
EOF
APPEND=no,所以在include目录下创建config.h
for i in ${TARGETS} ; do
echo "#define CONFIG_MK_${i} 1" >>config.h ;
done
该语句不知道什么意思,但是它没有执行
所以最后会生成/include/config.h,其内容如下:
#define CONFIG_BOARDDIR board/samsung/smdk2410
#include
#include
#include
可以看出我们需要在/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
#define CONFIG_BOARDDIR board/samsung/smdk2410
#include
#include
#include
配置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