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

87 篇文章 28 订阅
13 篇文章 0 订阅

目录

1.开机运行iRom中代码

2.BL1阶段

3.SPL阶段

3.1.从链接脚本u-boot.lds开始

4.uboot第二阶段代码

4.1 arch/arm/lib/vectors.S

4.2 arch/arm/cpu/armv7/start.S

4.3 arch/arm/lib/crto.S

4.3.1 commom/board_f.c

4.3.2 跳转 relocate

4.3.3 跳转 relocate_vectors

4.3.4 清零bss段

4.3.5 调用board_init_r

结尾

参考


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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值