当系统首次引导时,或系统被重置时,处理器会执行一个位于 Flash/ROM 中的已知位置处的代码,Bootloader 就是这第一段代码。它主要用来初始化处理器及外设,建立内存空间的映射图,然后调用Linux 内核。Linux 内核在完成系统的初始化之后需要挂载某个文件系统作为根文件系统(RootFilesystem),然后加载必要的内核模块,启动应用程序。(一个嵌入式 Linux 系统从软件角度看可以分为四个部分:引导加载程序(Bootloader),Linux 内核,文件系统,应用程序。)
Bootloader多数有两个阶段的启动过程:
Stage1 汇编
- 设置CPU为SVC模式,关闭MMU和DCache(ICache可以打开,指令cache,DCACHE需要mmu支持)。
- 硬件设备初始化:关闭看门狗,关中断,设置CPU始终频率,RAM初始化。
- 为第二阶段代码准备RAM准备空间,加载第二阶段代码。
- 设置堆栈指针 sp。
- 清BSS段,调用C函数,进入第二阶段。
Stage2 C语言
- 初始化本阶段要使用到的硬件设备(led uart 等)。
- 检测系统的内存映射。
- 加载内核映像和文件系统映像。
- 设置内核的启动参数。
第二阶段主要代码:
start_kernel
setup_arch(&command_line);
setup_command_line(command_line); //处理uboot传递进来的启动参数的(处理TAG)
unknown_bootoption
obsolete_checksetup //obsolete_checksetup从__setup_start到 __setup_end,调用用非early标识的函数
parse_early_param
do_early_param //do_early_param从__setup_start到 __setup_end,调用用early标识的函数(但因为__setup_param(str, fn, fn, 0)中early赋值为0,所以不在这里调用),所以我们主要用obsolete_checksetup
rest_init;
kernel_init
prepare_namespace
mount_root //这在后面我们会提到。mount_root是挂载根文件系统,因为Linux上的应用程序最终要在根文件系统上运行。
init_post //最后是init_post中运行应用程序。
bootcmd=nand read.jffs2 0x30007FC0 kernel; bootm 0x30007FC0
bootargs=noinitrd root=/dev/mtdblock3 init=/linuxrc console=ttySAC0
上面是uboot启动时打印的环境变量。其中我们能够看到根文件系统挂载到第4个分区:root=/dev/mtdblock3 (从0分区开始)。上面我们提到过,setup_arch(&command_line)和setup_command_line(command_line)就是用来处理uboot传递进来的启动参数的(处理TAG)
uImage由64字节头部和真正内核构成,头部有in_load(加载地址)和in_ep(入口地址),加载地址由64+0x30007FC0 =0x30008000。入口地址为程序运行地址。
我们通过查看prepare_namespace可以看到一个saved_root_name。查找saved_root_name,发现在Do_mounts.c文件中有对它的调用:
static int __init root_dev_setup(char *line)
{
strlcpy(saved_root_name, line, sizeof(saved_root_name));
return 1;
}
__setup("root=", root_dev_setup); //传入一个字符串,一个函数
Dir:init.h
#define __setup(str, fn) \
__setup_param(str, fn, fn, 0)
#define __setup_param(str, unique_id, fn, early) \
static char __setup_str_##unique_id[] __initdata = str; \
static struct obs_kernel_param __setup_##unique_id \
__attribute_used__ \
__attribute__((__section__(".init.setup"))) \
__attribute__((aligned((sizeof(long))))) \
= { __setup_str_##unique_id, fn, early }
//但是在Flash里没有分区,只能和uboot一样,将分区在代码里写死。一般在启动Linux的时候,Linux会自动打印出分区的信息
static struct mtd_partition smdk_default_nand_part[] = {
[0] = {
.name = "bootloader",
.size = 0x00040000,
.offset = 0,
},
[1] = {
.name = "params",
.offset = MTDPART_OFS_APPEND,
.size = 0x00020000,
},
[2] = {
.name = "kernel",
.offset = MTDPART_OFS_APPEND,
.size = 0x00200000,
},
[3] = {
.name = "root",
.offset = MTDPART_OFS_APPEND,
.size = MTDPART_SIZ_FULL,
}
};
在处理完uboot传递的参数,进行CPU和单板的校验,挂载根文件系统等一系列操作后,最终内核执行init_post()中的应用程序。
1.初始时钟
初始化内核时钟,主时钟,各个外设的时钟。2.关闭看门狗
看门狗是用来监控应用程序的异常跑飞而复位CPU,在初始化阶段,由于没有“喂狗”这一动作,有可能导致CPU不断复位,因此,首先会关闭看门狗,初始化完,再开启。3.建立中断向量表
中断向量表,中断源的识别标志,可用来形成相应的中断服务程序的入口地址,或者中断服务程序入口地址的偏移量和段基值。CPU利用中断向量表转入中断服务程序处理相关事务。4.初始化堆栈寄存器
堆栈的作用一个就是保存现场(上下文),如函数调用或者中断发送时,将当前执行地址压栈,调用完成再返回此处执行程序。另一个作用就是保存参数,如临时变量。因此,在启动阶段需初始化堆栈寄存器、堆栈的大小、起始地址等。5.内存初始化
选择内部或者外部RAM。CortexM3有3种启动方式
1、 BOOT1=1 BOOT0=1 ,中断向量表定位于SRAM区,即起始地址为0x2000000,同时复位后PC指针位于0x2000000处。
2、 BOOT1=x BOOT0=0,中断向量表定位于FLASH区,即起始地址为0x8000000,同时复位后PC指针位于0x8000000处。
3、 BOOT1=0 BOOT0=1 ,中断向量表定位于内置Bootloader区,此时可通过串口下载程序的二进制文件到flash区。
interrupt vector table
即为中断向量表0x0000 0000
地址处取出MSP的初始值0x0000 0004
地址处,即中断向量表中第一个元素,复位向量处取得PC指针初始值,然后执行用户代码0x0000 0004
地址单元,放的就是用户代码首地址