要探讨bootloader ,我们首先从全局来看看,嵌入式系统启动流程是怎么样的。大体上一个嵌入式Linux系统从软件角度分析可以分为四个部分:引导加载程序(bootloader),Linux内核,文件系统,应用程序。
当系统首次引导时,或系统被重置时,bootloader 首先被执行(位于Flash/ROM中的已知位置处)的代码。它主要用来初始化处理器及外设,然后调用Linux 内核。Linux 内核在完成系统的初始化之后需要挂载某个文件系统作为根文件系统(RootFilesystem),然后加载必要的内核模块,启动应用程序。这就是嵌入式Linux系统启动过程 Linux 引导的整个过程。
详细分析bootloader
bootloader 是操作系统启动之前运行的一段代码,它严格来说分成两个过程:
boot: 启动系统
初始化硬件设备,建立内存空间映射图,将系统的软硬件环境带到一个合适的状态。
loader:
将操作系统内核文件从存储设备加载到内存中,之后跳转到内核所在的地址运行。
目前市场上大多数的嵌入式系统都是通过uboot,来针对各自的需求进行的订制。但是uboot版本又有各种区别,但是在阅读uboot源码过程中的一些关键点都是一样的,当你进去源码目录后所需要注意的地方都是差不多的。
首先进入代码目录后,我们应当关注的第一个文件就是u-boot.lds。这是一个链接脚本。
进入u-boot.lds 中首先我们看这一句
OUTPUT_FORMAT("elf32-littlearm", "elf3 2-littlearm", "elf32-littlearm")
/*这句话的意思是指定可执行文件的输出格式ELF格式,小端。(什么是大小端,请读者自行学习*/
OUTPUT_ARCH(arm)
/*指定输出可执行文件的平台为ARM*/
ENTRY(_start)
/*指定输出可执行文件的起始代码段为_start*/
SECTIONS
{
/*指定可执行image文件的全局入口点,通常这个地址都放在ROM(flash)0x0位置。必须使编译器知道这个地址,通常都是修改此处来完成*/
. = 0x00000000;/*;从0x0位置开始*/
. = ALIGN(4);/*代码以4字节对齐*/
.text :
{
cpu/arm920t/start.o (.text)
/*代码的第一个代码部分*/
*(.text)
/*下面依次为各个text段函数*/
}
. = ALIGN(4);
/*代码以4字节对齐*/
.rodata : { *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.rodata*))) }
/*指定只读数据段*/
. = ALIGN(4);
/*代码以4字节对齐*/
.data : { *(.data) }
. = ALIGN(4);
/*代码以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);
/*代码以4字节对齐*/
__bss_start = .;
/*把__bss_start赋值为当前位置,即bss段的开始位置*/
.bss (NOLOAD) : { *(.bss) . = ALIGN(4); }
/*指定bss段,告诉加载器不要加载这个段*/
__bss_end = .;
/*把_end赋值为当前位置,即bss段的结束位置*/
上面即是lds 脚本解析。
那么现在我们来关注这句:cpu/arm920t/start.o (.text),首先看boot启动过程中第一阶段的代码,uboot启动的第一阶段代码时用汇编写的,start.o文件是由start.S 生成的,现在我们来看看具体的start.S中做了些什么事情。详细start.S代码讲解请参见:
http://blog.chinaunix.net/uid-22891435-id-380150.html。
此处我们讲解比较重要的几个点,把启动过程的脉络理清楚就行。
cpu/arm920t/start.S
bl lowlevel_init
/*定义在lowlevel_init.S文件中,board/CONCENWIT/arm920t,该函数来初始化底层硬件,具体如何初始化,请跳转到lowlevel_init.S 中阅读源码*/
/*2.系统时钟的初始化*/
bl system_clock_init
/*3.内存控制器的初始化*/
bl mem_ctrl_asm_init
/*4.串口的初始化*/
bl uart_asm_init
/*5.Nand Flash控制器的初始化*/
bl nand_asm_init
/*CPU内部硬件控制器已经能工作
当前u-boot.bin还在Nand Flash中,读Nand Flash ,写内存
将整个u-boot.bin从Nand Flash中拷贝到内存(外接1G)中*/
after_copy:
mmu_on:
/*所有的地址都将是虚拟地址,就要查询MMU的地址转换表,得到物理地址
建立栈*/
clear_bss:
/*清空bss段*/
ldr pc, _start_armboot (伪指令)// 跳转到 start_armboot第二阶段的代码函数入口(C代码)
_start_armboot:
.word start_armboot
整个过程我们可以用以下的图来归纳:
总结下,start.S 完成的事情如下:
start.S
1.设置CPU的状态,关闭中断
2.lowlevel_init(关看门狗,初始化系统时钟,内存控制,串口,Nand Flash控制器)
3.将整个u-boot.bin(C3E00000)拷贝到SDRAM
4.将MMU打开
5.建栈
6.清空BSS段
7.ldr pc, _start_armboot(将PC指向start_armboot函数执行)。
再看第二阶段start_armboot (lib_arm/board.c)代码,它继续通过for循环调用若干个初始化函数来继续进行初始化工作。
主要过程代码如下:
5.board.c / start_armboot函数
typedef int (init_fnc_t) (void);
//定义一个名为init_fnc_t函数类型,该函数返回值为int,参数void
init_fnc_t **init_fnc_ptr;
for (init_fnc_ptr = init_sequence; *init_fnc_ptr; ++init_fnc_ptr) {
if ((*init_fnc_ptr)() != 0) {
hang ();
}
}
typedef struct global_data {
//描述全局信息的结构,包括开发板
bd_t *bd;
unsigned long flags;
unsigned long baudrate;
unsigned long have_console; /* serial_init() was called */
unsigned long reloc_off; /* Relocation Offset */
unsigned long env_addr; /* Address of Environment struct */
unsigned long env_valid; /* Checksum of Environment valid? */
unsigned long fb_base; /* base address of frame buffer */
#ifdef CONFIG_VFD
unsigned char vfd_type; /* display type */
#endif
#if 0
unsigned long cpu_clk; /* CPU clock in Hz! */
unsigned long bus_clk;
phys_size_t ram_size; /* RAM size */
unsigned long reset_status; /* reset status register at boot */
#endif
void **jt; /* jump table */
} gd_t;
typedef struct bd_info {
//描述开发板的相关信息
int bi_baudrate; /* serial console baudrate */
unsigned long bi_ip_addr; /* IP Address */
unsigned char bi_enetaddr[6]; /* Ethernet adress */
struct environment_s *bi_env;
ulong bi_arch_number; /* unique id for this board */
ulong bi_boot_params; /* where this board expects params */
struct /* RAM configuration */
{
ulong start;
ulong size;
} bi_dram[CONFIG_NR_DRAM_BANKS];
#ifdef CONFIG_HAS_ETH1
/* second onboard ethernet port */
unsigned char bi_enet1addr[6];
#endif
} bd_t;
#define DECLARE_GLOBAL_DATA_PTR
register volatile gd_t *gd asm ("r8")
这是一个寄存器变量,他得地址被存放CPU 内部寄存器r8
目的是能快速的访问里面的内容
内容还是存放内存中
gd_t
bd_t
arm-linux-gcc -g -Os -fno-strict-aliasing -fno-common -ffixed-r8
该参数不要使用r8寄存器
int board_init(void)
{
DECLARE_GLOBAL_DATA_PTR;
//声明宏
#ifdef CONFIG_DRIVER_SMC911X
smc9115_pre_init();
#endif
//由于CONFIG_DRIVER_SMC911X没有被定义,smc9115_pre_init不会执行,实际上,可以通过修改宏的定义,实现移植代码的目的
#ifdef CONFIG_DRIVER_DM9000
dm9000_pre_init();
#endif
//初始化DM9000网卡
gd->bd->bi_arch_number = MACH_TYPE;(2456)
//2456是TPAD的ID号,不同开发板有不同的ID号
gd->bd->bi_boot_params = (PHYS_SDRAM_1+0x100);
= (0x20000100);
//u-boot启动内核时,会给内核传递启动参数,将启动参数所存放的地址初始化给gd->bd->bi_boot_params
return 0;
}
int dram_init(void)
{
DECLARE_GLOBAL_DATA_PTR;
gd->bd->bi_dram[0].start = 0x20000000;
gd->bd->bi_dram[0].size = 0x20000000;
#if defined(PHYS_SDRAM_2)
gd->bd->bi_dram[1].start = 0x40000000;
gd->bd->bi_dram[1].size = 0x20000000;
#endif
return 0;
}
整个过程的流程图如下:
结合上面分析的uboot的启动过程中的主要代码,可以总结详细启动流程如下:
u-boot启动过程:
1.start.S–>前8K代码在CPU内部的IRAM执行,是由IROM代码拷贝过去
2.lowlevel_init.S->CPU内部硬件控制器初始化
关闭看门狗,初始化系统时钟,初始化内存控制器,串口,Nand Flash
3.start.S->
将整个u-boot.bin拷贝到内存中
开启MMU,建立栈,清空BSS段
使用伪指令ldr pc, _start_armboot跳转至u-boot第二阶段C代码执行
4.start_armboot函数,执行了初始化列表
1.cpu_init
2.board_init(网卡初始化,板子的ID号,参数存放的地址)
3…..
4.dram_init(将外接内存的具体信息进行初始化,起始地址,大小)
5.main_loop函数
1.获取bootdelay 和 bootcmd环境变量
2.在bootdelay秒内:
整个uboot,启动流程完成了,下一篇将接着介绍有关的kernel启动流程和结构知识。