前言
u-boot的作用:CPU上电后,需要设置很多状态,包括CPU状态、中断状态、MMU状态等,其次要做的就是对硬件资源经行板级初始化、代码重定向等,最后若不进入命令行模式,就会将linux内核从flash(NAND,NOR FLASH,SD,MMC等)拷贝到DDR中,最后启动linux内核。
4412 u-boot启动流程:
A.开机运行iRom中代码
B.BL1阶段(E4412_N.bl1.xxxxG.bin:8KB,三星提供的bin文件,没有源码)
C.BL2(SPL)阶段(bl2.bin :16KB,uboot启动第一阶段代码,在uboot中称为SPL阶段)
D.uboot第二阶段代码(u-boot.bin :小于512KB,uboot启动第二阶段代码)
接下来,我就会按以上流程逐步开始分析uboot启动流程。
1.开机运行iRom中代码
4412内部存在一个大小位64Kb的iRom,物理内存首地址为0x0000_0000。ARM芯片启动时从物理地址0x0000_0000的存储介质中取第一条指令开始运行。三星在iROM中固化了一段启动代码,因此,芯片上电时首先运行iROM中的这段代码。这段代码主要做以下4个工作:
1. 初始化出程序运行的基本环境,比如关闭看门狗、设置栈以及初始化时钟等
2. 读取OM脚判断uboot的启动介质(内部EMMC、外部SD/MMC卡等)
3. 从启动介质加载BL1阶段代码到iRAM中特定的地址(0x0202_1400)处,4412的iRAM有256Kb,起始物理地址是0x0202_0000
4. iROM中的启动代码对BL1代码做完整性校验,校验通过后,跳转到iRAM中的BL1运行
2.BL1阶段
BL1阶段主要做如下工作:
1. 从启动介质拷贝BL2代码(SPL阶段)到iRAM的0x0202_3400处,因此在编译uboot时,BL2的链接地址需要设置为0x0202_3400
./include/configs/itop4412.h
/* MMC SPL */
#define COPY_BL2_FNPTR_ADDR 0x02020030
#define CONFIG_SPL_TEXT_BASE 0x02023400 /* 0x02021410 */
2. 校验BL2的完整性,通过后跳到BL2运行
在BL2中有两个数据Checksum和Signature,BL1对BL2的校验就是检查这两个数据。BL2有效程序必须小于14332B,14KB-4B的地方用于存放Checksum,14KB的地方用于存放Signature
Uboot2015中,编译出uboot的第一阶段SPL后,使用board/Samsung/itop4412/tools/mkitop4412spl.c计算校验和放到14KB-4B的地方。
编译SPL阶段的脚本文件scripts/Makefile.spl中调用mkitop4412spl
3.SPL阶段
至此uboot开始运行,BL2在iRAM中运行,主要的工作是:
1. 初始化时钟、初始化串口、初始化动态内存DRAM
2. 拷贝uboot第二阶段代码到DRAM中,然后跳转到DRAM中执行uboot第二阶段代码
详细过程见下段描述:
3.1.从链接脚本u-boot.lds开始
前言: u-boot.lds -找到u-boot启动入口的钥匙
程序的链接是由链接脚本来决定的,所以通过链接脚本可以找到程序的入口。uboot在未编译之前,可以在uboot代码里的arch/arm/cpu/下找到u-boot.lds,在编译后会基于这个.lds文件,生成最终使用的.lds,4412的lds源码如下:
OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
OUTPUT_ARCH(arm)
ENTRY(_start)
SECTIONS
{
. = 0x00000000;
. = ALIGN(4);
.text :
{
cpu/arm_cortexa9/start.o (.text)
cpu/arm_cortexa9/s5pc210/cpu_init.o (.text)
board/samsung/smdkc210/lowlevel_init.o (.text)
common/ace_sha1.o (.text)
*(.text)
}
. = ALIGN(4);
.rodata : { *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.rodata*))) }
. = ALIGN(4);
.data : { *(.data) }
. = ALIGN(4);
.got : { *(.got) }
__u_boot_cmd_start = .;
.u_boot_cmd : { *(.u_boot_cmd) }
__u_boot_cmd_end = .;
. = ALIGN(4);
__bss_start = .;
.bss : { *(.bss) }
_end = .;
}
根据第3行可以确定SPL阶段的入口点是_start。_start在源码arch/arm/lib/vectors.S中有定义:
*
*************************************************************************
*
* Symbol _start is referenced elsewhere, so make it global
*
*************************************************************************
*/
.globl _start
/*
*************************************************************************
*
* Vectors have their own section so linker script can map them easily
*
*************************************************************************
*/
.section ".vectors", "ax"
/*
*************************************************************************
*
* Exception vectors as described in ARM reference manuals
*
* Uses indirect branch to allow reaching handlers anywhere in memory.
*
*************************************************************************
*/
_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
SPL阶段的入口代码使用汇编写的,从_start入口点开始,进入复位中断向量执行点reset,开始设置程序运行的基本环境,比如将CPU设置为SVC模式、设置栈、关闭看门狗等;然后进入第一个c函数board_init_f,这个函数中会调用do_lowlevel_init初始化SOC内部的组件,比如系统时钟、串口、DRAM等,然后再调用copy_uboot_to_ram将uboot第二阶段代码拷贝到DRAM,并跳转到uboot第二阶段执行,详细调用过程如下图所示:
4.uboot第二阶段代码
uboot第二阶段的入口点和SPL阶段的入口点是一样的,都是从Arch/arm/lib/ vectors.S的_start进入arch/arm/cpu/armv7/start.S的reset。
SPL阶段已经完成了SOC内部各组件的初始化工作,第二阶段uboot会初始化SOC外部的一些组件比如存储设备EMMC/SD、网卡等,然后会实现uboot代码的主体功能比如环境变量、命令行、启动内核等功能。
以下会对流程进行详细分析:
4.1 arch/arm/lib/vectors.S
_start->reset
工作:uboot第二阶段入口点;跳转到reset
4.2 arch/arm/cpu/armv7/start.S
reset
工作:禁用中断,设置CPU为SVC模式(ARM处理器7种工作模式之一,系统复位或开机、软中断时进入到SVC模式);跳转到_main
4.3 arch/arm/lib/crto.S
_main
工作:
1.在DRAM中设置栈
2.跳转到 board_init_f(uboot启动中的第一个c文件)
4.3.1 commom/board_f.c
直接看源码
void board_init_f(ulong boot_flags)
{
gd->flags = boot_flags;
gd->have_console = 0;
if (initcall_run_list(init_sequence_f))
hang();
#if !defined(CONFIG_ARM) && !defined(CONFIG_SANDBOX) && \
!defined(CONFIG_EFI_APP) && !CONFIG_IS_ENABLED(X86_64)
/* NOTREACHED - jump_to_copy() does not return */
hang();
#endif
}
可以看到函数最主要的语句就是initcall_run_list(init_sequence_f),init_sequence_f是一个函数指针数组,因为存在很多预编译条件判断就不再这里赘述源码了,感兴趣的同学可以直接看源码。init_sequence_f数组里面放了很多初始化gd结构体的函数指针,重要的几个函数如下:
1.int reloc_fdt:重定位设备树
static int reloc_fdt(void)
{
#ifndef CONFIG_OF_EMBED
if (gd->flags & GD_FLG_SKIP_RELOC)
return 0;
if (gd->new_fdt) {
memcpy(gd->new_fdt, gd->fdt_blob, gd->fdt_size);
gd->fdt_blob = gd->new_fdt;
}
#endif
return 0;
}
2.env_init初始化env
int env_init(void)
{
struct env_driver *drv = env_driver_lookup_default();
int ret = -ENOENT;
if (!drv)
return -ENODEV;
if (drv->init)
ret = drv->init();
if (ret == -ENOENT) {
gd->env_addr = (ulong)&default_environment[0];
gd->env_valid = ENV_VALID;
return 0;
} else if (ret) {
debug("%s: Environment failed to init (err=%d)\n", __func__,
ret);
return ret;
}
return 0;
}
3.dram_init:gd->bd中关于DDR配置部分全局变量的赋值(大小 起始地址 等)
int dram_init(void)
{
/* We do not initialise DRAM here. We just query the size */
gd->ram_size = query_sdram_size();
return 0;
}
......受限于篇幅,对init_sequence_f函数组感兴趣的小伙伴可以直接去看一下源码。
4.3.2 跳转 relocate
b relocate_code
工作:代码段重定位,因为之前已经重定位过,所以该函数判断完条件后直接返回
4.3.3 跳转 relocate_vectors
b relocate_vectors
工作:重定位中断向量表
4.3.4 清零bss段
bss段通常是指用来存放程序中未初始化的或者初始化为0的全局变量和静态变量的一块内存区域。特点是可读写的,感兴趣的小伙伴可以深入了解一下!
4.3.5 调用board_init_r
/* 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 */
#if CONFIG_IS_ENABLED(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
之前讲解了 board_init_f 函数,在此函数里面会调用一系列的函数来初始化一些外设和 gd 的成员变量。但是 board_init_f 并没有初始化所有的外设,还需要做一些后续工作,这些后续工作就是由函数 board_init_r 来完成的,board_init_r 函数定义在文件 common/board_r.c中:
void board_init_r(gd_t *new_gd, ulong dest_addr)
{
/*
* Set up the new global data pointer. So far only x86 does this
* here.
* TODO(sjg@chromium.org): Consider doing this for all archs, or
* dropping the new_gd parameter.
*/
#if CONFIG_IS_ENABLED(X86_64)
arch_setup_gd(new_gd);
#endif
#ifdef CONFIG_NEEDS_MANUAL_RELOC
int i;
#endif
#if !defined(CONFIG_X86) && !defined(CONFIG_ARM) && !defined(CONFIG_ARM64)
gd = new_gd;
#endif
#ifdef CONFIG_NEEDS_MANUAL_RELOC
for (i = 0; i < ARRAY_SIZE(init_sequence_r); i++)
init_sequence_r[i] += gd->reloc_off;
#endif
if (initcall_run_list(init_sequence_r))
hang();
/* NOTREACHED - run_main_loop() does not return */
hang();
}
第26行,调用 initcall_run_list 函数来执行初始化序列 init_sequence_r,init_sequence_r 是一个函数集合,init_sequence_r 也定义在文件 common/board_r.c 中,由于 init_sequence_f 的内容比较长,里面有大量的条件编译代码,受限于篇幅,也像之前分析最具代表性的几个。
1.initr_serial:初始化传串口
static int initr_serial(void)
{
serial_initialize();
return 0;
}
2.interrupt_init:初始化中断
int interrupt_init (void)
{
/*
* setup up stacks if necessary
*/
IRQ_STACK_START_IN = gd->irq_sp + 8;
return 0;
}
3.initr_enable_interrupts:使能中断
static int initr_enable_interrupts(void)
{
enable_interrupts();
return 0;
}
4.initr_env:初始化环境变量
static int initr_env(void)
{
/* initialize environment */
if (should_load_env()) // 从指定设备加载环境变量,并验证有效性
env_relocate();
else
set_default_env(NULL); // 失败使用默认的
#ifdef CONFIG_OF_CONTROL
env_set_addr("fdtcontroladdr", gd->fdt_blob);
#endif
/* Initialize from environment */
load_addr = env_get_ulong("loadaddr", 16, load_addr);
return 0;
}
5.run_main_loop:主循环,处理命令
static int run_main_loop(void)
{
#ifdef CONFIG_SANDBOX
sandbox_main_loop_init();
#endif
/* main_loop() can return to retry autoboot, if so just run it again */
for (;;)
main_loop();
return 0;
}
uboot 启动以后会进入 3 秒倒计时,如果在 3 秒倒计时结束之前按下按下回车键,那么就
会进入 uboot 的命令模式,如果倒计时结束以后都没有按下回车键,那么就会自动启动 Linux 内
核,这个功能就是由 run_main_loop 函数来完成的。
6.main_loop:
main_loop()在common/main.c
/* We come here after U-Boot is initialised and ready to process commands */
void main_loop(void)
{
const char *s;
bootstage_mark_name(BOOTSTAGE_ID_MAIN_LOOP, "main_loop"); // 打印启动进度
#ifdef CONFIG_VERSION_VARIABLE
env_set("ver", version_string); /* set version variable,cmd/version.c */
#endif /* CONFIG_VERSION_VARIABLE */
cli_init(); // 初始化 hushshell 相关的变量
run_preboot_environment_command(); // 获取环境变量 perboot 的内容,perboot是一些预启动命令,一般不使用这个环境变量
#if defined(CONFIG_UPDATE_TFTP)
update_tftp(0UL, NULL, NULL);
#endif /* CONFIG_UPDATE_TFTP */
s = bootdelay_process();
if (cli_process_fdt(&s))
cli_secure_boot_cmd(s);
autoboot_command(s); // 此函数就是检查倒计时是否结束、被打断
cli_loop(); // uboot 的命令行处理函数
panic("No CLI available");
}
结尾
经过以上四个阶段,uboot便走完了它的一生,接下来便是linux的启动!
参考
1.https://www.cnblogs.com/lztutumo/p/13233094.html 番茄大佬的uboot启动流程,写得比较好,可以看看!