第二阶段分析
我们先贴出这一阶段的程序流程图,以便理解:
start_armboot
cpu_init //初始化IRQ/FIQ模式的栈
board_init
/* 设置时钟 */
clk_power->MPLLCON = ((M_MDIV << 12) + (M_PDIV << 4) + M_SDIV);
设置IO管脚
gd->bd->bi_arch_number = MACH_TYPE_SMDK2410;//机器ID
gd->bd->bi_boot_params = 0x30000100;//传参的存放地址
icache_enable()
dcache_enable();
interrupt_init //初始化定时器
env_init //检查flash上的环境参数是否有效
init_baudrate //以下三个函数用于初始化串口
serial_init
console_init_f
dram_init //检测系统内存映射
gd->bd->bi_dram[0].start = PHYS_SDRAM_1; //内存起始地址
gd->bd->bi_dram[0].size = PHYS_SDRAM_1_SIZE; //内存大小
flash_init //识别norflash并初始化
nand_init //识别nandflash
env_relocate //将环境变量读入内存有效位置
main_loop //根据环境变量启动内核
我们再贴出代码:
对应的初始化函数定义在一个结构体里:
init_fnc_t *init_sequence[] = {
cpu_init,/* basic cpu dependent setup */
board_init,/* basic board dependent setup */
interrupt_init,/* set up exceptions */
env_init,/* initialize environment */
init_baudrate,/* initialze baudrate settings */
serial_init,/* serial communications setup */
console_init_f,/* stage 1 init of console */
display_banner,/* say that we are here */
#if defined(CONFIG_DISPLAY_CPUINFO)
print_cpuinfo,/* display cpu info (and speed) */
#endif
#if defined(CONFIG_DISPLAY_BOARDINFO)
checkboard,/* display board info */
#endif
dram_init,/* configure available RAM banks */
display_dram_config,
NULL,
};
start_armboot函数代码如下:
void start_armboot (void)
{
………………………
}
(1)初始化本阶段要用的硬件设备
最主要的是设置系统时钟、初始化串口,只要这两个设置就好了,就可以从串口打印信息了
board_init函数设置MPLL、改变系统时钟,它是开发板相关的函数,在board/smdk2410/smdk2410.c中实现。值得注意的是,board_init函数中还保存了机器类型id,这将在调用内核时传给内核,board_init()函数代码如下:
2.int board_init (void)
{
S3C24X0_CLOCK_POWER * const clk_power = S3C24X0_GetBase_CLOCK_POWER();
S3C24X0_GPIO * const gpio = S3C24X0_GetBase_GPIO();
/* to reduce PLL lock time, adjust the LOCKTIME register */
clk_power->LOCKTIME = 0xFFFFFF;
/* configure MPLL */
clk_power->MPLLCON = ((M_MDIV << 12) + (M_PDIV << 4) + M_SDIV);
/* some delay between MPLL and UPLL */
delay (4000);
/* configure UPLL */
clk_power->UPLLCON = ((U_M_MDIV << 12) + (U_M_PDIV << 4) + U_M_SDIV);
/* some delay between MPLL and UPLL */
delay (8000);
/* set up the I/O ports */
gpio->GPACON = 0x007FFFFF;
gpio->GPBCON = 0x00044555;
gpio->GPBUP = 0x000007FF;
gpio->GPCCON = 0xAAAAAAAA;
gpio->GPCUP = 0x0000FFFF;
gpio->GPDCON = 0xAAAAAAAA;
gpio->GPDUP = 0x0000FFFF;
gpio->GPECON = 0xAAAAAAAA;
gpio->GPEUP = 0x0000FFFF;
gpio->GPFCON = 0x000055AA;
gpio->GPFUP = 0x000000FF;
gpio->GPGCON = 0xFF95FFBA;
gpio->GPGUP = 0x0000FFFF;
gpio->GPHCON = 0x002AFAAA;
gpio->GPHUP = 0x000007FF;
/* arch number of SMDK2410-Board */
gd->bd->bi_arch_number = MACH_TYPE_SMDK2410;/*传给内核的机器码*/
/* adress of boot parameters */
gd->bd->bi_boot_params = 0x30000100;/*启动参数*/
icache_enable();
dcache_enable();
return 0;
}
在cpu/arm920t/s3c24x0/serial.c中定义了串口初始化程序serial_int(),贴出相关代码:
3.int serial_init (void)
{
serial_setbrg ();
return (0);
}
为简化分析步骤就不具体分析了
(2)检测系统内存映射(memory map)
对于特定的开发板,其内存的分布是明确的,所以可以直接设置。board/smdk2410/smdk2410.c中的dram.init函数指定了本开发板的内存起始地址为0x30000000,大小为0x4000000,代码如下:
4.int dram_init (void)
{
gd->bd->bi_dram[0].start = PHYS_SDRAM_1;//0x30000000
gd->bd->bi_dram[0].size = PHYS_SDRAM_1_SIZE;//0x4000000
return 0;
}
这些设置的参数,将在后面向内核传递参数时用到
(3)我们知道即便是内核启动,也是通过u-boot命令来实现的,u-boot中每个命令都通过U_BOOT_CMD宏来定义,格式如下:
U_BOOT_CMD(name,maxargs,repeatable,command,"usage","help")
参数意义如下:
name:命令的名字,注意,它不是一个字符串(不要用双引号括起来)
maxargs:最大的参数个数
repeatable:命令是否可重复,可重复是指运行一个命令后,下次敲回车即可再次运行
command:对应的函数指针,类型为(*cmd)(struct cmd_tbl_s *,int,int,char *[])。
usage:简短的使用说明,这是个字符串。
help:叫详细的使用说明,这时个字符串。
宏U_BOOT_CMD在include/commond.h中定义,如下所示:
#define U_BOOT_CMD(name,maxargs,rep,cmd,usage,help)
cmd_tbl_t __u_boot_cmd_##name Struct_Section = {#name, maxargs, rep, cmd, usage, help}
Struct_Section也是在include/commond.h中定义的,如下所示:
#define Struct_Section __attribute__ ((unused,section (".u_boot_cmd")))
比如对于bootm命令,它如下定义:
U_BOOT_CMD(
bootm, CFG_MAXARGS, 1, do_bootm,
"bootm - boot application image from memory\n",
"[addr [arg ...]]\n - boot application image stored in memory\n"
"\tpassing arguments 'arg ...'; when booting a Linux kernel,\n"
"\t'arg' can be the address of an initrd image\n"
#ifdef CONFIG_OF_FLAT_TREE
"\tWhen booting a Linux kernel which requires a flat device-tree\n"
"\ta third argument is required which is the address of the of the\n"
"\tdevice-tree blob. To boot that kernel without an initrd image,\n"
"\tuse a '-' for the second argument. If you do not pass a third\n"
"\ta bd_info struct will be passed instead\n"
#endif
);
宏U_BOOT_CMD扩展开后如下所示:
cmd_tbl_t __u_boot_cmd_bootm __attribute__ ((unused,section (".u_boot_cmd")))=
{"bootm",CFG_MAXARGS,1,do_bootm."string1","string2"};
对于每个使用U_BOOT_CMD宏来定义的命令,其实都是在”.u_boot_cmd“段中定义一个cmd_tbl_t结构。连接脚本中有如下代码:
__u_boot_cmd_start = .;
.u_boot_cmd : { *(.u_boot_cmd) }
__u_boot_cmd_end = .;
程序中就是根据命令的名字在内存段__u_boot_cmd_start~__u_boot_cmd_end 之间找到它的cmd_tbl_t结构,然后调用它的函数find_cmd,关于这个函数我们可以参考common/command.c中的find_cmd函数。
最后我们来总结一下,命令的实现与应用:
我们用U_BOOT_CMD宏定义一个命令,包括命令名、对应的函数,以及说明!定义的这个命令被存放在__u_boot_cmd_start ~__u_boot_cmd_end之间的段里面。
当我们使用命令的时候,就根据命令的名字找到其对应的函数进行操作!
nand read.jffs2和bootm命令
我们已nand read.jffs2和bootm命令为例来分析一下,顺便可以把我们u_boot最核心的部分(启动内核)来分析一下:
(1)首先我们在命令行里输入nand read.jffs2 0x30007fc0 kernel //这句话用来从0x30007fc0处读取kernel分区内容
(2)程序运行在common/main.c中,s = getenv("bootcmd")来获得输入的命令,然后调用run_command (s, 0)函数。在这个函数里首先对命令进行解析,并提取命令参数,然后调用函数 find_cmd() 在 __u_boot_cmd_start~__u_boot_cmd_end 之间寻找相同的命令,并返回对应的 cmd_tbl_t 结构体,find_cmd()函数代码如下:
cmd_tbl_t *find_cmd (const char *cmd)
{
cmd_tbl_t *cmdtp;
cmd_tbl_t *cmdtp_temp = &__u_boot_cmd_start; /*Init value */
const char *p;
int len;
int n_found = 0;
/*
* Some commands allow length modifiers (like "cp.b");
* compare command name only until first dot.
*/
len = ((p = strchr(cmd, '.')) == NULL) ? strlen (cmd) : (p - cmd);
for (cmdtp = &__u_boot_cmd_start;cmdtp != &__u_boot_cmd_end; cmdtp++)//这就是我们所说的遍历了
{
if (strncmp (cmd, cmdtp->name, len) == 0)//名字要相同
{
if (len == strlen (cmdtp->name))//长度要相同
return cmdtp; /* full match */
cmdtp_temp = cmdtp; /* abbreviated command ? */
n_found++;
}
}
if (n_found == 1) { /* exactly one match */
return cmdtp_temp;
}
return NULL; /* not found or ambiguous command */
}
(3)找到命令后会调用命令对应的函数,代码如下:
(cmdtp->cmd) (cmdtp, flag, argc, argv)
在分析内核启动之前我们先来补充一些知识:
分区,在嵌入式中,各个分区都在内核里写死了,具体在include/configs中的smdk2410.h中,不过我没有发现有具体的代码,估计是在移植的时候自己添加进去的!这里我们把韦东山老师的代码贴出来,分析一下:
在nandflash上从0地址开始,前256k存放bootloader,接下来128k存放环境变量,然后是2M存放kernel,剩下的是root分区。
那么 nand read.jffs2 0x30007fc0 kernel 对应的函数是common/cmd_nand.c文件里的do_nand()函数,具体代码我们不分析了,只是说一下它的作用:读kernel分区内容到内存中
(4)在命令行输入:
bootm 0x30007fc0 //这是启动内核的
跟上面一样找到相应的命令,执行相应的程序 。我们也要补充一点知识:
u_boot下要用到uImage,uImage由一个头和真正的内核组成
其中头部的定义为:
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_load:加载地址,表示内核运行时要放在哪里
ih_ep :内核入口地址
当bootm 0x30007fc0时,如果发现内核的存放地址与加载uImage的头里面的加载地址不相符,就会把内核重新拷贝到加载地址处。这当然要花费一定的时间,所以我们一般会让它们相符,以加快内核启动速度。
对应的函数是common/cmd_bootm.c下的do_bootm()函数,关于这个函数我们不再分析具体代码,只是说一下它的功能:
.根据头部将内核移动到合适的地方
.启动内核,启动内核会调用armlinux.c中函数:do_bootm_linux()对这个函数我们也不分析其具体代码,我们只说一下它的功能:设置启动参数并跳到入口地址处。
我们还得来说一说启动参数的问题,我们知道当内核启动起来之后,u_boot就没有用了,所以u_boot需要将一些内核参数放在跟内核约定好的某个位置,以便内核能够在无u_boot下访问,下面一些函数完成这项功能:
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)
它带了三个参数:bd->bi_arch_number表示机器码
bd->bi_boot_params启动参数地址
这两个参数在board/smdk2410/smdk2410.c文件里可以找到定义:
gd->bd->bi_arch_number = MACH_TYPE_SMDK2410 //不解释,在内核分析里会详细分析
gd->bd->bi_boot_params = 0x30000100 //启动参数放在0x30000100开始的位置,要记清楚,内核分析里要用到的