本帖最后由 wytt 于 2017-8-26 19:33 编辑
很多人可能都认为Bootloader是一个无关紧要的,但其实我个人认为想要更好地学习嵌入式Linux,从写自己第一个简单的Bootloader也可以算是一个简单的切入了。(虽然我自己写得并不怎么样,希望大家多多指导批评) 其实Bootloader说白了就是将操作系统内核复制到内存中运行,它在系统上电时就开始执行,是用来启动内核的。
不同的结构来说起始的地址都不同,对于我学习的ARM结构的CPU一般是从地址0x0000000开始的,Bootloader就存放在这个地址的开始处,这样一上电就可以执行。
Bootloader大多都是两阶段的启动过程
第一阶段使用汇编来实现,它完成一些依赖于CPU体系结构的初始化,并调用第二阶段的代码,所有嵌入式BootLoader最开始一般都是用汇编语言来 写的,因为开发板在上电后没有准备好C程序运行环境,比如说堆栈指针SP没有指到正确的位置,第一部分的初始化利用汇编来写的话还能有效的提高bootloader的运行效率。
第二阶段的代码通常使用C语言来实现,这样可以实现更复杂的功能,而且代码会有更好的可读性和可移植性。
对于写Bootloader第一阶段(start.s)的代码可分为下面几个步骤
(1)关看门狗:如果没有关闭看门狗,开发板会在一定的时间周期内不断进行重启
ldr r0, =0x53000000
mov r1, #0
str r1, [r0]
(2)设置时钟(不同开发板需要设置的时间参数不一样)
ldr r0, =0x4c000014
mov r1, #0x03;
str r1, [r0]
/* MPLLCON = S3C2440_MPLL_200MHZ */
ldr r0, =0x4c000004
ldr r1, =S3C2440_MPLL_200MHZ
str r1, [r0]
(3)初始化SDRAM:为加载Bootloader的第二阶段代码准备RAM空间。
(4)重定位:重定位就是把Bootloader本身的代码从flash复制到它的链接地址去,在第一阶段的重定位代码中,需要调用nand_init对nand flash进行初始化设置,在拷贝代码前还需要判断开发板是为nor启动还是nand启动
ldr sp, =0x34000000
bl nand_init
mov r0, #0
ldr r1, =_start
ldr r2, =__bss_start
sub r2, r2, r1
bl copy_code_to_sdram
bl clean_bss
判断开发板为nor启动还是nand启动(放在Init.c中):
如果将S3C2440配置成从NANDFLASH启动(将开发板的启动开关拔到nand端,此时OM0管脚拉低)S3C2440的Nand控制器会自动把Nandflash中的前4K代码数据搬到内部SRAM中(地址为0x40000000),同时还把这块SRAM地址映射到了0x00000000地址。CPU从0x00000000位置开始运行程序。
如果将S3C2440配置成从Norflash启动(将开发的启动开关拔到nor端,此时OM0管脚拉高),0x00000000就是norflash实际的起始地址,norflash中的程序就从这里开始运行,不涉及到数据拷贝和地址映射。
说白了,就是当开发板为nand启动时,nand flash的前4K数据将会自动拷贝到SDRAM中,然后将SDRAM的地址映射到内存中,也就是一开始写入nand flash前4K中的数据也就是直接写入到了内存中,我们就可以通过判断写入值和内存中0地址的值是否相同来判断是nor启动还是nand启动。代码如下:
int isBootFromNorFlash(void)
{
volatile int *p = (volatile int *)0;
int val;
val = *p;
*p = 0x12345667;
if(*p == 0x12345667)
{
*p = val;
return 0;
}
else
{
return 1;
}
}
(5)执行main函数,跳转到main函数中执行。Bootloader第二段代码(C语言部分)
用C语言可用下面代码来调用内核
在Mian函数中,我们调用了nand_read(0x60000+64, (unsigned char *)0x30008000, 0x200000),
意思是:我们这里是将uImage拷贝到0x30008000处,uImage是压缩的内核,包括一个头还有真正的内核。头是64字节,uImage的地址是0x60000,所以真正的内核的地址是0x60000+64。
thekernel = (void (*)(int, int, unsigned int))0x30008000;
thekernel(0, 362, 0x30000100);
Bootloader与内核的交互是单向的,Bootloader将各类参数传给内核,由于它们不能同时运行,传递的办法只有一个:Bootloader将参数放在某个约定的地点之后再启动内核,内核从这个地方获取参数。
内核一般都是以标记列表的形式来传递启动参数,因此启动参数的设置就显得十分重要了。标记其实就是一种数据结构,而标记列表就是多个标记的组合,标记列表一般以ATAG_CORE开始,以标记ATAG_NONE结束。
标记的数据结构为tag,它是由一个tag_header结构体和一个union组成,它的形式如下:
struct tag_header {
u32 size; //传递类型的大小
u32 tag; //传递参数的类型
};struct tag { struct tag_header hdr;
union {
struct tag_core core;
struct tag_mem_range mem_range;
struct tag_cmdline cmdline;
struct tag_clock clock;
struct tag_ethernet ethernet;
} u;
};
下面就详细来说明如何传递参数(放在Init.c中)
第一步:设置标记ATAG_CORE(标记列表就是从这里开始的)
假设Bootloader与内核约定存放的地址为0x30000100,则标记ATAG_CORE可以这么设置
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);
第二步设置内存标记
假设开发板使用的内存起始地址为0x30000000,大小为0x400000,则内存标记可以这么设置:
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);
第三步:设置命令行标记
命令行就是一个字符串,它用来控制内核的一些行为
比如“noinitrd root=/dev/mtdblock3 init=/linuxrc console=ttySAC0”表示根文件系统在MTD3分区上,系统启动后执行第一个程序是/linuxrc,控制台为ttySAC0,即第一个串口。
int len = strlen (cmdline) + 1;
params->hdr.tag = ATAG_CMDLINE;
params->hdr.size =(sizeof (struct tag_header) + len + 3) >> 2;
strcpy (params->u.cmdline.cmdline, cmdline);
params = tag_next (params);
第四步就是设置标记队列的最后一个标记ATAG_NONE
params->hdr.tag = ATAG_NONE;
params->hdr.size = 0;
在第二阶段C语言的main函数中我们需要做的是:
1:从nand flash 中把内核读入到内核
2:设置参数
3:跳转执行
第一步:从nand flash 中把内核读入到内核
调用nand_read(unsigned int addr, unsigned char *buf, unsigned int len);
addr指的是内核存放的地址
buf指的是你想把内核拷到什么地方
len指的是你想拷多大
注意:写读函数时不同nand flash有不同的设置
写nand_read()函数的步骤(对于不同的nand flash操作不同,看对应的手册)
1、片选
void nand_select(void)
{
NFCONT &= ~(1<<1);
}
2、发出读命令00h
void nand_cmd(unsigned char cmd)
{
volatile int i;
NFCMMD = cmd;
for(i = 0 ; i < 10; i++);
}
3、发出地址(这个地方我也是半懂半不懂的)
void nand_addr(unsigned char addr)
{
volatile int i;
unsigned int col = addr % 2048;
unsigned int page = addr / 2048;
NFADDR =col & 0xff;
for(i = 0 ; i < 10; i++);
NFADDR =(col >> 8) & 0xff;
for(i = 0 ; i < 10; i++);
NFADDR =page & 0xff;
for(i = 0 ; i < 10; i++);
NFADDR =(page >> 8) & 0xff;
for(i = 0 ; i < 10; i++);
NFADDR =(page >> 16) & 0xff;
for(i = 0 ; i < 10; i++);
}
4、发出读命令30h
5、判断状态
判断R/n管脚,高电平表示已经准备就绪。
6、读数据
读数据直接读对应数据寄存器就可以了
7、读完取消选中
因为nand flash一次只能传递八位数据,因为数据需要分成绩几个周期来进行传递,需要不断循环上述过程知道读取完毕。
|