序言
此案例的Bootloader是参考韦东山老师《从零写简单U_BOOT》视频课程设计的,其中有90%以上的相似度。但由于视频的网盘链接失效了,所以没办法在此分享给读者。但是有兴趣的读者,可以通过百问网的客服了解相关的情况。
本Bootloader设计重点是带领大家理解bootloader如何实现启动内核的过程,并没有实现从外部获取镜像文件的功能。当然这部分的功能,u-boot上本来就有。
相关的硬件:JZ2440(韦东山老师的百问科技出品,强烈推荐给嵌入式Linux初学者)
相关的内核版本:Linux-2.6.22
存储分区
图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 表示传递给内核的启动参数。
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需要知道的信息:
- 在nand flash里面的bootloader 和 kernel的分区信息(自己合理规划分区)
- 内核运行地址和内核启动函数地址(通过uImage文件头可以获取)
- bootloader 运行地址(自己合理规划)
- 内核对nand flash的分区信息(内核源码)
- 内核参数的格式,内核启动函数的第二个入口参数的值(通过看内核源码,或者内核设计文档获知)
bootloader需要做事情:
- 初始化硬件
- 代码重定位
- 拷贝内核
- 设置内核参数
- 调用内核启动地址,并把内核参数地址(第3个入口参数)和硬件标识符(第2个入口参数)