ldr pc, _start_armboot
_start_armboot: .word start_armboot
而start_armboot()就是第二阶段的入口,首先我们要认识两个重要的结构体,start_armboot()函数的一系列初始化可以说都是以这个结构体为主线的:
typedef struct global_data {
bd_t *bd;
unsigned long flags;
unsigned long baudrate;
unsigned long have_console; /* serial_init() was called */
unsigned long reloc_off; /* Relocation Offset */
unsigned long env_addr; /* Address of Environment struct */
unsigned long env_valid; /* Checksum of Environment valid? */
unsigned long fb_base; /* base address of frame buffer */
#ifdef CONFIG_VFD
unsigned char vfd_type; /* display type */
#endif
#if 0
unsigned long cpu_clk; /* CPU clock in Hz! */
unsigned long bus_clk;
unsigned long ram_size; /* RAM size */
unsigned long reset_status; /* reset status register at boot */
#endif
void **jt; /* jump table */
} gd_t;
typedef struct bd_info {
int bi_baudrate; /* serial console baudrate */
unsigned long bi_ip_addr; /* IP Address */
unsigned char bi_enetaddr[6]; /* Ethernet adress */
struct environment_s *bi_env;
ulong bi_arch_number; /* unique id for this board */
ulong bi_boot_params; /* where this board expects params */
struct /* RAM configuration */
{
ulong start;
ulong size;
} bi_dram[CONFIG_NR_DRAM_BANKS];
#ifdef CONFIG_HAS_ETH1
/* second onboard ethernet port */
unsigned char bi_enet1addr[6];
#endif
} bd_t;
#define DECLARE_GLOBAL_DATA_PTR register volatile gd_t *gd asm ("r8")
这里会申明一个全局寄存器指针变量gd,指定r8为存储的寄存器,并且为volatile,即告知编译器不要优化,使CPU每次都到指定的内存去内容。每个.c若要使用这个gd,只需要在文件的前面加以 DECLARE_GLOBAL_DATA_PTR 申明即可。
那这个指针变量指的是哪里的内存的,函数start_armboot()的前部分有如下code:
gd = (gd_t*)(_armboot_start - CFG_MALLOC_LEN - sizeof(gd_t));
/* compiler optimization barrier needed for GCC >= 3.4 */
__asm__ __volatile__("": : :"memory");
memset ((void*)gd, 0, sizeof (gd_t));
gd->bd = (bd_t*)((char*)gd - sizeof(bd_t));
memset (gd->bd, 0, sizeof (bd_t));
其中存放全局配置内容的gd在_armboot_start 偏下CFG_MALLOC_LEN 下紧随其下的即使bd的存储空间了,内存分布图为:
这就要求我们在设置TEXT_BASE一定要远大于SDRAM的基址0x30000000才行,同时后续会讲到cpu_init()会设置IRQ和FRQ的栈,这会占用bd的内存空间,所幸的是uboot并没有使能中断,所以bd的内存不会被破坏。
接下来是一系列基本的初始化:
for (init_fnc_ptr = init_sequence; *init_fnc_ptr; ++init_fnc_ptr) {
if ((*init_fnc_ptr)() != 0) {
hang ();
}
}
//其中init_sequence是一个函数指针数组
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,
};
我们依次分析里面的每一个函数:
a. cpu_init()设置IRQ和FRQ的栈,不过没用到,也不应该用到,否则其指向的内存刚好和bd的重叠导致数据被破坏
b. board_init(),该函数明显和具体的板子有关,到board/tq2440下的tq2440.c有该函数的实现
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;
}
主要是设置时钟的频率,GPIO,以及gd arch_number boot_params,这两个参数到时候会传给kernel, arch_number 用于校验kernel是否合法,boot_params是传给kernel的参数的起始地址。因为uboot传给kernel是单向的,所以由于不可能同时运行uboot和kernel,所以uboot只能事先将要传递的参数放置某地址处,然后启动kernel并将该地址作为参数传给kernel,后面我们会讲解到的。
c. interrupt_init(),中断初始化,啥都没做,即不使能中断
d. env_init(),环境变量的初始化,无非就是设置gd->env_addr和gd->env_valid,env_init()在env_nand.c env_flash.c均有实现,其主要思想就是判断gd->env_addr指向哪,当通过crc校验tq2440.h指定的换环境地址CFG_ENV_ADDR后,如果错误,则使用默认的环境变量default_environment,通常uboot初次在板子启动的时候还没有将环境变量保存在存储介质上,所以都会指向default_environment,default_environment是一个数组,存储在内存当中,当uboot跑起来后执行save 环境变量指令后才会保存到指定的存储位置处,到下次启动时进过crc校验发现有数据了才设置gd->env_addr指向该处。所以板子初次是使用default_environment,直到save保存到存储介质后下次才会从该处获取,这样的好处是当我们更改了环境变变量原有的值或者新增的值可以得以保存。
uchar default_environment[] = {
#ifdef CONFIG_BOOTARGS
"bootargs=" CONFIG_BOOTARGS "\0"
#endif
#ifdef CONFIG_BOOTCOMMAND
"bootcmd=" CONFIG_BOOTCOMMAND "\0"
#endif
#ifdef CONFIG_RAMBOOTCOMMAND
"ramboot=" CONFIG_RAMBOOTCOMMAND "\0"
#endif
#ifdef CONFIG_NFSBOOTCOMMAND
"nfsboot=" CONFIG_NFSBOOTCOMMAND "\0"
#endif
#if defined(CONFIG_BOOTDELAY) && (CONFIG_BOOTDELAY >= 0)
"bootdelay=" MK_STR(CONFIG_BOOTDELAY) "\0"
#endif
#if defined(CONFIG_BAUDRATE) && (CONFIG_BAUDRATE >= 0)
"baudrate=" MK_STR(CONFIG_BAUDRATE) "\0"
#endif
#ifdef CONFIG_LOADS_ECHO
"loads_echo=" MK_STR(CONFIG_LOADS_ECHO) "\0"
#endif
#ifdef CONFIG_ETHADDR
"ethaddr=" MK_STR(CONFIG_ETHADDR) "\0"
#endif
#ifdef CONFIG_ETH1ADDR
"eth1addr=" MK_STR(CONFIG_ETH1ADDR) "\0"
#endif
#ifdef CONFIG_ETH2ADDR
"eth2addr=" MK_STR(CONFIG_ETH2ADDR) "\0"
#endif
#ifdef CONFIG_ETH3ADDR
"eth3addr=" MK_STR(CONFIG_ETH3ADDR) "\0"
#endif
#ifdef CONFIG_IPADDR
"ipaddr=" MK_STR(CONFIG_IPADDR) "\0"
#endif
#ifdef CONFIG_SERVERIP
"serverip=" MK_STR(CONFIG_SERVERIP) "\0"
#endif
#ifdef CFG_AUTOLOAD
"autoload=" CFG_AUTOLOAD "\0"
#endif
#ifdef CONFIG_PREBOOT
"preboot=" CONFIG_PREBOOT "\0"
#endif
#ifdef CONFIG_ROOTPATH
"rootpath=" MK_STR(CONFIG_ROOTPATH) "\0"
#endif
#ifdef CONFIG_GATEWAYIP
"gatewayip=" MK_STR(CONFIG_GATEWAYIP) "\0"
#endif
#ifdef CONFIG_NETMASK
"netmask=" MK_STR(CONFIG_NETMASK) "\0"
#endif
#ifdef CONFIG_HOSTNAME
"hostname=" MK_STR(CONFIG_HOSTNAME) "\0"
#endif
#ifdef CONFIG_BOOTFILE
"bootfile=" MK_STR(CONFIG_BOOTFILE) "\0"
#endif
#ifdef CONFIG_LOADADDR
"loadaddr=" MK_STR(CONFIG_LOADADDR) "\0"
#endif
#ifdef CONFIG_CLOCKS_IN_MHZ
"clocks_in_mhz=1\0"
#endif
#if defined(CONFIG_PCI_BOOTDELAY) && (CONFIG_PCI_BOOTDELAY > 0)
"pcidelay=" MK_STR(CONFIG_PCI_BOOTDELAY) "\0"
#endif
#ifdef CONFIG_EXTRA_ENV_SETTINGS
CONFIG_EXTRA_ENV_SETTINGS
#endif
"\0"
};
从上面我们可以知道如果要设置指定的环境变量的话,只需要在tq2440.h里定义该宏即可
e. init_baudrate(),直接获取环境变量的配置,所以该函数必须在env_init()之后。
f. serial_init(),串口的初始化,在u-boot-1.1.6\cpu\arm920t\s3c24x0\serial.c里实现的,里面直接调用serial_setbrg,主要就是设置波特率等一些设置,注意的是
/* value is calculated so : (int)(PCLK/16./baudrate) -1 */
reg = get_PCLK() / (16 * gd->baudrate) - 1;
由于get_PCLK() -> get_HCLK() ->get_FCLK(), 而2410 和2440对这三个频率的获取公式不一样,所以在移植的时候要注意这点!
g. dram_init(),SDRAM的初始化,也在board/tq2440下的tq2440.c下
int dram_init (void)
{
gd->bd->bi_dram[0].start = PHYS_SDRAM_1;
gd->bd->bi_dram[0].size = PHYS_SDRAM_1_SIZE;
return 0;
}
#define PHYS_SDRAM_1 0x30000000 /* SDRAM Bank #1 */
#define PHYS_SDRAM_1_SIZE 0x04000000 /* 64 MB */
这个函数指针数组基本就是这样子了,
我们接着往下看,
如果有nor flash的话会有flash_init(),明显这是跟板子有关的,在u-boot-1.1.6\board\tq2440\flash.c里,具体的实现请读者自行分析,如果在tq2440.h里申明了CFG_CMD_NAND 的话 还会初始化nand_init(); 里面最终会调用board_nand_init(nand); 这个是extern,即如果用户想使用nand flash的话 那么得自己完成相应的code,当然是最底层的code的实现,上层的driver已经基本是完成的。
继续往下走会发现有个env_relocate ();这个是环境变量的重定向,为什么要重定向呢,主要是前面只是指向了环境的基址,但如果指向的是存储在存储介质入flash或nand的话,那么访问速度就会慢,所以干脆搬到SDRAM来以便快速的访问!只是奇怪如果是指向default_environment的话已经在SDRAM了不知道为何还要搬移,估计是为了统一,即不管之前到底是在default_environment的ram内存还是非易失性存储介质都统一搬移到指定处,而这个“指定”的内存是使用malloc,即前面事先预留的,
#define CFG_MALLOC_LEN (CFG_ENV_SIZE + 128*1024)
显然这个malloc特意在128K基础上再开辟CFG_ENV_SIZE来存放环境变量。
最后 函数调用 main_loop (); 下面大概讲诉这个 main_loop ();
#if defined(CONFIG_BOOTDELAY) && (CONFIG_BOOTDELAY >= 0)
s = getenv ("bootdelay");
bootdelay = s ? (int)simple_strtol(s, NULL, 10) : CONFIG_BOOTDELAY;
debug ("### main_loop entered: bootdelay=%d\n\n", bootdelay);
s = getenv ("bootcmd");
debug ("### main_loop: bootcmd=\"%s\"\n", s ? s : "<UNDEFINED>");
if (bootdelay >= 0 && s && !abortboot (bootdelay)) {
run_command (s, 0);
...........................................
也就是说板子在delay时间内没有任何按键的话就会执行bootcmd,而正是这个命令将会启动内核!
3.接下来重点讲解bootcmd,一般其命令格式如下:
#define CONFIG_BOOTCOMMAND "nand read 0x32000000 0x200000 0x300000; bootm
0x32000000"
该‘bootcmd’分为两个命令 一个是nand read 0x32000000 0x200000 0x300000;,即从nand地址为0x200000 copy size为0x300000 到0x32000000处,其实就是实现将kernel的code从存储介质拷贝到内存当中去,第二个命令则是调用bootm命令并将刚才拷贝的kernel基址作为参数传进去,看一下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
);
显然最后会调用do_bootm函数:
int do_bootm (cmd_tbl_t *cmdtp, int flag, int argc, char *argv[])
{
addr = simple_strtoul(argv[1], NULL, 16);
memmove (&header, (char *)addr, sizeof(image_header_t));
data = (ulong)&header;
len = sizeof(image_header_t);
checksum = ntohl(hdr->ih_hcrc);
hdr->ih_hcrc = 0;
if (crc32 (0, (uchar *)data, len) != checksum) {
data = addr + sizeof(image_header_t);
len = ntohl(hdr->ih_size);
if (verify) {
puts (" Verifying Checksum ... ");
if (crc32 (0, (uchar *)data, len) != ntohl(hdr->ih_dcrc)) {
**memmove ((void *) ntohl(hdr->ih_load), (uchar *)data, len);**
**if (gunzip ((void *)ntohl(hdr->ih_load), unc_len,
(uchar *)data, &len) != 0)**
**BZ2_bzBuffToBuffDecompress ((char*)ntohl(hdr->ih_load),
&unc_len, (char *)data, len,
CFG_MALLOC_LEN < (4096 * 1024), 0);**
do_bootm_linux (cmdtp, flag, argc, argv,
addr, len_ptr, verify);
该函数的基本作用就是分析该kernel的头部信息(所以必须编译出来的kernel要经过uboot的mkimage对image添加头部信息成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;是最终kernel的基址,所以不管前面的nand read 0x32000000 0x200000 0x300000;到0x32000000还是其他地址,都会通过解析这个头部信息来获取最终的加载地址,在将code搬移到指定的ih_load前会先判断该kernel是否是压缩的,如果没压缩直接memmove ((void ) ntohl(hdr->ih_load), (uchar )data, len); 到指定的ih_load处,如果是压缩的就解压到指定ih_load处。同时会对头部信息和kernel进行crc校验 其中ih_hcrc ih_dcrc两个校验值又mkimage工具生成。
所以do_bootm()函数就是将kernel经过解压(如果需要)搬移到指定的ih_load处,并校验数据的完整性,最后调用do_bootm_linux ()函数
4.do_bootm_linux ()分析:
主要code如下:
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);
theKernel = (void (*)(int, int, uint))ntohl(hdr->ih_ep);
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
#ifdef CONFIG_INITRD_TAG
if (initrd_start && initrd_end)
setup_initrd_tag (bd, initrd_start, initrd_end);
#endif
#if defined (CONFIG_VFD) || defined (CONFIG_LCD)
setup_videolfb_tag ((gd_t *) gd);
#endif
setup_end_tag (bd);
#endif
cleanup_before_linux ();
theKernel (0, bd->bi_arch_number, bd->bi_boot_params);
其实就是调用kernel,申明一个void (*theKernel)(int zero, int arch, uint params); 函数指针,然后指向kernel的入口处,接着传参调用,至于参数是怎么传递,uboot在调用kernel之前先在指定的传参基址处依次赋值,这个传参基址就是在之前board_init()里定义的gd->bd->bi_boot_params = 0x30000100;
当然,除了传参基址外,还要规定传参的数据结构,linux 2.4.x 后采用标记列表(tagged list)的形式来启动参数,具体可以自行上网查询。最后调用theKernel 后会根据这个传参基址自行解析参数,后续会对 linux kernel讲解时提及的!