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

案头见蠹鱼,犹胜凡俦侣。这篇文章主要讲述u-boot启动流程详解-基于iTop4412开发板相关的知识,希望能为你提供帮助。
前言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_ADDR0x02020030
#define CONFIG_SPL_TEXT_BASE0x02023400 /* 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_vectorsb relocate_vectors
工作:重定位中断向量表
4.3.4 清零bss段      bss段通常是指用来存放程序中未初始化的或者初始化为0的全局变量和静态变量的一块内存区域。特点是可读写的,感兴趣的小伙伴可以深入了解一下!
4.3.5 调用board_init_r
/* call board_init_r(gd_t *id, ulong dest_addr) */
movr0, 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! */
bxlr
#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的启动!
参考【u-boot启动流程详解-基于iTop4412开发板】1.??https://www.cnblogs.com/lztutumo/p/13233094.html?? 番茄大佬的uboot启动流程,写得比较好,可以看看!

    推荐阅读