My Linux Bootloader设计详解

序言

    此案例的Bootloader是参考韦东山老师《从零写简单U_BOOT》视频课程设计的,其中有90%以上的相似度。但由于视频的网盘链接失效了,所以没办法在此分享给读者。但是有兴趣的读者,可以通过百问网的客服了解相关的情况。

    本Bootloader设计重点是带领大家理解bootloader如何实现启动内核的过程,并没有实现从外部获取镜像文件的功能。当然这部分的功能,u-boot上本来就有。
    相关的硬件:JZ2440(韦东山老师的百问科技出品,强烈推荐给嵌入式Linux初学者)
    相关的内核版本:Linux-2.6.22

存储分区

NandFlash存储分区

图1 nandflash存储分区

    在本案例中,My Bootloader存放在nand flash的 0x00000000 ~ 0x0003FFFF 区间中,大小为256K,params 可忽略,至于kernel 和 root 都是假定已经被烧录到指定的地址上面去了。

bootloader与内核运行地址

    在此我要强调的一点,在JZ2240,bootloader和内核都是运行在内存上。我现在觉得其实也不一定必须是内存,目前看来只是要满足两个条件,一个是读取速度快,二是必须映射在地址空间上。而明显NAND Flash并不映射在MPU的地址空间上,读取速度也没nor flash快。所以NAND FLASH肯定不是作为bootloader的运行地址。
    而NOR FLASH行不行?我觉得可能可行,只要内核和bootloader不会对NOR FLASH本身进行写入操作。不过我暂时没发现有人会用NorFlash运行内核,运行bootloader铁定是没什么问题。
    Bootloader的运行地址为0x33F80000,而JZ2440的内存地址范围是0x300000000 ~ 0x3400000,因为0x33F8000~0x3400000都是bootloader代码段的合法区间,共512字节大小。
    如下图是,bootloader的lds文件,“. = 0x33f80000;”指定了运行地址。

SECTIONS {
    . = 0x33f80000;
    .text : { *(.text) }
    
 . = ALIGN(4);
    .rodata :  {*(.rodata*)} 
 
 . = ALIGN(4);
 .data   :  { *(.data) }
 
 . = ALIGN(4);
    __bss_start = .;
    .bss  : { *(.bss)  *(COMMON) }
    __bss_end = .;

    内核的运行地址是0x30008000,但是在此我不贴出内核的链接文件。因为不重要。我们的焦点应该在uImage格式的内核镜像上,如下图所示。


    uImage 由 64字节的Header 和 真正的内核镜像组成。而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_magic ih_hcrc ih_size 都是为了校验内核镜像内容正确的信息,而ih_load和ih_ep分别是内核的运行地址,和启动内核的入口地址函数。通俗的说就是,我把内核镜像拷贝到ih_load 上后,在调用在ih_ep地址上的函数,即可启动内核。 因此我们并不需要了解内核的链接过程,只需要通过uImage文件头即可以知道如何启动内核。

Boot代码流程1:硬件初始化

    关于硬件初始化的寄存器说明,就不在这里详细说了,S3C2440已经是烂大街的玩法和教程。但我只在此处强调的是,1. 硬件初始化代码只能用汇编写,因为此时sp寄存器还没初始化,不能使用C代码(但也不是说一定不行,反正只要在4K代码内完成代码重定位,你可以在初始化一开始就设置sp=0x1000,然后就可调用C语言)2. 关于必要初始化的硬件有哪里?看门狗,时钟,SDRAM和NAND Flash。 至于为什么一定要有SDRAM 和 NAND Flash,请看代码重定向的说明。
    如下图所示,为硬件初始化代码

/*1. 关看门狗 */
 /*WTCON = 0;*/ 
 ldr r0, =0x53000000
 mov r1, #0
 str r1, [r0]
/*2. 设置时钟*/
 /*CLKDIVN  = 0x03; */
 ldr r0, =0x4c000014
 mov r1, #0x03
 str r1, [r0]
/* 如果HDIVN非0,CPU的总线模式应该从“fast bus mode”变为“asynchronous bus mode” */
 mrc p15, 0, r1, c1, c0, 0  /* 读出控制寄存器 */ 
 orr r1, r1, #0xc0000000   /* 设置为“asynchronous bus mode” */
 mcr p15, 0, r1, c1, c0, 0  /* 写入控制寄存器 */
 /*MPLLCON = S3C2440_MPLL_200MHZ*/
 ldr r0, =0x4c000004
 ldr r1, =S3C2440_MPLL_200MHZ
 str r1, [r0]
/*3. 初始化SDRAM*/
 ldr r0, =MEM_CTL_BASE
 adr r1, sdram_config     /* sdram_config的当前地址 */
 add r3, r0, #(13*4)
/*4. 重定位: 把bootloader本身的代码从flash复制到它的链接地址去*/
 ldr sp, =0x34000000
 bl nand_init
Boot代码流程2:代码重定向

    此处,我要说明一下S3C2440的启动特性。分NAND FLASH启动和NOR FLASH启动两种。
当从NAND启动时
    mpu会自动把NAND FLASH的前4K的内容,拷贝到SRAM里面,并把SRAM的地址映射到 0x00000000,也就是上电复位执行地址。

当从Nor flash启动时
    NOR FLASH直接映射到0x00000000地址,也就是说复位上电执行的代码,是直接从NOR FLASH上读出来的。
    在此处可能有人会有疑问,那Bootloader的运行地址不是应该在0x33F80000么,为什么放在0x00000000地址上也能执行?
    那是因为在代码重定位以前的所有代码都是位置无关的代码,也就是无论你放在哪个位置都是能执行的。比如:bl nand_init 这一句汇编,bl 指令它使用的是相对位置,去找到函数nand_init并跳转过去的。
    又如下图所示,在函数nand_init (必须在4K代码中) 中,通篇没有调用其他函数,都是直接访问寄存器赋值,因此也是与位置无关的代码。而事实上也是因为C语言本身无法控制其函数跳转使用的是相对地址还是绝对地址,所以代码重定位以前的代码一般都是用汇编写的。

void nand_init(void)
{
#define TACLS   0
#define TWRPH0  1
#define TWRPH1  0
 /* 设置时序 */
 NFCONF = (TACLS<<12)|(TWRPH0<<8)|(TWRPH1<<4);
 /* 使能NAND Flash控制器, 初始化ECC, 禁止片选 */
 NFCONT = (1<<4)|(1<<1)|(1<<0);
}

    正如前面说的,NAND FLASH启动时,只有4K代码会被读出来执行,所以必须有代码重定位这个动作。所谓的代码重定位,也就是把在NAND FLASH中的所有Bootloader代码全部拷贝到SDRAM的0x33F8000上,然后用绝对地址跳转的方式,跳到main函数执行。
    如下图所示,函数copy_code_to_sdram就是代码重定位实现函数,“ ldr pc, =main” 就是把main函数的绝对地址赋给程序计数器(PC),以实现跳转功能。

 mov r0, #0
 ldr r1, =_start
 ldr r2, =__bss_start
 sub r2, r2, r1
 
 bl copy_code_to_sdram
 bl clean_bss
/* 5. 执行main */
 ldr lr, =halt
 ldr pc, =main

如下图所示,函数copy_code_to_sdram是直接调用函数nand_read,拷贝bootloader代码的


int copy_code_to_sdram(unsigned char *src, unsigned char * dest, unsigned int len)
{
 int i = 0;
 /*如果是NOR启动*/
 if(isBootFormNorFlash())
 {
  while(i<len)
  {
   dest[i] = src[i];
   i++;
  }
 }
 else
 {
  //nand_init();
  nand_read((unsigned int)src, dest, len);
 }
 return 0;
}

    这时候或许有人会说,我怎么知道bootloader到底有多大?
    此时,我会说,如果你嫌麻烦,就直接把bootloader整个分区拷贝过去。但在本案例中,我们使用了__bss_start - _start的方式得出bootloader代码段的大小。_start函数是bootloader放在最开始的代码。__bss_start 是 bss段的起始地址**(可参考章节运行地址贴出来的链接文件)**,两者相减刚好就是bootloader代码段的大小。

Boot代码流程3:拷贝内核
int main(void)
{
	#define MACH_TYPE_S3C2440              362
	void (*theKernel)(int zero, int arch, unsigned int params);
	image_header_t *hdr = (image_header_t *)0x30007FC0;
	unsigned char * ptr_8;	
	unsigned long entry_point;
	volatile unsigned int *p = (unsigned int *)0x30008000;

	/*0.设置串口:内核启动时会从串口打印信息,但是内核一开始没有初始化串口*/
	 uart0_init();
 
	 puts("Copy kernel form nand\n\r");
	 /*1. 从Nand FLASH里把内核读入内存*/
 	nand_read(0x60000, (unsigned char *)0x30007FC0, 0x200000);
 	ptr_8 = (unsigned char *)(&(hdr->ih_ep));
 	entry_point = 0;
 	entry_point += (((unsigned long)(ptr_8[0])) << 24);
 	entry_point += (((unsigned long)(ptr_8[1])) << 16);
 	entry_point += (((unsigned long)(ptr_8[2])) << 8);
 	entry_point += (((unsigned long)(ptr_8[3])) << 0);
	/*2. 设置启动参数*/
	puts("Set Boot param\n\r");
	setup_start_tag ();
	setup_memory_tags ();
	setup_commandline_tag ();
	setup_end_tag ();
	puts("Boot Kernel\n\r");
	theKernel = (void(*)(int, int, unsigned int))entry_point;
	theKernel(0, MACH_TYPE_S3C2440, 0x30000100); /*mov pc #0x30008000*/
 	return 0;
 }

    如上图所示,在main函数中,调用函数nand_read(0x60000, (unsigned char *)0x30007FC0, 0x200000);,即从nand flash 的 kernel分区中把内核拷贝出来,直接放到0x30007FC0位置。
    此时,有人会问为什么不是0x30008000地址,那是因为前面说过uImage有64字节的header,这64字节是不被内核运行所需的,因此0x30008000 - 64,也就是0x30007FC0位置。
    “image_header_t *hdr = (image_header_t *)0x30007FC0;” 通过指针hdr,我们就可以读出uImage header的相关信息。

Boot代码流程4:设置内核参数

    首先内核参数,由于关系到内核启动的成败。因此需要遵守一些内核的约定:参数必须有固定的格式,如下面的代码所示:
    hdr.tag 是本tag的标识符,ATAG_CORE 0x54410001 代表着是tag的开始部分,hdr.size就是本tag的大小。
    start_tag: flags pagesize rootdev 暂时不清楚什么意思,反正都赋0
    memory_tags: start 表示板子上内存的起始地址 size 表示内存的大小,专门为内核提供内存的地址空间
    commandline_tag:cmdline 表示传递给内核的启动参数。

"noinitrd root=/dev/mtdblock3 init=/linuxrc console=ttySAC0"

    root=/dev/mtdblock3 代表 根文件系统是nand flash的第4个分区,
    init=/linuxrc 代表 init进程的程序名/脚本名
    console=ttySAC0 代表选择串口0为控制台输入输出

void setup_start_tag (void)
{
	#define ATAG_CORE 0x54410001
	// 让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 (void)
{
#define PHYS_SDRAM_1  0x30000000 /* SDRAM Bank #1 */
#define PHYS_SDRAM_1_SIZE 0x04000000 /* 64 MB */

 	params->hdr.tag = ATAG_MEM;
 	params->hdr.size = tag_size (tag_mem32);

 	params->u.mem.start = PHYS_SDRAM_1;
 	params->u.mem.size = PHYS_SDRAM_1_SIZE;
 	params = tag_next (params);
 }

void setup_commandline_tag (void)
{
#define CONFIG_BOOTARGS     "noinitrd root=/dev/mtdblock3 init=/linuxrc console=ttySAC0"
	char bootargs[] = CONFIG_BOOTARGS;
	char *p = bootargs;
	params->hdr.tag = ATAG_CMDLINE;
	params->hdr.size = (sizeof (struct tag_header) + strlen (p) + 1 + 3) >> 2;
	strcpy (params->u.cmdline.cmdline, p);
	params = tag_next (params);
}
void setup_end_tag (void)
{
	params->hdr.tag = ATAG_NONE;
	params->hdr.size = 0;
}

在这里插入图片描述
如下图,为linux-2.6.22内核里面的nand flash分区信息,其中root代表的就是mtdblock3分区

/* NAND parititon from 2.4.18-swl5 */
static struct mtd_partition smdk_default_nand_part[] = {
 [0] = {
        .name   = "bootloader",
        .size   = 0x00040000,
        .offset = 0,
 },
 [1] = {
        .name   = "params",
        .offset = MTDPART_OFS_APPEND,
        .size   = 0x00020000,
         },
 [2] = {
        .name   = "kernel",
        .offset = MTDPART_OFS_APPEND,
        .size   = 0x00200000,
 },
  [3] = {
        .name   = "root",
        .offset = MTDPART_OFS_APPEND,
        .size   = MTDPART_SIZ_FULL,
         }
};
Boot代码流程5:调用内核启动函数

    在解释uImage文件时,有说过uImage文件头是有内核启动函数地址的,所以在main函数代码中,通过以下代码获取启动函数地址。

 ptr_8 = (unsigned char *)(&(hdr->ih_ep));
 entry_point = 0;
 entry_point += (((unsigned long)(ptr_8[0])) << 24);
 entry_point += (((unsigned long)(ptr_8[1])) << 16);
 entry_point += (((unsigned long)(ptr_8[2])) << 8);
 entry_point += (((unsigned long)(ptr_8[3])) << 0);

    然后通过如下语句,调用启动函数

 theKernel = (void(*)(int, int, unsigned int))entry_point;
 theKernel(0, MACH_TYPE_S3C2440, 0x30000100); /*mov pc #0x30008000*/

    指的注意的是MACH_TYPE_S3C2440 和0x30000100作为入口参数,分别代表着内核对S3C2440硬件的支持标识符,和 内核参数的地址。如此的话,内核才能成功启动。

设计总结

    个人觉得在本案例中,除了对mpu的启动特性有必要的认识以后,还需要对bootloader与内核的交互有足够的了解,这也是我想强调的东西。所以在此总结一下交互的要点:
bootloader需要知道的信息:

  1. 在nand flash里面的bootloader 和 kernel的分区信息(自己合理规划分区)
  2. 内核运行地址和内核启动函数地址(通过uImage文件头可以获取)
  3. bootloader 运行地址(自己合理规划)
  4. 内核对nand flash的分区信息(内核源码)
  5. 内核参数的格式,内核启动函数的第二个入口参数的值(通过看内核源码,或者内核设计文档获知)

bootloader需要做事情:

  1. 初始化硬件
  2. 代码重定位
  3. 拷贝内核
  4. 设置内核参数
  5. 调用内核启动地址,并把内核参数地址(第3个入口参数)和硬件标识符(第2个入口参数)
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值