一、BootLoader
1.1 什么是BootLoader
在系统上电后,需要一段程序来初始化:
- 初始化异常向量表,进入SVC模式,关中断;
- 关闭MMU和cache;
- 关闭开门狗;
- 初始换系统时钟;
- 初始化内存;
- 重定位,BootLoader可能大于4K,将代码从Nand Flash复制到内存中;
- 跳转到main;
没有一个BootLoader完全支持所有的CPU,所有我们想要使用BootLoader一般情况下需要自己更改,我们可以增强BootLoader的功能,让他具有网络功能,可以通过NFS远程下载linux内核和根文件系统到Nand Flash。
-
启动加载模式: 这种模式也称为“自主“模式。也就是BootLoader从目标机上的某个固态存储器设备将操作系统加载到RAM中运行,整个过程中并没有用户介入,这种模式是在嵌入式产品发布里的通用模式。
-
下载模式:在这种模式里,目标机的BootLoader将通过串口连接或者网络连接等通信手段从主机下载文件,例如:下载内核映像和根文件系统映像等。从主机上下载的文件通常首先被BootLoader保存在目标机的RAM中,然后再被BootLoader写到目标机的 Flash类的固态存储设备中,BootLoader的这种模式是在开发时使用的,工作于这种模式的BootLoader通常都会向它的终端用户提供一个简单的命令行接口。
在嵌入式Linux系统中从软件的角度通常可以分为4个层次:
-
引导加载程序:包括固化在固化中的boot程序(可选),和BootLoader两大部分。有些CPU在运行BootLoader之前运行一段固化的程序。比如x86结构的CPU就是先运行BIOS中的固件,然后才运行硬盘的第一个分区的BootLoader。在大多数的嵌入式系统中并没有固件,BootLoader是上电后第一个执行的程序。
-
Linux内核:嵌入式定制的内核以及启动参数,启动参数可以是BootLoader传递给内核的,也可以是内核默认的。
-
文件系统:包含根文件系统和建立于Flash内存设备上的文件系统。里面包含了Linux系统能够运行所必要的应用程序和库文件等。比如可以给用户提供操作于Linux的控制的shell程序。
-
用户应用程序:特定于用户的应用程序,他们也存储在文件系统中,有时在用户应用程序和内核层之间还可以包括一个嵌入式图形用户界面。
1.2 BootLoader启动的两个阶段
从固态存储设备上启动BootLoader大多都是分为两个阶段,第一阶段使用汇编来完成。它完成一些依赖于CPU体系架构的初始化,并调用第二阶段的代码,第二阶段则使用C原因实现,这样可以实现更多的复杂功能,而且代码会有更好的可读性和可移植性。
这里我们以Mini2440为例,第一阶段
- 硬件初始化,主要包括初始化异常向量表,进入SVC模式,关闭所有中断,关闭MMU和cache,关闭看门狗,系统时钟初始化;
- 初始化内存,Nand Flash,为加载BootLoader第二阶段的代码到RAM中做准备;
- 从Nand Flash复制BootLoader第二阶段的代码到RAM;
- 设置堆栈,清.bss段,搭建C语言编程环境 ;
- 跳转到第二阶段代码处。
第二阶段
- 初始化串口0,一方面方便我们的调试,另一方面也为内核启动时打印信息做好初始化;
- 检测系统内存映射,将内核映像和根文件系统映像从Nand Flash加载到RAM中;
- 为内核设置启动参数(bootloader与内核约定好地址,存放一些内核启动的参数,供内核启动使用);
- 调用内核,将内核放在适当的内存地址,直接跳转到内核入口点,调用内核之前要满足以下条件;
1.CPU寄存器的设置: R0=0, R1=机器类型IDA;ARM结构的CPU,机器类型在linux/arch/arm/tools/mach-types,R2=启动参数标记列表在RAM中的起始地址;
2.CPU工作模式:必须禁止中断(IRQ,FIQ),CPU必须为SVC模式;
3.Cache和MMU设置:MMU必须关闭,I cache可以打开也可以关闭,D cache必须关闭;
二、BootLoader第一阶段实现
2.1 初始化异常向量表
.text .global _start _start: b reset /* https://blog.csdn.net/shenlong1356/article/details/104602535 */ /* 没有使用伪指令ldr pc,=undefined_instruction,位置有关指令,获取标号地址, 该地址与链接地址有关 */ /* 因为该指令会在内存开辟一空间,把undefined_instruction的地址(这个地址取决于链接地址)存放在内存空间中,然后ldr从内存[pc,#xx]取值赋值给pc */ /* ldr pc,_undefined_instruction,位置无关指令 获取标号的值 */ ldr pc,_undefined_instruction /* 从内存取und异常函数地址到PC */ ldr pc,_software_instruction /* 从内存取swi异常函数地址到PC */ ldr pc,_prefetch_abort /* 从内存取预取指令异常异常函数地址到PC */ ldr pc,_data_abort /* 从内存取访问内存异常函数地址到PC */ ldr pc,_not_used /* 保留 */ ldr pc,_irq /* 从内存取irq异常函数地址到PC */ ldr pc,_fiq /* 从内存取fiq异常函数地址到PC */ /* 未定义异常处理程序地址 */ _undefined_instruction: .word undefined_instruction /* 取undefined_instruction处理函数的地址,保存到该处 */ /* 软中断异常处理程序地址 */ _software_instruction: .word software_instruction /* 预取指定终止处理程序地址 */ _prefetch_abort: .word prefetch_abort /* 数据访问终止处理程序地址 */ _data_abort: .word data_abort /* 保留 */ _not_used: .word not_used /* IRQ正常中断请求处理程序地址 */ _irq: nop /* FIQ快速中断处理程序地址 */ _fiq: nop /* 未定义异常处理程序 */ undefined_instruction: nop /* 软中断异常处理程序 */ software_instruction: nop /* 预取指定终止处理程序 */ prefetch_abort: nop /* 数据访问终止处理程序 */ data_abort: nop /* 保留 */ not_used: nop
2.2 开启SVC模式,关闭fiq,irq中断
set_svcmode: mrs r0,cpsr /* r0 = cpsr */ bic r0,r0,#0x1f /* M[4:0]清0 */ orr r0,r0,#0xd3 /* 设置为SVC模式,M[4:0] 10011, 并关闭fiq,irq中断 */ msr cpsr,r0 /* cpsr = r0 */ mov pc,lr /* bl指令将下一条指令地址复制到了lr,子程序返回 */
2.3 关闭所有中断
.EQU INTMASK, 0x4a000008 /* 中断屏蔽寄存器 */ disable_interrupt: mvn r1,#0x00 /* 取反赋值 r1 = 0xffffffff */ ldr r0,=INTMASK str r1,[r0] /* INTMASK每一位均写1 屏蔽中断 */ mov pc,lr /* bl指令将下一条指令地址复制到了lr,子程序返回 */
2.4 关闭看门狗
.EQU WTCON, 0x53000000 /* 看门狗控制寄存器地址 #define等价于标准汇编里的EQU 用来定义常量 */ disable_watchdog: ldr r0,=WTCON /* 伪指令加载WTCON值到r0 */ mov r1,#0x00 str r1,[r0] /* 把[WTCON]内存单元清零 */ mov pc,lr
2.5 系统时钟初始化
/* 初始化系统时钟 FCLK = 400MHz,HCLK = 100MHz, PCLK = 50MHz, UPLL=48MHz */ .EQU LOCKTIME, 0x4c000000 .EQU MPLLCON, 0x4c000004 .EQU UPLLCON, 0x4c000008 .EQU CLKDIVN, 0x4c000014 .EQU M_MDIV, 92 /* Fin=12M UPLL=400M */ .EQU M_PDIV, 1 .EQU M_SDIV, 1 .EQU U_MDIV, 56 /* Fin=12M UPLL=48M */ .EQU U_PDIV, 2 .EQU U_SDIV, 2 .EQU DIVN_UPLL, 0 /* FCLK:HCLK:PCLK=1:4:8 */ .EQU HDIVN, 2 .EQU PDIVN, 1 system_clock_init: /* 设置Lock Time */ ldr r0,=LOCKTIME ldr r1,=0xffffffff str r1,[r0] /* 设置分频系数 */ ldr r0,=CLKDIVN ldr r1,=((DIVN_UPLL<<3) | HDIVN <<1 | PDIVN) str r1,[r0] /* CPU改为异步总线模式 */ mrc p15,0,r1,c1,c0,0 orr r1,r1,#0xC0000000 mcr p15,0,r1,c1,c0,0 /* 设置UPLL */ ldr r0,=UPLLCON ldr r1,=((U_MDIV<<12) | (U_PDIV<<4) | U_SDIV) str r1, [r0] nop nop nop nop nop nop nop /* 设置MPLL */ ldr r0,=MPLLCON ldr r1,=((M_MDIV << 12) | (M_PDIV << 4) | M_SDIV) str r1,[r0] mov pc,lr
2.6 初始化SDRAM
/* SDRAM初始化 */ .text .global memory_init #define BWSCON 0x48000000 /* 总线宽度和等待控制寄存器 0x00*/ #define BANKCON0 0x48000004 /* Bank0 控制寄存器 0x0700 */ #define BANKCON1 0x48000008 /* Bank1 控制寄存器 0x0700 */ #define BANKCON2 0x4800000C /* Bank2 控制寄存器 0x0700 */ #define BANKCON3 0x48000010 /* Bank3 控制寄存器 0x0700 */ #define BANKCON4 0x48000014 /* Bank4 控制寄存器 0x0700 */ #define BANKCON5 0x48000018 /* Bank5 控制寄存器 0x0700 */ #define BANKCON6 0x4800001C /* Bank6 控制寄存器 0x0700 */ #define BANKCON7 0x48000020 /* Bank7 控制寄存器 0x0700 */ #define REFRESH 0x48000024 /* SDRAM 刷新控制寄存器 0xAC0000 */ #define BANKSIZE 0x48000028 /* 可变Bank大小寄存器 0x00 */ #define MRSRB6 0x4800002C /* 模式寄存器组寄存器Bank6 */ #define MRSRB7 0x48000030 /* 模式寄存器组寄存器Bank7 */ memory_init: /* 初始化BWSCON */ ldr r0,=BWSCON ldr r1,=0x02000000 str r1,[r0] /* 初始化BANKCON0 */ ldr r1,=0x00000700 str r1,[r0,#0x04] /* 初始化BANKCON1 */ ldr r1,=0x00000700 str r1,[r0,#0x08] /* 初始化BANKCON2 */ ldr r1,=0x00000700 str r1,[r0,#0x0C] /* 初始化BANKCON3 */ ldr r1,=0x00000700 str r1,[r0,#0x10] /* 初始化BANKCON4 */ ldr r1,=0x00000700 str r1,[r0,#0x14] /* 初始化BANKCON5 */ ldr r1,=0x00000700 str r1,[r0,#0x18] /* 初始化BANKCON6 */ ldr r1,=0x00018005 str r1,[r0,#0x1C] /* 初始化BANKCON7 */ ldr r1,=0x00018005 str r1,[r0,#0x20] /* 初始化REFRESH */ ldr r1,=0x008C04F5 str r1,[r0,#0x24] /* 初始化BANKSIZE */ ldr r1,=0x000000B1 str r1,[r0,#0x28] /* 初始化MRSRB6 */ ldr r1,=0x00000030 str r1,[r0,#0x2C] /* 初始化MRSRB7 */ ldr r1,=0x00000030 str r1,[r0,#0x30] mov pc,lr /* bl指令将下一条指令地址复制到了lr,子程序返回 */
2.7 初始化栈、bss段初始化
按照我们之前介绍的步骤,这一步应该是复制第二阶段代码到内存,但是我们复制NAND代码到内存采用c语言写的,因此需要先初始化栈,bss段初始化。
/* bss段初始化,该段内存清零 */ bss_init: ldr r0,=__bss_start @ 获取.bss段初始地址 在链接文件中定义 ldr r1,=__end @ 获取.bss段的结束地址 在链接文件中定义 cmp r0,r1 moveq pc,lr @ 相等则返回 clear: mov r2,#0x00 str r2,[r0],#0x04 @ [r0] = 0x00 r0=r0+4 cmp r0,r1 bne clear @不相等跳转 mov pc,lr @ 返回 # 初始化堆栈、必须先初始化SDRAM stack_init: ldr sp,=0x34000000 /* 满栈降序方式,设置svc模式下的栈指针 */ mov pc,lr /* bl指令将下一条指令地址复制到了lr,子程序返回 */
2.8 复制BootLoader第二阶段的代码到SDRAM
/************************************************************************** * * Function : 自动区分是nand启动还是nor启动 * NAND启动,此时内部4k SRAM映射到0x00处,才可以访问该内存 * NOR启动,此时内部2M NOR FLASH映射到地址0x00处,此时无法写入内存,片内SRAM映射到0x40000000地址处 * *************************************************************************/ int is_boot_from_nor_flash(void) { volatile u32 *p = (volatile u32 )0x00; u32 val = *p; /* get value from address 0x00 */ *p = 0x12345678; /* write value to address 0x00 */ /* 写成功, 对应nand启动 */ if(*p == 0x12345678){ *p = val; return 0; } return 1; } /************************************************************************** * * Function : 将代码从nand falsh复制到sdram * *************************************************************************/ void copy_nand_to_sdram(void) { /* 要从lds文件中获得 __code_start, __bss_start 然后从0地址把数据复制到__code_start */ extern int __code_start, __bss_start; volatile u32 *dest = (volatile u32*)&__code_start; volatile u32 *end = (volatile u32*)&__bss_start; volatile u32*src = (volatile u32*)0; u32 len = (u32)(&__bss_start) - (u32)(&__code_start); /* nor falsh boot: nor flash address 0x00 */ if (is_boot_from_nor_flash()) { /* 把nor flash的内容全部copy到sdram */ while (dest < end) { *dest++ = *src++; } } else { // 将nand flash内容复制到SDRAM nand_init(); nand_chip.nand_read_data(dest, (u32)src, len); } }
我们采用NAND启动时,复制NAND FALSH地址0x00处代码到SDRAM __code_start地址处。
__code_start为链接起始地址0x33f80000。
2.9 跳转到第二阶段代码处
/* 跳转到main执行 */
ldr pc,=main /* 跳转到SDRAM */
三、BootLoader第二阶段实现
3.1 初始化串口0
帮内核设置串口: 内核启动的开始部分会从串口打印一些信息,但是内核一开始没有初始化串口所以要完成初始化串口不然后内核启动的时候可能出错。
uart_init();
3.2 复制内核
- 检测系统内存映射,将内核映像和根文件系统映像从Nand Flash加载到RAM中;kernel的大小为0x400000,存放在Nand Flash的0x60000的地址。
- 内核地址0x30008000(内核中配置好的地址):
/* 定义函数指针 */ void (*theKernel)(int zero, int arch, unsigned int params); volatile unsigned int *p = (volatile unsigned int *)0x30008000; /* 从NAND FLASH地址0x60000读取0x400000大小字节的数据到内存0x30008000处,读取数据起始地址要按页对齐 */ uart_putchar('rn'); /* 发送换行符 */ uart_send_str("start copy kernel to SDRAM,length:0x400000.rn"); nand_read_data((u8 *)0x30008000,0x60000, 0x400000); uart_putchar('rn'); /* 发送换行符 */ uart_send_str("kernel has been copyed.rn");
kernel分区的起始地址是0x60000,至于拷贝的目的地址0x30008000,是默认的,S3C2440芯片一般都是这个地址,也可以额自己手动修改为其它的。
我们的内核其实大概只有2M,但是我们这里拷贝4M的内容也没关系,所以传0x400000。
3.3 为内核设置启动参数
内核和BootLoader约定好在某个地址存放启动参数,比如此处是0x30000100开始存放启动参数,BootLoader将参数存放到0x30000100开始的地方,内核启动时会从这里取出参数。
实现代码如下:
/* 内核参数 */ static struct tag *params; static void setup_start_tag () { params = (struct tag *) 0x30000100; 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); } //内存设置 void setup_memory_tags () { params->hdr.tag = ATAG_MEM; params->hdr.size = tag_size (tag_mem32); params->u.mem.start = 0x30000000; params->u.mem.size = 64*1024*1024; params = tag_next (params); } void setup_commandline_tag (char *commandline) { char *p; if (!commandline) return; /* eat leading white space */ for (p = commandline; *p == ' '; p++); /* skip non-existent command lines so the kernel will still * use its default command line. */ if (*p == '