从零开始写一个简单的bootloader(2)

从零开始写一个简单的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)

 跳转执行kernel

这一步就更加简单了,我们都知道函数的名字其实就是一个地址值而已,我们前面已经把内核拷贝到内存0x3000800的地方,只要将这个地址值赋给函数指针变量thekernel,再执行就可以了,并传递相应的参数,比如tag的起始地址。

thekernel = (void (*)(int, int, unsigned int))0x30008000;
thekernel(0, 362, 0x30000100);

结尾 

        到这里,我们从零写一个bootloader的过程就介绍到这里,有很多地方写的很粗糙,不过旨在能把整个流程介绍清楚,并给自己的学习做一个总结,方便日后回忆。

 

猜你喜欢

转载自blog.csdn.net/lee_jimmy/article/details/82285942