赞
踩
uboot就是一段引导程序,在加载系统内核之前,完成硬件初始化,内存映射,为后续内核的引导提供一个良好的环境。uboot是bootloader的一种,全称为universal boot loader。
为了生成u-boot.bin这个文件,首先要生成构成u-boot.bin的各个库文件、目标文件。为了各个库文件、目标文件就必须进入各个子目录执行其中的Makefile。由此,确定了整个编译的命令和顺序。
(1)第一步:配置,执行make pangu_basic_defconfig进行配置,生成.config文件
(2)第二步:编译,执行make进行编译,生成u-boot.bin。
BL0
ROM上的固化程序(Boot Rom)
BL1(u-boot-spl)
启动操作系统
指定代码段和数据段、只读数据段在内存中的存放地址;(地址具体为i.m6ull , 其他芯片可能不是 0X87800000)
u-boot.map 是 uboot 的映射文件,看到某个文件或者函数链接到了哪个地址,
__image_copy_start 为 0X87800000,而.text 的起始地址也是0X87800000。
vectors 段保存中断向量表,vectors 段的起始地址也是 0X87800000,说明整个 uboot 的起始地址就是 0X87800000,
这也是为什么我们裸机例程的链接起始地址选择 0X87800000 了,目的就是为了和 uboot 一致。
指定代码的入口地址;
Boot ROM会通过检测启动方式来加载第二阶段bootloader。uboot已经是一个bootloader了,那么为什么还多一个uboot spl呢?
这个主要原因是对于一些SOC来说,它的内部SRAM可能会比较小,小到无法装载下一个完整的uboot镜像,那么就需要spl,它主要负责初始化外部RAM和环境,并加载真正的uboot镜像到外部RAM(DDR)中来执行。
所以由此来看,SPL应该是一个非常小的loader程序,可以运行于SOC的内部SRAM中,它的主要功能就是加载真正的uboot并运行之。
_start:
#ifdef CONFIG_SYS_DV_NOR_BOOT_CFG
.word CONFIG_SYS_DV_NOR_BOOT_CFG
#endif
b reset
ldr pc, _undefined_instruction
ldr pc, _software_interrupt
ldr pc, _prefetch_abort
ldr pc, _data_abort
ldr pc, _not_used
ldr pc, _irq
ldr pc, _fiq
有一条跳转指令b reset跳转到reset函数处去执行
注意,spl的流程在reset中就应该被结束,也就是说在reset中,就应该转到到BL2,也就是uboot中了。
reset:
/* Allow the board to save important registers */
b save_boot_params @进入reset第一步跳转到save_boot_params
save_boot_params_ret: @ save_boot_params 内部通过cpsr 设置cpu为SVC模式,关闭FIQ和IRQ中断
/*
* disable interrupts (FIQ and IRQ), also set the cpu to SVC32 mode,
* except if in HYP mode already
*/
mrs r0, cpsr
and r1, r0, #0x1f @ mask mode bits
teq r1, #0x1a @ test for HYP mode
bicne r0, r0, #0x1f @ clear all mode bits
orrne r0, r0, #0x13 @ set SVC mode
orr r0, r0, #0xc0 @ disable FIQ and IRQ
msr cpsr,r0
/* the mask ROM code should have PLL and others stable */
#ifndef CONFIG_SKIP_LOWLEVEL_INIT
bl cpu_init_cp15 @ 跳转到cpu_init_cp15 ,初始化协处理器CP15,从而禁用MMU和TLB。
bl cpu_init_crit @ 跳转到cpu_init_crit ,进行一些关键的初始化动作,也就是平台级和板级的初始化
#endif
bl _main @ 跳转到_main ,加载BL2以及跳转到BL2的主体部分
进入reset函数,首先设置cpu为SVC模式,关闭中断。然后跳转到cpu_init_cp15 ,初始化协处理器CP15,从而禁用MMU和TLB。跳转到cpu_init_crit ,进行一些关键的初始化动作,也就是平台级和板级的初始化。最后跳转到**_main**,加载BL2以及跳转到BL2的主体部分
ENTRY(cpu_init_cp15)
/*
* Invalidate L1 I/D
*/
mov r0, #0 @ set up for MCR
mcr p15, 0, r0, c8, c7, 0 @ invalidate TLBs
mcr p15, 0, r0, c7, c5, 0 @ invalidate icache
mcr p15, 0, r0, c7, c5, 6 @ invalidate BP array
mcr p15, 0, r0, c7, c10, 4 @ DSB
mcr p15, 0, r0, c7, c5, 4 @ ISB
@@ 这里只需要知道是对CP15处理器的部分寄存器清零即可。
@@ 将协处理器的c7\c8清零等等,各个寄存器的含义请参考《ARM的CP15协处理器的寄存器》
/*
* disable MMU stuff and caches
*/
mrc p15, 0, r0, c1, c0, 0
bic r0, r0, #0x00002000 @ clear bits 13 (--V-)
bic r0, r0, #0x00000007 @ clear bits 2:0 (-CAM)
orr r0, r0, #0x00000002 @ set bit 1 (--A-) Align
orr r0, r0, #0x00000800 @ set bit 11 (Z---) BTB
#ifdef CONFIG_SYS_ICACHE_OFF
bic r0, r0, #0x00001000 @ clear bit 12 (I) I-cache
#else
orr r0, r0, #0x00001000 @ set bit 12 (I) I-cache
#endif
mcr p15, 0, r0, c1, c0, 0
@@ 通过上述的文章的介绍,我们可以知道cp15的c1寄存器就是MMU控制器
@@ 上述对MMU的一些位进行清零和置位,达到关闭MMU和cache的目的,具体的话去看一下上述文章吧。
ENDPROC(cpu_init_cp15)
cpu_init_cp15 用来设置 CP15 相关的内容,完成启动ICACHE,关闭DCACHE,关闭MMU和TLB 。
ENTRY(cpu_init_crit)
/*
* Jump to board specific initialization...
* The Mask ROM will have already initialized
* basic memory. Go here to bump up clock rate and handle
* wake up conditions.
*/
b lowlevel_init @ go setup pll,mux,memory
ENDPROC(cpu_init_crit)
cpu_init_crit函数的内容是跳转到lowlevel_init函数。
lowlevel_init主要完成平台级和板级的初始化
在lowlevel_init中,我们要实现如下:
此时初始化SP指向 内存空间为IRAM(内部ram ,OCRAM 128K ,0x00900000),(初始化内存空间,为第二阶段准备ram)
** _main函数的主要工作是 **
ENTRY(_main)
/*
* Set up initial C runtime environment and call board_init_f(0).
*/
@ 为c语言环境准备
@ uboot-spl和uboot代码共用,CONFIG_SPL_BUILD来区分是谁调用
#if defined(CONFIG_SPL_BUILD) && defined(CONFIG_SPL_STACK)
ldr sp, =(CONFIG_SPL_STACK) @ 在spl中为 c语言环境设置栈
#else
ldr sp, =(CONFIG_SYS_INIT_SP_ADDR)
#endif
#if defined(CONFIG_CPU_V7M) /* v7M forbids using SP as BIC destination */
mov r3, sp
bic r3, r3, #7
mov sp, r3
#else
bic sp, sp, #7 /* 8-byte alignment for ABI compliance */
#endif
mov r0, sp
bl board_init_f_alloc_reserve @ 把栈前面的空间分配给GD
mov sp, r0 @重新设置指针SP
/* set up gd here, outside any C code */
mov r9, r0 @ 保存GD地址到r9寄存器
bl board_init_f_init_reserve @ 初始化GD空间
mov r0, #0
bl board_init_f @ 跳转到板级前期初始化函数,
#if ! defined(CONFIG_SPL_BUILD)
/*
* Set up intermediate environment (new sp and gd) and call
* relocate_code(addr_moni). Trick here is that we'll return
* 'here' but relocated.
*/
@ 设置sp和gd的中间环境
ldr sp, [r9, #GD_START_ADDR_SP] /* sp = gd->start_addr_sp */
#if defined(CONFIG_CPU_V7M) /* v7M forbids using SP as BIC destination */
mov r3, sp
bic r3, r3, #7
mov sp, r3
#else
bic sp, sp, #7 /* 8-byte alignment for ABI compliance */
#endif
ldr r9, [r9, #GD_BD] /* r9 = gd->bd */
sub r9, r9, #GD_SIZE /* new GD is below bd */
adr lr, here
ldr r0, [r9, #GD_RELOC_OFF] /* r0 = gd->reloc_off */
add lr, lr, r0
#if defined(CONFIG_CPU_V7M)
orr lr, #1 /* As required by Thumb-only */
#endif
ldr r0, [r9, #GD_RELOCADDR] /* r0 = gd->relocaddr */
b relocate_code @ 重定位代码
here:
/*
* now relocate vectors
*/
bl relocate_vectors @重定位向量表
/* Set up final (full) environment */
@设置最终的环境
bl c_runtime_cpu_setup /* we still call old routine here */
#endif
#if !defined(CONFIG_SPL_BUILD) || defined(CONFIG_SPL_FRAMEWORK)
# ifdef CONFIG_SPL_BUILD
/* Use a DRAM stack for the rest of SPL, if requested */
bl spl_relocate_stack_gd
cmp r0, #0
movne sp, r0
movne r9, r0
# endif
ldr r0, =__bss_start /* this is auto-relocated! */
#ifdef CONFIG_USE_ARCH_MEMSET
ldr r3, =__bss_end /* this is auto-relocated! */
mov r1, #0x00000000 /* prepare zero to clear BSS */
subs r2, r3, r0 /* r2 = memset len */
bl memset
#else
ldr r1, =__bss_end /* this is auto-relocated! */
mov r2, #0x00000000 /* prepare zero to clear BSS */
clbss_l:cmp r0, r1 /* while not at end of BSS */
#if defined(CONFIG_CPU_V7M)
itt lo
#endif
strlo r2, [r0] /* clear 32-bit BSS word */
addlo r0, r0, #4 /* move to next */
blo clbss_l
#endif
#if ! defined(CONFIG_SPL_BUILD)
bl coloured_LED_init
bl red_led_on
#endif
/* call board_init_r(gd_t *id, ulong dest_addr) */
mov r0, r9 /* gd_t */
ldr r1, [r9, #GD_RELOCADDR] /* dest_addr */
/* call board_init_r */
@ 跳转board_init_r 设置最终环境
#if defined(CONFIG_SYS_THUMB_BUILD)
ldr lr, =board_init_r /* this is auto-relocated! */
bx lr
#else
ldr pc, =board_init_r /* this is auto-relocated! */
#endif
/* we should not return here. */
#endif
ENDPROC(_main)
因为后面是C语言环境,首先是设置堆栈
初始化gd(下图中global date,内部ram) , 进行清零(同上内部ram)
调用 board_init_f 函数(将SP指针从内部IRAM,转移到外部DDR),主要用来初始化 DDR,定时器,完成代码拷贝等等
调用函数 relocate_code,也就是代码重定位函数,此函数负责将 uboot 拷贝到新的地方去
调用函数 relocate_vectors,对中断向量表做重定位
清除 BSS 段 , 。
bss段不占用空间,都是未初始化的全局变量或者已经初始化为零的变量,本来就是零,直接清零就好。不清零的话未初始化的变量可能会存在未知的数值。
设置函数 board_init_r 的两个参数 , 调用 board_init_r 函数 ,
board_init_r 函数打印一些列的信息到串口,然后会进入main_loop() 。main_loop会进行倒计时,如果此时按下回车就会进入uboot的shell交互界面,否则就会自动引导启动OS系统。
重定位就是 uboot 将自身拷贝到 DRAM 的另一个地放去继续运行(DRAM 的高地址处)。我们知道,一个可执行的 bin 文件,其链接地址和运行地址要相等,也就是链接到哪个地址,在运行之前就要拷贝到哪个地址去。现在我们重定位以后,运行地址就和链接地址不同了,这样寻址的时候不会出问题吗?
分析问题产生原因
r0 = gd->relocaddr = 0x9ff47000 , uboot重定位后的首地址
r1 = 0x87800000 源地址的首地址
r2 = 0x8785dc6c 源地址的结束地址
r4 = 0x9ff46000 - 0x87800000 = 0x18747000 偏移量
拷贝是从r1复制往r0粘贴 , 一次两个32位
当r1等于r2,拷贝完成
当简单粗暴的将uboot从0x87800000拷贝到0x9ff47000 ,程序运行时地址和连接地址不同,发生错误。uboot解决方法是使用位置无关码,借用 .rel.dyn 段
使用位置无关码解重定位后和连接地址不同问题的原理
举例: board_init 函数会调用 rel_test,rel_test 会调用全局变量 rel_a
源代码
static int rel_a = 0;
void rel_test(void)
{
rel_a = 100;
printf("rel_test\r\n");
}
int board_init(void)
{
...
rel_test();
...
}
反汇编代码
8785dcf8 <rel_a>:
8785dcf8: 00000000 andeq r0, r0, r0
878042b4 <rel_test>:
878042b4: e59f300c ldr r3, [pc, #12] ; 878042c8 <rel_test+0x14>
878042b8: e3a02064 mov r2, #100 ; 0x64
878042bc: e59f0008 ldr r0, [pc, #8] ; 878042cc <rel_test+0x18>
878042c0: e5832000 str r2, [r3]
878042c4: ea00d64c b 87839bfc <printf>
878042c8: 8785dcf8 ; <UNDEFINED> instruction: 0x8785dcf8
878042cc: 87842aaf strhi r2, [r4, pc, lsr #21]
从反汇编代码中分析:
重定位后,地址变化
9ffa4cf8 <rel_a>:
9ffa4cf8: 00000000 andeq r0, r0, r0
9ff4b2b4<rel_test>:
9ff4b2b4: e59f300c ldr r3, [pc, #12] ; 878042c8 <rel_test+0x14>
9ff4b2b8: e3a02064 mov r2, #100 ; 0x64
9ff4b2bc: e59f0008 ldr r0, [pc, #8] ; 878042cc <rel_test+0x18>
9ff4b2c0: e5832000 str r2, [r3]
9ff4b2c4: ea00d64c b 87839bfc <printf>
9ff4b2c8: 8785dcf8 ; <UNDEFINED> instruction: 0x8785dcf8
9ff4b2cc: 87842aaf strhi r2, [r4, pc, lsr #21]
uboot 中使用 .rel.dyn 段具体实现位置无关码的原理
完成这个功能在连接的时候需要加上”-pie”
.rel.dyn 段代码段
8785dcec: 87800020 strhi r0, [r0, r0, lsr #32]
8785dcf0: 00000017 andeq r0, r0, r7, lsl r0
……
8785e2fc: 878042c8 strhi r4, [r0, r8, asr #5]
8785e300: 00000017 andeq r0, r0, r7, lsl r0
board_init_f 并没有初始化所有的外设,还需要做一些后续工作,这些后续工作就是由函数 board_init_r 来完成的
uboot relocate后的板级初始化 ,最后执行run_main_loop
static int run_main_loop(void)
{
/* main_loop() can return to retry autoboot, if so just run it again */
for (;;)
main_loop();
// 这里进入了主循环,而autoboot也是在主循环里面实现
return 0;
}
进入了main_loop()函数。
void main_loop(void)
{
const char *s;
bootstage_mark_name(BOOTSTAGE_ID_MAIN_LOOP, "main_loop");
// 这里用于标记uboot的进度,对于tiny210来说起始什么都没做
cli_init();
// cli的初始化,主要是hush模式下的初始化
run_preboot_environment_command();
// preboot相关的东西,后续有用到再说明
s = bootdelay_process();
if (cli_process_fdt(&s))
cli_secure_boot_cmd(s);
autoboot_command(s);
// autoboot的东西,后续使用autoboot的时候再专门说明
cli_loop();
// 进入cli的循环模式,也就是命令行模式
panic("No CLI available");
}
cli_loop 函数是 uboot 的命令行处理函数,我们在 uboot 中输入各种命令,进行各种操作就是有 cli_loop 来处理的
void cli_loop(void)
{
#ifdef CONFIG_SYS_HUSH_PARSER
parse_file_outer();
/* This point is never reached */
for (;;);
#else
cli_simple_loop(); //永远不会执行
#endif /*CONFIG_SYS_HUSH_PARSER*/
}
int parse_file_outer(void)
{
int rcode;
struct in_str input;
setup_file_in_str(&input);
rcode = parse_stream_outer(&input, FLAG_PARSE_SEMICOLON);
return rcode;
}
static int parse_stream_outer(struct in_str *inp, int flag)
{
struct p_context ctx;
o_string temp=NULL_O_STRING;
int rcode;
int code = 1;
do {
......
rcode = parse_stream(&temp, &ctx, inp,
flag & FLAG_CONT_ON_NEWLINE ? -1 : '\n');
......
if (rcode != 1 && ctx.old_flag == 0) {
......
run_list(ctx.list_head);
......
} else {
......
}
b_free(&temp);
/* loop on syntax errors, return on EOF */
} while (rcode != -1 && !(flag & FLAG_EXIT_FROM_LOOP) &&
(inp->peek != static_peek || b_peek(inp)));
return 0;
}
uboot把所有命令的数据结构都放在一个表格中,我们后续称之为命令表。表中的每一项代表着一个命令,其项的类型是cmd_tbl_t。
struct cmd_tbl_s {
char *name; /* Command Name */
int maxargs; /* maximum number of arguments */
int repeatable; /* autorepeat allowed? */
/* Implementation function */
int (*cmd)(struct cmd_tbl_s *, int, int, char * const []);
char *usage; /* Usage message (short) */
#ifdef CONFIG_SYS_LONGHELP
char *help; /* Help message (long) */
#endif
};
typedef struct cmd_tbl_s cmd_tbl_t;
参数说明如下:
name:定义一个命令的名字。 其实就是执行的命令的字符串。这个要注意。
maxargs:这个命令支持的最大参数
repeatable:是否需要重复
cmd:命令处理函数的地址
usage:字符串,使用说明
help:字符串,帮助
Uboot使用U_BOOT_CMD来定义一个命令。CONFIG_CMD_XXX来使能uboot中的某个命令。
//uboot命令定义代码
#define U_BOOT_CMD(_name, _maxargs, _rep, _cmd, _usage, _help)
U_BOOT_CMD最终是定义了一个cmd_tbl_t类型的结构体变量,所有的命令最终都是存放在.u_boot_list段里面。cmd_tbl_t结构体中的cmd成员变量就是具体的命令执行函数,命令执行函数都是do_xxx。
// bootm就是我们的命令字符串
// 在tiny210.h中定义了最大参数数量是64
// #define CONFIG_SYS_MAXARGS 64 /* max number of command args */
// 1表示重复一次
// 对应命令处理函数是do_bootm
// usage字符串是"boot application image from memory"
// help字符串是bootm_help_text定义的字符串。
U_BOOT_CMD(
bootm, CONFIG_SYS_MAXARGS, 1, do_bootm,
"boot application image from memory", bootm_help_text
);
//并且命令处理函数的格式如下:
//当命令处理函数执行成功时,需要返回0.返回非0值
int do_bootm(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[])
函数 cmd_process 来处理命令并执行
函数 cmd_process代码
enum command_ret_t cmd_process(int flag, int argc, char * const argv[],
int *repeatable, ulong *ticks)
{
enum command_ret_t rc = CMD_RET_SUCCESS;
cmd_tbl_t *cmdtp;
/* Look up command in command table */
cmdtp = find_cmd(argv[0]);
// 第一个参数argv[0]表示命令,调用find_cmd获取命令对应的表项cmd_tbl_t。
if (cmdtp == NULL) {
printf("Unknown command '%s' - try 'help'\n", argv[0]);
return 1;
}
/* found - check max args */
if (argc > cmdtp->maxargs)
rc = CMD_RET_USAGE;
// 检测参数是否正常
/* If OK so far, then do the command */
if (!rc) {
if (ticks)
*ticks = get_timer(0);
rc = cmd_call(cmdtp, flag, argc, argv);
// 调用cmd_call执行命令表项中的命令,成功的话需要返回0值
if (ticks)
*ticks = get_timer(*ticks);
// 判断命令执行的时间
*repeatable &= cmdtp->repeatable;
// 这个命令执行的重复次数存放在repeatable中的
}
if (rc == CMD_RET_USAGE)
rc = cmd_usage(cmdtp);
// 命令格式有问题,打印帮助信息
return rc;
}
在run_main_loop函数倒计时结束时没有输入,uboot执行bootcmd命令,启动内核
bootcmd 保存着 uboot 默认命令,这些命令一般都是用来启动 Linux 内核的,读取Linux镜像文件和设备树文件(从MMC、SD卡、网络读取等)到DARM(DDR)中,然后启动内核(bootz、bootm)。
bootm和bootz的不同地方
bootm ${kernel_load_address} ${ramdisk_load_address} ${devicetree_load_address}
bootz ${kernel_load_address} ${ramdisk_load_address} ${devicetree_load_address}
bootz的使用(tftp从网络读取)
tftp 80800000 zImage
tftp 83000000 imx6ull-14x14-emmc-7-1024x600-c.dtb
bootz 80800000 - 83000000
bootz流程
do_bootz是bootz的执行函数
do_bootz
kernel_entry函数是内核的入口函数。内核镜像第一行代码
kernel_entry(0, machid, r2);
此函数有三个参数:zero,arch,params,
kernel_entry函数的传参
如何从uboot跳转到内核
直接修改PC寄存器的值为Linux内核所在的地址,CPU从内核所在的地址去取指令,从而执行内核代码
为什么要传参数给内核
在此之前, uboot已经完成了硬件的初始化,可以说已经噎适应了“这块开发板。然而,内核并不是对于所有的开发板都能完美适配的(如果适配了,可想而知这个内核有多庞大,又或者有新技术发明了,可以完美的适配各种开发板),此时,对于开发板的环境一无所知。所以,要想启动 Linux内核, uboot必须要给内核传递一些必要的信息来告诉内核当前所处的环境。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。