从零开始写一个简单的bootloader(1)
上一篇文章我们介绍了一些初始化的动作,包括:关看门狗、设置系统时钟、初始化SDRAM、初始化NAND flash还有重定位代码等。这篇文章就开始介绍怎么把内核读到内存、怎么设置传递给内核的参数以及跳转执行内核。
我们先给出代码,再对代码做详细的分析
int main(void)
{
void (*thekernel)(int zero, int arch, unsigned int params);
/*0. 设置串口:内核启动时会打印一些信息,
*需要在uboot启动的时候初始化
*/
uart_init();
puts("start to copy kernel\n\r");
/*1. 从NAND FLASH 里把内核读入内存*/
nand_read(0x60000+64, (unsigned char *)0x30008000, 0x200000);
puts("setup the command line \n\r");
/*2. 设置参数*/
setup_start_tag();
setup_memory_tags();
setup_commandline_tag("noinitrd root=/dev/mtdblock3 init=/linuxrc console=ttySAC0");
setup_end_tag();
puts("Boot kernel \n\r");
/*3. 跳转执行*/
thekernel = (void (*)(int, int, unsigned int))0x30008000;
thekernel(0, 362, 0x30000100);
puts("Error \n\r");
/*如果thekernel执行成功,就不会执行到这里*/
return 1;
}
我们可以看到在拷贝内核之前进行了串口的初始化。我们很多时候看到内核在启动的过程中有各种打印信息,就是在bootloader启动的时候做了初始化。而且在一开始就初始化串口,也方便我们等一下做调试打印。
UART是常用的一种传输协议,不难但是很实用,详细的初始化过程我放到了这篇文章:UART协议简述及编程。
然后就是把内核从NAND flash拷贝到内存。这里用到的是前面写的nand_read函数(NAND FLASH的读操作及原理)。这里就不再贴出详细的代码了,主要解释一下函数的参数的意义。
nand_read(0x60000+64, (unsigned char *)0x30008000, 0x200000);
下图是我板子的分区信息,可以看出来kernel的大小为0x200000,存放在NAND FLASH的0x60000的地址。
(1)我们第一个参数是拷贝的源路径,kernel分区的起始地址是0x60000,但是为什么拷贝时要加上64呢?因为我烧写到板子的是UImage。而UImage = 64byte + ZImage组成。而ZImage才是我们真正的内核。所以我们拷贝的内容应该是ZImage,所以拷贝的其实地址也就是0x60000+64。
(2)至于拷贝的目的地址0x30008000,是定死的,JZ2440芯片一般都是这个地址,除非自己手动改过
(3)我们的内核其实大概只有1.8M,但是我们这里拷贝2M的内容也没关系,所以传0x200000
我们bootloader起来后,再跳转到内核去执行,这时候bootloader的工作已经完成了,如果它要告诉内核什么东西(也就是传递什么参数给内核),就要事先把这些内容存放在一个固定的地址,然后再把这个地址告诉内核,叫他去这个地方取参数就好了。
我们这里传递的内容主要是内存的其实地址是多少、内存有多大;根文件系统在什么地方、串口打印从哪个串口输出等等。实现这部分的代码我们是参考uboot的。
我们先给出整个参数tag的地址分布:
用一个结构体来存放参数的内容:
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;
/*
* Acorn specific
*/
struct tag_acorn acorn;
/*
* DC21285 specific
*/
struct tag_memclk memclk;
} u;
};
起始的tag:
void setup_start_tag(void)
{
params = (struct tag *)0x30000100; /*存放tag的起始地址*/
params->hdr.tag = ATAG_CORE; /*0x54410001,标识是起始tag的参数*/
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);
}
内存的信息tag:
void setup_memory_tags(void)
{
params->hdr.tag = ATAG_MEM; /*0x54410002*/
params->hdr.size = tag_size (tag_mem32);
params->u.mem.start = 0x30000000; /*内存其实地址*/
params->u.mem.size = 64*1024*1024; //内存大小为64MB
params = tag_next (params);
}
根文件系统信息:
void setup_commandline_tag(char *cmdline)
{
char *p;
if (!cmdline)
return;
/* eat leading white space */
for (p = cmdline; *p == ' '; p++);
/* skip non-existent command lines so the kernel will still
* use its default command line.
*/
if (*p == '\0')
return;
params->hdr.tag = ATAG_CMDLINE; /*0x54410009*/
params->hdr.size =
(sizeof (struct tag_header) + strlen (p) + 3) >> 2; //加3是为了向上取整
strcpy (params->u.cmdline.cmdline, p);
params = tag_next (params);
}
tag的结束:
void setup_end_tag(void)
{
params->hdr.tag = ATAG_NONE; /*0x00000000*/
params->hdr.size = 0;
}
代码都一目了然比较简单,这里不再细讲。代码中有个小技巧就是tag_next()和tag_size()函数,tag_next其实就是将params指针移动每个结构体的大小这么多。而tag_size中为什么向右移2(实际是除以4),是因为系统是32位的,每一格4个字节,所以指针移位操作都是4字节为单位的操作。
#define tag_next(t) ((struct tag *)((u32 *)(t) + (t)->hdr.size))
#define tag_size(type) ((sizeof(struct tag_header) + sizeof(struct type)) >> 2)
这一步就更加简单了,我们都知道函数的名字其实就是一个地址值而已,我们前面已经把内核拷贝到内存0x3000800的地方,只要将这个地址值赋给函数指针变量thekernel,再执行就可以了,并传递相应的参数,比如tag的起始地址。
thekernel = (void (*)(int, int, unsigned int))0x30008000;
thekernel(0, 362, 0x30000100);
到这里,我们从零写一个bootloader的过程就介绍到这里,有很多地方写的很粗糙,不过旨在能把整个流程介绍清楚,并给自己的学习做一个总结,方便日后回忆。