前言

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中代码

u-boot启动流程详解-基于iTop4412开发板_iTop4412

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

u-boot启动流程详解-基于iTop4412开发板_linux_02

      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

    u-boot启动流程详解-基于iTop4412开发板_iTop4412_03

Uboot2015中,编译出uboot的第一阶段SPL后,使用board/Samsung/itop4412/tools/mkitop4412spl.c计算校验和放到14KB-4B的地方。

  编译SPL阶段的脚本文件scripts/Makefile.spl中调用mkitop4412spl

u-boot启动流程详解-基于iTop4412开发板_linux_04

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

      u-boot启动流程详解-基于iTop4412开发板_嵌入式_05

      SPL阶段的入口代码使用汇编写的,从_start入口点开始,进入复位中断向量执行点reset,开始设置程序运行的基本环境,比如将CPU设置为SVC模式、设置栈、关闭看门狗等;然后进入第一个c函数board_init_f,这个函数中会调用do_lowlevel_init初始化SOC内部的组件,比如系统时钟、串口、DRAM等,然后再调用copy_uboot_to_ram将uboot第二阶段代码拷贝到DRAM,并跳转到uboot第二阶段执行,详细调用过程如下图所示:

                          u-boot启动流程详解-基于iTop4412开发板_嵌入式_06

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启动流程,写得比较好,可以看看!