前一文件是平台相关的,后一文件是开发板相关的。
Uboot第一阶段分析:
1、硬件设备的初始化:将cpu的工作模式设置为管理模式,关闭WATCHDOG,设置FCLK,HCLK,PCLK的比例,关闭mmu,cache.
2、为加载bootloader的第二阶段代码准备RAM空间,即初始化内存芯片,使它可用,通过在start.S中调用lowlevel_init函数来
设置存储控制器。代码在board/smdk2410/lowlevel_init.S中。
3、复制第二阶段代码到RAM空间中。实际中可能把一二阶段的代码全部都复制到RAM空间中了。
自己写bootloader的时候,应尽可能早地复制代码,跳转到复制后的代码中执行,这样不用考虑后面的代码的位置无关的问题。
要注意弄清楚从哪里复制到那里去,复制多少的问题,根据链接脚本可以解决这些问题。(一般开发中会从主机上下载uboot到
内存上,再把它复制到flash中存储着。开机时再从flash复制到内存上,链接地址上去运行)
mov r0, #0 //这里uboot存储在flash 0地址上,从flash的 0地址开始复制
ldr r1, =_start //uboot链接地址,复制到这里去,1.1.6内核中,链接地址是0x33f80000
ldr r2, =__bss_start //程序最后的bss段不用复制,所以结束地址是__bss_start,而不是__bss_end
sub r2, r2, r1 //复制的大小: __bss_start - _start
//...接着调用复制的代码
4、设置栈,栈放在哪里?栈一般指向SDRAM较高的地址。
5、清BSS段,往BSS段中写0。
6、跳转到第二阶段代码的C入口点。
通过如下命令直接跳转,它调用lib_arm/board.c中的start_armboot函数,这是第二阶段的入口。
ldr pc,_start_armboot
_start_armboot: .word start_armboot
插入讲一下:环境变量有bootcmd,bootargs,serverip,ipaddr,等,获取指定环境变量值函数是:getenv(),环境变量除了
可以在uboot命令行下设置,还可以在哪个文件中设置?可以编译uboot之前在include\configs\smdk2410.h文件中设置。
启动的时候,uboot main_loop()函数中主动读取环境变量bootcmd的值(或称引导命令),然后run_command:
s = getenv(bootcmd)
//s 就是uboot下print出来的:bootcmd = nand read.jffs2 0x30007fc0 kernel;bootm 0x30007cf0
if (bootdelay >= 0 && s && !abortboot (bootdelay))
//如果延时大于等于零,并且没有在延时过程中接收到按键,则引导内核。
run_command (s, 0); //运行引导内核的命令。
在启动的倒计时中按下空格键,将进入main_loop循环。
uboot界面下循环执行下面代码(uboot命令行界面):
readline()//读入串口的数据,根据输入对应地来run_command
run_command()
无论是获取bootcmd值自动启动内核还是 uboot命令行下启动内核,效果应该一样。
下面分析通过环境变量bootcmd的值自动启动内核过程:
bootcmd = nand read.jffs2 0x30007fc0 kernel;bootm 0x30007cf0
bootcmd的第一个命令nand read.jffs2 0x30007fc0 kernel 与 nand read.jffs2 0x30007fc0 0x00060000 0x00200000 作用是一样的,就是把内核从nand的kernel分区读到内存0x30007fc0,实际上是把kernel分区全部内容读到0x30007fc0。实际上可以把内核读到一个比较随意的地址,然后bootm 0x30007fc0 命令会把真正的内核从这个随意的地址移到内核头部指定的加载地址处
(这里真正的内核的加载地址是0x30008000,即真正的内核应该被加载到0x30008000),(内核头部含有加载地址和入口地址,
头部64字节,0x30007fc0+64 = 0x30008000,这里让头部存于0x30007fc0,则真正的内核被存于0x30008000,刚好处于加载地址,不用移动)
为什么用jffs2? 答:用jffs2则读取的长度不需要页对齐,其他格式的话,读取的长度需要页对齐或块对齐限制。
bootcmd的第二个命令bootm 0x30007cf0
bootm命令调用过程:
bootm命令
...
调用do_bootm()函数 // if ((cmdtp->cmd) (cmdtp, flag, argc, argv) != 0)
移动内核
do_bootm_linux
char *commandline = getenv ("bootargs");
theKernel = (void (*)(int, int, uint))ntohl(hdr->ih_ep);
setup_start_tag (bd);
setup_memory_tags (bd);
setup_commandline_tag (bd, commandline);
setup_end_tag (bd);
theKernel (0, bd->bi_arch_number, bd->bi_boot_params);
下面主要分析这个过程中的各种setup tag 和theKernel函数。
第一个设置的tag必须是用setup_start_tag()函数设置的start tag。在介绍setup_start_tag()函数之前,先介绍几个结构体。
typedef struct bd_info {
int bi_baudrate;
unsigned long bi_ip_addr;
unsigned char bi_enetaddr[6];
struct environment_s *bi_env;
ulong bi_arch_number; //机器ID
ulong bi_boot_params; //用于保存启动参数链表的地址
struct
{
ulong start;
ulong size;
} bi_dram[CONFIG_NR_DRAM_BANKS];
#ifdef CONFIG_HAS_ETH1
unsigned char bi_enet1addr[6];
#endif
} bd_t
对bd_t结构体,这里主要涉及其bi_arch_number成员和bi_boot_parmas成员。
typedef struct global_data {
bd_t *bd;
unsigned long flags;
unsigned long baudrate;
unsigned long have_console;
unsigned long reloc_off;
unsigned long env_addr;
unsigned long env_valid;
unsigned long fb_base;
#ifdef CONFIG_VFD
unsigned char vfd_type;
#endif
#if 0
unsigned long cpu_clk;
unsigned long bus_clk;
unsigned long ram_size;
unsigned long reset_status;
#endif
void **jt;
} gd_t;
include/am-arm/global_data.h头文件的第64行:
64 #define DECLARE_GLOBAL_DATA_PTR register volatile gd_t *gd asm ("r8")
它的作用是声明一个全局的gd_t类型指针gd,并且gd是保存在ARM的r8这个寄存器里面的,且有且仅有一个gd_t结构体,就是gd。gd结构体用来保存与平台硬件相关的一些信息,或uboot与内核交互用的一些必要信息,这些信息在board.c和smdk2410.c文件中收集,主要在各种硬件的init函数中收集。
在smdk2410.c中填充了gd结构体两个重要的成员的成员:
/* arch number of SMDK2410-Board */
gd->bd->bi_arch_number = MACH_TYPE_SMDK2410;
/* adress of boot parameters */
gd->bd->bi_boot_params = 0x30000100;
对于gd_t结构体的分配与设置,可以参考board.c里面start_armboot()函数。
下面是board.c里面start_armboot()函数部分内容:
void start_armboot (void)
{
...
/* Pointer is writable since we allocated a register for it */
gd = (gd_t*)(_armboot_start - CFG_MALLOC_LEN - sizeof(gd_t)); //gd指向某个地址
/* compiler optimization barrier needed for GCC >= 3.4 */
__asm__ __volatile__("": : :"memory");
memset ((void*)gd, 0, sizeof (gd_t)); //gd结构体清零
gd->bd = (bd_t*)((char*)gd - sizeof(bd_t)); //这个gd中的bd所在的地址。
memset (gd->bd, 0, sizeof (bd_t)); //清零gd->bd
monitor_flash_len = _bss_start - _armboot_start;
for (init_fnc_ptr = init_sequence; *init_fnc_ptr; ++init_fnc_ptr) {//调用init_sequence[]中的各初始化函数
if ((*init_fnc_ptr)() != 0) {
hang ();
}
}
...
}
启动参数结构体:
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;
};
struct tag_header {
__u32 size;
__u32 tag;//tag结构体的的类型标志,有值ATAG_CORE、ATAG_MEM、ATAG_CMDLINE
//和ATAG_NONE(用于最后一个空的tag,表示tag链表结束)
};
lib_arm\board.c部分代码:
static struct tag *params;
void do_bootm_linux (cmd_tbl_t *cmdtp, int flag, int argc, char *argv[],
ulong addr, ulong *len_ptr, int verify)
{
void (*theKernel)(int zero, int arch, uint params);
image_header_t *hdr = &header;
bd_t *bd = gd->bd;
//smdk2410.c中填充了gd两个重要的成员的成员:
//gd->bd->bi_arch_number = MACH_TYPE_SMDK2410;
//gd->bd->bi_boot_params = 0x30000100;
#ifdef CONFIG_CMDLINE_TAG
char *commandline = getenv ("bootargs");
#endif
#if defined (CONFIG_SETUP_MEMORY_TAGS) || \
defined (CONFIG_CMDLINE_TAG) || \
defined (CONFIG_INITRD_TAG) || \
defined (CONFIG_SERIAL_TAG) || \
defined (CONFIG_REVISION_TAG) || \
defined (CONFIG_LCD) || \
defined (CONFIG_VFD)
setup_start_tag (bd);
#ifdef CONFIG_SERIAL_TAG
setup_serial_tag (¶ms);
#endif
#ifdef CONFIG_REVISION_TAG
setup_revision_tag (¶ms);
#endif
#ifdef CONFIG_SETUP_MEMORY_TAGS
setup_memory_tags (bd);
#endif
#ifdef CONFIG_CMDLINE_TAG
setup_commandline_tag (bd, commandline);
#endif
setup_end_tag (bd);
#endif
theKernel (0, bd->bi_arch_number, bd->bi_boot_params);
}
//注意必须首先调用setup_start_tag(bd),最后调用setup_end_tag(bd)
static void setup_start_tag (bd_t *bd)
{
params = (struct tag *) bd->bi_boot_params;
//ulong bd->bi_boot_params, tag第一个成员是tag_header hdr,是__u32类型
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 (bd_t *bd)
{
int i;
for (i = 0; i < CONFIG_NR_DRAM_BANKS; i++) {
params->hdr.tag = ATAG_MEM;
params->hdr.size = tag_size (tag_mem32);
params->u.mem.start = bd->bi_dram[i].start;
params->u.mem.size = bd->bi_dram[i].size;
params = tag_next (params);
}
}
static void setup_commandline_tag (bd_t *bd, char *commandline)
{
char *p;
if (!commandline)
return;
/* eat leading white space */
for (p = commandline; *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;
params->hdr.size =
(sizeof (struct tag_header) + strlen (p) + 1 + 4) >> 2;
strcpy (params->u.cmdline.cmdline, p);
params = tag_next (params);
}
......
static void setup_end_tag (bd_t *bd)
{
params->hdr.tag = ATAG_NONE;
params->hdr.size = 0;
}
do_bootm_linux()函数中设置好这些tag(启动参数)后
接着调用: theKernel (0, bd->bi_arch_number, bd->bi_boot_params);启动内核。
在介绍theKernel()函数之前,先来看一下bootm命令与内核的加载地址和入口地址的关系。
uImage包含头部header结构体和zImage(真正的内核)。
typedef struct image_header {
uint32_t ih_magic;
uint32_t ih_hcrc;
uint32_t ih_time;
uint32_t ih_size;
uint32_t ih_load; //真正的内核的加载地址,应该加载到这里
uint32_t ih_ep; //入口地址,正常运行时,ih_ep应该指向真正的内核,否则无法正常启动。
//当真正的内核的实际加载地址等于ih_load时,
uint32_t ih_dcrc; //ih_ep 应等于ih_load(即指向真正的内核的实际加载地址)才能正常启动。
uint8_t ih_os;
uint8_t ih_arch;
uint8_t ih_type;
uint8_t ih_comp;
uint8_t ih_name[IH_NMLEN];
} image_header_t;
bootm会判断是否需要移动内核,若需要移动,则仅仅把真正的内核移动到header指定的ih_load中。
但源码中bootm xxx会根据xxx是否与ih_load相同来判断是否需要移动真正的内核,相同则不移动,不同则移动。
这是有点不合理的,xxx 是整个内核(包含头部)下载到内存中的位置。
举几个启动内核例子说明他们的关系:
1、假设:真正的内核应该的 加载地址 ih_load : 30008000
入口地址 ih_ep :30008040
a、若把内核(包含头部)下载到30008000,则真正的内核此时处于30008040,与入口地址ih_ep 30008040相等。
bootm 30008000启动内核,这个地址与ih_load 30008000相等,不移动真正的内核,而入口地址ih_ep 300080040正确
指向真正的内核30008040,可以正常启动。
b、如若内核(包含头部)下载地址不是30008000,而是其他的xxx,则应bootm xxx启动,此时bootm判断出 内核(包含头部)下 载地址xxx 与ih_load 30008000 不相等,则移动真正的内核至 ih_load 30008000,此时入口地址ih_ep 30008040 不能正确指 向真正的内核ih_load 30008000,不能正常启动。
2、假设:真正的内核应该的 加载地址 ih_load : 30008000
入口地址 ih_ep :30008000
a、若把内核(包含头部)下载到30008000,则真正的内核此时处于30008040,与入口地址ih_ep 30008000不相等。
bootm 30008000启动内核,这个地址与ih_load 30008000相等,不移动真正的内核,而入口地址ih_ep 300080000不能正 确指向真正的内核30008040,不可以正常启动。
b、如果只把内核(包含头部)下载地址改为0x30007cf0,则真正的内核此时处于30008000,与真正的内核应该的加载地址 ih_load 30008000相等。bootm 判断出内核(包含头部)下载地址0x30007cf0与真正的内核应该的加载地址ih_load 30008000不相等,则把真正的内核移植ih_load 30008000(实际上下载时,真正的内核刚好处于30008000,刚好重叠,不需 移动),此时入口地址ih_ep 30008000能正确指向真正的内核 ih_load 30008000,可以正常启动。
可以看到,真正的内核的加载地址ih_load其实没有什么用,无论bootm 是否移动真正的内核,最终启动时只要保证ih_ep指向真正的内核最终在内存中的实际加载地址即可正常启动。
好了,看theKernel,
对于theKernel,do_bootm_linux()函数里面有下面3条语句:
void (*theKernel)(int zero, int arch, uint params);//声明一个函数指针
theKernel = (void (*)(int, int, uint))ntohl(hdr->ih_ep);//初始化这个函数指针
//ntohl()用来将参数指定的32 位netlong 转换成主机字符顺序,在将这个东东强制转换为对应的函数指针赋给theKernel
//ntohl(hdr->ih_ep)相当于0x30008000(假设ih_ep为0x30008000,ih_load和ih_ep在用mkimage制作内核时通过-a,-e选
//项指定),不管真正的内核的实际加载地址是否与ih_load相等,此时的ih_ep应指向真正的内核在内存中的实际加载地址。
thekernel(0,bd->bi_arch_number,bd->bi_boot_parmas);
/* theKernel(0, bd->bi_arch_number, bd->bi_boot_params);这相当于汇编:
* mov r0,#0
* ldr r1,=362
* ldr r2,=0x30000100
* mov pc,#0x30008000
*/
theKernel()第一个参数指定为0;第二个参数bd->bi_arch_number是机器ID,smkd2410是MACH_TYPE_SMDK2410=193,而MACH_TYPE_S3C2440 =362;第三个参数保存着的是启动参数的地址0x30000100。而theKernel指针本身是0x30008000,指向真正的内核。
theKernel函数会把它的三个参数分别赋给r0,r1,r2寄存器,并把thekernel指向的地址0x30008000赋给pc寄存器,跳转到0x30008000执行。
内核代码中会处理r0,r1,r2。
再一次说明一下,theKernel()参数中的bd实际上来自是gd->bd,在smdk2410.c中填充了gd两个重要的成员的成员:
//gd->bd->bi_arch_number = MACH_TYPE_SMDK2410;
//gd->bd->bi_boot_params = 0x30000100;
即可在smdk2410.c中设置机器ID和bi_boot_params,内核机器码和uboot机器码必须一致才能启动内核。