简介:
本文主要介绍为JZ2440写BootLoader来引导内核,启动Linux系统。本文为BootLoader的第二阶段,即介绍如何引导内核和启动内核。
声明:
本文主要是看了韦东山老师的视频后,所写的课程总结。希望对你有所帮助。
BootLoader第二阶段代码:
这里我们还要强调一下BootLoader的目的:系统初始化,加载并引导内核程序。而我们在上面一篇文章:嵌入式Linux——写jz2440BootLoader的第一阶段代码中已经详细的介绍了第一阶段系统的初始化,而在BootLoader的第二阶段主要就是解决下面两个问题,即加载并引导内核。同时我们知道我们在u-boot中会设置一些参数,例如bootargs或者其他的设置。那么我们是通过何种方式让内核知道我们为他传递了参数,同时知道参数是什么。这就需要在内核和BootLoader之间建立一种协议,用这种协议来完成参数的传递。而这种协议我们说的更通俗一点就是一种数据传输的格式即TAG参数。BootLoader将要传递的参数以TAG参数的格式进行编辑,然后传递给内核。设置好参数我们就要跳到内核告诉他参数的位置,并执行内核代码。
通过上面的介绍我们大致可以将第二阶段的代码分为下面几步:
1. 从nand中将内核读到SDRAM中
2. 设置TAG参数
3. 跳转到内核,执行内核代码
而完成上面几步后程序就进入kernel程序中运行了。而这时我们BootLoader的使命就算完成了。同时BootLoader程序也就没有任何作用了。
1. 从nand中将内核读到SDRAM中
下面我们详细讲解上面的几步,来了解我们是如何进入内核函数的。而在写这些之前我希望大家对内核有些了解,因为只有我们了解了下面的接口信息,我们上面的接口程序才好写。而那些内核信息是需要我们了解的,我会在后面的文中提及。下面我们还是看上面的步骤1,同样又是要转移数据,那么就逃脱不了我们数据传输的三要素即,源,目的,长度。既然是将内核从nand读到SDRAM,那么我们的源和目的就有了,即源是nand,而目的为SDRAM,而具体的源与目的的地址我们就要看在nand中和SDRAM中的内存分配情况了。
我们先看在nand中的分配情况,即看源的具体地址。如下图中红色方框所示:
我们将内核的映像文件uImage放到了上图中红色方框圈出空间,在这里开始地址为0x60000,那么我们的源地址本应该是在0x60000这个位置上。但是我们要注意的是在0x60000处存放的是kernel的映像文件uImage,而不是kernel本身。所以我们要从真正kernel的位置拷贝数据。而kernel真正开始的地方是在uImage开始地址后64字节处,即0x00060000+64处。而为什么是64字节,这就要看uImage的文件了,如下图:
从上面我们知道uImage由64字节头部和kernel的组成。而64字节的头部就包括下面的内容:
typedef struct image_header {
uint32_t ih_magic; /* Image Header Magic Number */
uint32_t ih_hcrc; /* Image Header CRC Checksum */
uint32_t ih_time; /* Image Creation Timestamp */
uint32_t ih_size; /* Image Data Size */
uint32_t ih_load; /* Data Load Address */
uint32_t ih_ep; /* Entry Point Address */
uint32_t ih_dcrc; /* Image Data CRC Checksum */
uint8_t ih_os; /* Operating System */
uint8_t ih_arch; /* CPU architecture */
uint8_t ih_type; /* Image Type */
uint8_t ih_comp; /* Compression Type */
uint8_t ih_name[IH_NMLEN]; /* Image Name */
} image_header_t;
其中最重要的是ih_size和ih_load两个参数,第一个是映像文件的大小,而另一个是映像文件的加载地址。这里我们就要引入在上面讨论的数据转移的目的地址了。这个加载地址就是我们的目的地址。而我们这个时候就需要了解一些内核的知识了,因为这里的内核我们在u-boot中可以任意放置,但是在BootLoader中我们必须要放到规定的位置,这是为什么那?是因为在u-boot中有memmove函数来将kernel文件移动到正确的位置。而在BootLoader中 我们并没有这个函数,所以我们只能规规矩矩的将这个内核文件放到内核规定的位置。而内核规定的位置是哪里哪?这就要看内核文件了,我们在内核中有如下代码:
#ifdef CONFIG_CPU_S3C2400
#define PHYS_OFFSET UL(0x0C000000)
#else
#define PHYS_OFFSET UL(0x30000000)
#endif
#define KERNEL_RAM_VADDR (PAGE_OFFSET + TEXT_OFFSET)
#define KERNEL_RAM_PADDR (PHYS_OFFSET + TEXT_OFFSET)
/*
* swapper_pg_dir is the virtual address of the initial page table.
* We place the page tables 16K below KERNEL_RAM_VADDR. Therefore, we must
* make sure that KERNEL_RAM_VADDR is correctly set. Currently, we expect
* the least significant 16 bits to be 0x8000, but we could probably
* relax this restriction to KERNEL_RAM_VADDR >= PAGE_OFFSET + 0x4000.
*/
#if (KERNEL_RAM_VADDR & 0xffff) != 0x8000
#error KERNEL_RAM_VADDR must start at 0xXXXX8000
#endif
而通过上面的信息我们确定了要将内核加载到SDRAM中的0x30008000的地方。而第三个问题加载长度的问题,我们可以读取uImage的头部中ih_size参数来确定映像文件的大小,进而确定要传输数据的大小。但是我们这里就不这么做了,我们直接读取nand中为uImage分配的空间大小就可以了。
所以从nand中将内核读到SDRAM中代码为:
//将内核从nand flash中拷贝到SDRAM中
nand_read(0x00060000+64,0x30007fc0+64,0x200000);
2. 设置TAG参数
我们知道kernel在启动的过程中要读取一些BootLoader为其传递的参数来设置kernel的启动参数,而这些参数都是什么,他们以一种什么样的方式传递到kernel中,这些都是我们要讨论的。我们知道BootLoader为内核传递参数是有一定的规律性的,而不是想怎么传就怎么传,这样就不符合内核的思想了。而内核的思想是模块化,同时也便于移植,所以他就有一定的防范,而这个规范就是TAG参数,BootLoader与内核之间约定用过这种方式来传递参数,即BootLoader通过TAG参数的方式将要传输的数据放到指定的地址中,而内核则通过TAG参数的方式将存放在指定地点中的数据读出。这就体现了TAG的参数的重要性了。
同时为了便于区分和识别,TAG参数也有他的开始TAG和结束TAG,而在开始和结束TAG之间的就是我们要传递的参数了。我在下面将几个重要的TAG参数画出,如下图:
从图中我们可以看出这里主要分为4个TAG,他们分别为start_tag,memory_tag,comandline_tag,end_tag。我们在前面已经说了start_tag和end_tag分别表示了TAG参数的开始和结束,所以这里我们不细分析这两个参数,我只想强调一点的是在start_tag中标明了TAG参数的开始地址。接下来我们看memory_tag和commandline_tag,memory_tag的作用是向内核传递SDRAM的起始地址以及大小,而commandline_tag的作用是向内核传递命令行参数。
在memory_tag中,我们向内核传递了SDRAM的起始地址为0x30000000,SDRAM的大小为0x400000。
在commandline_tag中我们将命令行参数"console=ttySAC0 115200 root=/dev/mtdblock3 rootfstype=jffs2"传递到内核中。
所以设置内核参数的代码为:
//设置TAG参数
setup_start_tag();
setup_memory_tags();
setup_commandline_tag("console=ttySAC0 115200 root=/dev/mtdblock3 rootfstype=jffs2");
setup_end_tag();
struct tag *params;
static void setup_start_tag (void)
{
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);
}
static void setup_memory_tags (void)
{
params->hdr.tag = ATAG_MEM;
params->hdr.size = tag_size (tag_mem32);
params->u.mem.start = 0x30000000;
params->u.mem.size = 0x400000;
params = tag_next (params);
}
int strlen(char *p)
{
int i=0;
while(*p != '\0'){
i++;
p++;
}
}
void strcpy(char *dir,const char *source)
{
int i = 0;
while(source[i] != '\0'){
dir[i] = source[i];
i++;
}
}
static void setup_commandline_tag (char *commandline)
{
int len = strlen (commandline);
params->hdr.tag = ATAG_CMDLINE;
params->hdr.size =
(sizeof (struct tag_header) + len + 3) >> 2;
strcpy (params->u.cmdline.cmdline, commandline);
params = tag_next (params);
}
static void setup_end_tag (void)
{
params->hdr.tag = ATAG_NONE;
params->hdr.size = 0;
}
3. 跳转到内核,执行内核代码
做完上面的准备工作,我们就要正式跳转到内核,去执行内核的代码了。既然我们这里的代码是跳转到内核去执行内核的第一条代码。那么我们就要看看在内核中对我们这个跳转的指令有什么样的要求了,我们找到 arch\arm\kernel\head.S 文件,可以看到这里说明对这个跳转指令的要求:
/*
* Kernel startup entry point.
* ---------------------------
*
* This is normally called from the decompressor code. The requirements
* are: MMU = off, D-cache = off, I-cache = dont care, r0 = 0,
* r1 = machine nr.
*
* This code is mostly position independent, so if you link the kernel at
* 0xc0008000, you call this at __pa(0xc0008000).
*
* See linux/arch/arm/tools/mach-types for the complete list of machine
* numbers for r1.
*
* We're trying to keep crap to a minimum; DO NOT add any machine specific
* crap here - that's what the boot loader (or in extreme, well justified
* circumstances, zImage) is for.
*/
其实这里是说明在内核开启前应该有的硬件环境,如关闭MMU和D-cache,而不用关心I-cache,并且这里r0的值为0,r1的值为机器ID,而关于将内核放到特定的位置,我们在前面已经说了。而最后我们要向内核传递的是TAG参数的首地址。而传完这些我们就可以跳转到内核去执行内核的代码了。但是内核中各个单板对应的机器ID不同,则他们的初始化函数也不尽相同。这里我们以自己的开发板为例来进行说明,我们的开发板为JZ2440,但是我们在 arch\arm\tools\mach-types 文件中找到相关的机器ID为:
s3c2410 ARCH_S3C2410 S3C2410 182
smdk2410 ARCH_SMDK2410 SMDK2410 193
s3c2440 ARCH_S3C2440 S3C2440 362
所以这里我们有三个可选项,但是我们考虑到在内核中2410的完整性会比2440好一些,所以这里我们会选择一个2410的机器ID,最后我们选择了SMDK2410的机器ID。这样我们在初始化的时候也就选择了使用SMDK2410内核初始化的方式来初始化我们的JZ2440,这里内核在初始化机器ID的时候就会走 arm\mach-s3c2410\mach-smdk2410.c中的代码:
MACHINE_START(SMDK2410, "SMDK2410") /* @TODO: request a new identifier and switch
* to SMDK2410 */
/* Maintainer: Jonas Dietsche */
.phys_io = S3C2410_PA_UART,
.io_pg_offst = (((u32)S3C24XX_VA_UART) >> 18) & 0xfffc,
.boot_params = S3C2410_SDRAM_PA + 0x100,
.map_io = smdk2410_map_io,
.init_irq = s3c24xx_init_irq,
.init_machine = smdk2410_init,
.timer = &s3c24xx_timer,
MACHINE_END
进而按着这里的函数来初始化CPU。
好了,这似乎讲的有点跑题了,那么继续回到跳转到内核这个话题。上面我们已经知道了内核的首地址在0x30008000,所以我们的这条跳转的函数指针应该指向这个首地址。同时我们知道要传递0,机器ID和TAG参数首地址这三个参数到内核,所以这个函数指针要有三个参数。而最后我们就要执行跳转指令跳转到内核了。所以跳转到内核的代码为:
void (*theKernel)(int zero, int arch, uint params);
theKernel = (void (*)(int, int, uint))0x30008000;
//跳转到内核地址
theKernel (0, 193, 0x30000100);
初始化串口
我们写到这里本应该算是写完了,因为我们已经可以进入内核去执行内核程序了。但是这里我们需要注意的一点是在内核开始时并没有初始化串口,所以我们要在这里做一些关于串口0初始化的设置。这里我们要设置串口的波特率为115200,同时他有8个数据位,无校验位,1个停止位。而具体关于初始化串口的代码为:
/*
* 初始化UART0
* 115200,8N1,无流控
*/
void uart0_init(void)
{
GPHCON |= 0xa0; // GPH2,GPH3用作TXD0,RXD0
GPHUP = 0x0c; // GPH2,GPH3内部上拉
ULCON0 = 0x03; // 8N1(8个数据位,无较验,1个停止位)
UCON0 = 0x05; // 查询方式,UART时钟源为PCLK
UFCON0 = 0x00; // 不使用FIFO
UMCON0 = 0x00; // 不使用流控
UBRDIV0 = UART_BRD; // 波特率为115200
}
/*
* 发送一个字符
*/
void putc(unsigned char c)
{
/* 等待,直到发送缓冲区中的数据已经全部发送出去 */
while (!(UTRSTAT0 & TXD0READY));
/* 向UTXH0寄存器中写入数据,UART即自动将它发送出去 */
UTXH0 = c;
}
好了,写到这里我们的BootLoader就执行完了,同时如果我们的BootLoader代码正确,那么CPU将会一去不复返了。同时这也就完成了BootLoader的使命了。