当前位置:   article > 正文

uboot启动流程详细分析(基于i.m6ull)

uboot启动流程

uboot介绍

uboot就是一段引导程序在加载系统内核之前,完成硬件初始化,内存映射,为后续内核的引导提供一个良好的环境。uboot是bootloader的一种,全称为universal boot loader。

一、uboot的makefile

1.1 makefile整体解析过程

为了生成u-boot.bin这个文件,首先要生成构成u-boot.bin的各个库文件、目标文件。为了各个库文件、目标文件就必须进入各个子目录执行其中的Makefile。由此,确定了整个编译的命令和顺序。

1.2 makefile整体编译过程

  • 首先,根据各个库文件、目标文件出现的先后顺序,依次进入各个子目录编译从而生成这些目标
  • 然后,回到顶层目录,继续执行顶层Makefile的总目标,最后生成u-boot.bin。

uboot的编译分为两步:配置、编译。

(1)第一步:配置,执行make pangu_basic_defconfig进行配置,生成.config文件
在这里插入图片描述

(2)第二步:编译,执行make进行编译,生成u-boot.bin
在这里插入图片描述

二、uboot启动流程

在这里插入图片描述

  • uboot分为 uboot-spl 和 uboot 两个组成部分。

uboot启动分三个阶段

  1. BL0
    ROM上的固化程序(Boot Rom)

  2. BL1(u-boot-spl)

  • 初始化部分时钟(和SDRAM相关)
  • 初始化DDR(外部SDRAM)
  • 从存储介质上(比如SD\eMMC\nand flash)将BL2镜像加载到SDRAM上
  • 验证BL2镜像的合法性
  • 跳转到BL2镜像所在的地址上
  1. BL2 (uboot)
  • 初始化部分硬件,包括时钟、内存等等
  • 加载内核到内存上
  • 加载文件系统、atags或者dtb到内存上
  • 根据操作系统启动要求正确配置好一些硬件

启动操作系统

2.1 uboot的链接文件(u-boot.lds)

链接文件的作用

  1. 指定代码段和数据段、只读数据段在内存中的存放地址;(地址具体为i.m6ull , 其他芯片可能不是 0X87800000)

    • u-boot.map 是 uboot 的映射文件,看到某个文件或者函数链接到了哪个地址,

    • __image_copy_start 为 0X87800000,而.text 的起始地址也是0X87800000。

    • vectors 段保存中断向量表,vectors 段的起始地址也是 0X87800000,说明整个 uboot 的起始地址就是 0X87800000,

    • 这也是为什么我们裸机例程的链接起始地址选择 0X87800000 了,目的就是为了和 uboot 一致。
      在这里插入图片描述
      在这里插入图片描述

  2. 指定代码的入口地址;

    • 连接文件中找到程序的入口点:_start, 其中_start 在文件 arch/arm/lib/vectors.S 。

2.2 uboot启动流程

第一阶段(uboot-spl , 入口是上述lds文件中分析的_start)

  • SPL是Secondary Program Loader的简称,第二阶段程序加载器,这里所谓的第二阶段是相对于SOC中的Boot ROM来说的

Boot ROM会通过检测启动方式来加载第二阶段bootloader。uboot已经是一个bootloader了,那么为什么还多一个uboot spl呢?

  • 这个主要原因是对于一些SOC来说,它的内部SRAM可能会比较小,小到无法装载下一个完整的uboot镜像,那么就需要spl,它主要负责初始化外部RAM和环境,并加载真正的uboot镜像到外部RAM(DDR)中来执行。

  • 所以由此来看,SPL应该是一个非常小的loader程序,可以运行于SOC的内部SRAM中,它的主要功能就是加载真正的uboot并运行之。

2.2.1. 进入_start函数:

_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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

有一条跳转指令b reset跳转到reset函数处去执行
注意,spl的流程在reset中就应该被结束,也就是说在reset中,就应该转到到BL2,也就是uboot中了。

2.2.2. 进入reset函数

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的主体部分	
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

进入reset函数,首先设置cpu为SVC模式关闭中断。然后跳转到cpu_init_cp15初始化协处理器CP15,从而禁用MMU和TLB。跳转到cpu_init_crit ,进行一些关键的初始化动作,也就是平台级和板级的初始化。最后跳转到**_main**,加载BL2以及跳转到BL2的主体部分

  • 关闭中断。
    uboot引导linux起到的过程中本身就是一个完成的过程,不需要中断机制。

2.2.3 cpu_init_cp15函数

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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

cpu_init_cp15 用来设置 CP15 相关的内容,完成启动ICACHE,关闭DCACHE,关闭MMU和TLB

  • 关闭MMU
    ,MMU是用于虚拟地址向物理地址进行映射的一个结构。在 uboot阶段操作的就直接是 物理地址,所以不需要转换。
  • 启动ICACHE(指令),关闭DCACHE(数据)
    启动指令CACHE课可以加快指令读取的速度,但是数据CACHE 必须 要关闭,因为它本身是一个CPU的二级缓存,在运行程序的时候可能会往里面去取数据,但是此时ram里面的数据可能并没有存入到里面,这就可能导致读取到错误的数据。

2.2.4 cpu_init_crit函数(包含lowlevel_init函数)

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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

cpu_init_crit函数的内容是跳转到lowlevel_init函数。

2.2.5. lowlevel_init 函数(平台级和板级的初始化)

lowlevel_init主要完成平台级和板级的初始化
在lowlevel_init中,我们要实现如下:

  • 检查一些复位状态
  • 关闭看门狗
  • 系统时钟的初始化
  • 内存、DDR的初始化
  • 串口初始化(可选)
  • Nand flash的初始化
在im6ull中

此时初始化SP指向 内存空间为IRAM(内部ram ,OCRAM 128K ,0x00900000),(初始化内存空间,为第二阶段准备ram)
在这里插入图片描述

2.2.6. _main函数

** _main函数的主要工作是 **

  • 设置c语言的运行环境
  • 设置sp和gd的中间环境
  • 重定义代码
  • 设置最终的环境
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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 因为后面是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系统。

2.2.7. board_init_f 函数

  1. 初始化一系列外设,比如串口、定时器,或者打印一些消息等。
  2. 重新设置环境(sp 和 gd) , 新的 sp 和 gd 将会存放到 DDR 中(外部),而不是内部的 RAM 了
    • uboot 会将自己重定位到 DRAM(DDR) 最后面的地址区域,也就是将自己拷贝到 DRAM 最后面的内存区域中。这么做的目的是给 Linux 腾出空间,防止 Linuxkernel 覆盖掉 uboot,将 DRAM 前面的区域完整的空出来。
    • 在拷贝之前肯定要给 uboot 各部分分配好内存位置和大小,比这些信息都保存在 gd 的成员变量中(从板子配置文件里读取),因此要对 gd 的这些成员变量做初始化。最终形成一个完整的内存“分配图”,在后面重定位 uboot 的时候就会用到这个内存“分配图”(外部)。
    • 注:上电后芯片内部Boot ROM把uboot搬移到DRAM头部(0x87800000),重定位则再搬移到DDR后部
      在这里插入图片描述

2.2.8. relocate_code 函数

  • relocate_code 函数是用于代码拷贝

重定位就是 uboot 将自身拷贝到 DRAM 的另一个地放去继续运行(DRAM 的高地址处)。我们知道,一个可执行的 bin 文件,其链接地址和运行地址要相等,也就是链接到哪个地址,在运行之前就要拷贝到哪个地址去。现在我们重定位以后,运行地址就和链接地址不同了,这样寻址的时候不会出问题吗?

代码动态重定位原理

  1. 分析问题产生原因

    r0 = gd->relocaddr = 0x9ff47000 , uboot重定位后的首地址
    r1 = 0x87800000 源地址的首地址
    r2 = 0x8785dc6c 源地址的结束地址
    r4 = 0x9ff46000 - 0x87800000 = 0x18747000 偏移量
    拷贝是从r1复制往r0粘贴 , 一次两个32位
    当r1等于r2,拷贝完成

    当简单粗暴的将uboot从0x87800000拷贝到0x9ff47000 ,程序运行时地址和连接地址不同,发生错误。uboot解决方法是使用位置无关码,借用 .rel.dyn 段

  2. 使用位置无关码解重定位后和连接地址不同问题的原理

    举例: 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();
    	...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    反汇编代码

    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]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    从反汇编代码中分析:

    • 想要找到 rel_a 的地址,首先 r3 = pc + 12 = 0x878042b4 + 8 + 12 = 0x878042c8 。(由ARM 流水线决定 pc = 当前地址 + 8)
    • 之后 r3 在0x878042c8 中存储数据为0x8785dcf8,即为rel_a地址
    • 这里没直接读取rel_a , 而是借助0x878042c8 。 0x878042c8 就是Label

    重定位后,地址变化

    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]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 这时 Label中的值还是重定位之前的,必须要将8785dcf8换为重定位后的rel_a地址
    • 重定位后的Label中的数据 = 0x878042c8(老的Label) + 0x18747000(uboot整体的偏移量) = 0x9ffa4cf8
    • 读取 rel_a 地址为 ,r3 = 0x9ff4b2b4 + 8 + 12 = 0x9ff4b2c8 ,即为Label地址,然后从Label中读取到 0x9ffa4cf8,为rel_a重定义后真是地址
  3. 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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • .rel.dyn 段的格式,也就是两个 4 字节数据为一组。
      高 4 字节是 Label 地址标识 0X17,低 4 字节就是 Label 的地址,
    • 第三行是878042c8,第四行是00000017 。说明878042c8是一个Label。正是上述分析中 存放 rel_a 地址的Label
    • 在重定位后,rel_a 真是地址 = Label内数据 + offset(0x18747000)

2.2.9 relocate_vectors函数

  • 中断向量表重定位后的地址,就是重定位后uboot的首地址

2.2.10. board_init_r 函数(板级初始化,进入第二阶段)

board_init_f 并没有初始化所有的外设,还需要做一些后续工作,这些后续工作就是由函数 board_init_r 来完成的
uboot relocate后的板级初始化 ,最后执行run_main_loop

第二阶段

2.2.11 run_main_loop函数

  • run_main_loop-> main_loop->
    • -> bootdelay_process 获取bootdelay的值,然后保存到stored_bootdelay
      全局变量里面,获取bootcmd环境变量值,并且将其
      返回
    • -> autoboot_command 参数是bootcmd的值。检查倒计时内有无打断
      • -> abortboot 参数为boot delay,此函数会处理倒计时
        • -> abortboot_normal 参数为boot delay,此函数会处理倒计时
      • ->run_command_list函数,参数为s(bootcmd的命令)倒计时结束时hush shell没输入执行,启动内核
    • -> cli_loop 是uboot命令模式处理函数。
      • -> parse_file_outer
        • -> parse_stream_outer
          • -> parse_stream 解析输入的字符,得到命令
          • -> run_list 运行命令
            • -> run_list_real
              • -> run_pipe_real
                • -> cmd_process 处理命令,也就是执行命令
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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

进入了main_loop()函数。

2.2.11 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");
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 调用 bootstage_mark_name 函数,打印出启动进度。
  • 调用 cli_init 函数,跟命令初始化有关,初始化 hush shell 相关的变量。( hush shell 为uboot倒计时结束的输入shell)
  • run_preboot_environment_command 函数,获取环境变量 perboot 的内容,perboot
    是一些预启动命令
  • bootdelay_process 函数,此函数会读取环境变量 bootdelay (延迟时间)和 bootcmd(启动内核命令) 的内容,
    然后将 bootdelay 的值赋值给全局变量 stored_bootdelay,返回值为环境变量 bootcmd 的值。
  • autoboot_command(s)函数,此函数就是检查倒计时是否结束?倒计时结束之前有
    没有被打断?输入参数s为bootcmd的命令
    • 倒计时自然结束执行函数run_command_list,此函数会执行参数 s 指定的一系列命令,也就是环境变量 bootcmd 的命令,bootcmd 里面保存着默认的启动命令,因此 linux 内核启动
    • 倒计时结束之前按下了键盘上的按键,那么 run_command_list函数就不会执行,执行后面的cli_loop函数
  • cli_loop函数,是 uboot 的命令行处理函数

2.2.12 cli_loop函数

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*/
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 调用 parse_file_outer函数

2.2.13 parse_file_outer函数

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;
 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 调用函数 setup_file_in_str 初始化变量 input 的成员变量
  • 调用函数 parse_stream_outer,这个函数就是 hush shell 的命令解释器,负责接收命令行输入,然后解析并执行相应的命令

2.3.14 函数 parse_stream_outer

 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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • do-while 循环就是处理输入命令的
  • 函数 parse_stream 进行命令解析
  • 调用 run_list 函数来执行解析出来的命令,run_list 调用 run_list_real 函数,run_list_real 函数调用 run_pipe_real 函数,run_pipe_real 函数调用 cmd_process 函数。最终通过函数 cmd_process 来处理命令

2.3.15 补充:uboot的命令

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;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

参数说明如下:

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) 
  • 1
  • 2

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[])
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

2.3.16 函数 cmd_process

函数 cmd_process 来处理命令并执行

  • cmd_process
    • ->find_cmd 从.u_boot_list段里面查找命令,当找到对应的命令以后以返回值的
      形式给出,为cmd_tbl_t类型
    • ->cmd_call
      ->cmdtp->cmd 直接引用cmd成员变量,执行do_xxx函数

函数 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;
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • find_cmd函数, 从.u_boot_list段获取命令对应的命令表项cmd_tbl_t 。
  • cmd_call函数,执行命令表项cmd_tbl_t 中的命令,执行do_xxx函数

2.3.17 uboot启动linux内核(bootz命令)

在run_main_loop函数倒计时结束时没有输入,uboot执行bootcmd命令,启动内核

  • bootcmd 保存着 uboot 默认命令,这些命令一般都是用来启动 Linux 内核的,读取Linux镜像文件和设备树文件(从MMC、SD卡、网络读取等)到DARM(DDR)中,然后启动内核(bootz、bootm)。

  • bootm和bootz的不同地方

    • bootm用于加载uImage和ramdisk
      bootm ${kernel_load_address} ${ramdisk_load_address} ${devicetree_load_address}
    • bootz用于加载zImage和ext4文件系统
      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
    
    • 1
    • 2
    • 3

bootz流程
在这里插入图片描述

2.3.18 do_bootz函数

do_bootz是bootz的执行函数
do_bootz

  • -> bootz_start
    • -> do_bootm_states 阶段为BOOTM_STATE_START
      • -> bootm_start 对images全局变量清零
    • -> images->ep = 0X80800000
    • ->bootz_setup 判断zImage是否正确
    • -> bootm_find_images 查找镜像文件
      • -> boot_get_fdt 找到设备树,然后将设备树起始地址和长度,写入到images的ft_addr和ft_len成员变量中。
    • -> bootm_disable_interrupts 关闭中断相关
    • -> images.os.os = IH_OS_LINUX; 表示要启动Linux系统
    • -> do_bootm_states 状态BOOTM_STATE_OS_PREP 、BOOTM_STATE_OS_FAKE_GO 、
      BOOTM_STATE_OS_GO,
      • -> bootm_os_get_boot_func 查找Linux内核启动函数。找到Linux内核启动函数
        do_bootm_linux,赋值给boot_fn。
      • -> boot_fn(BOOTM_STATE_OS_PREP, argc, argv, images); 就是do_bootm_linux。
        • -> boot_prep_linux 启动之前的一些工作,对于使用设备树来说,他会将Bootargs传递给Linux内核,通过设备树完成。也就是向Linux内核传参。
        • -> boot_jump_linux
          • -> machid= gd->bd->bi_arch_number;
          • -> kernel_entry = (void (*)(int, int, uint))images->ep; 0X80800000。
          • -> announce_and_cleanup 输出Starting kernel……
          • -> kernel_entry(0, machid, r2); 启动Linux内核。Uboot的最终使命,启动Linux内核。

2.3.19 kernel_entry函数

kernel_entry函数是内核的入口函数。内核镜像第一行代码

kernel_entry(0, machid, r2);
  • 1

此函数有三个参数:zero,arch,params,

  • 第一个参数 zero 同样为 0;
  • 第二个参数为机器 ID;
  • 第三个参数启动参数标记列表(ATAGS)或者设备树(DTB)首地址,ATAGS 是传统的方法,用于传递一些命令行信息啥的,如果使用设备树的话就要传递设备树(DTB)。
  • 是汇编函数,因为内核开始是汇编代码。
  • images->ep 保存着 Linux内核镜像的起始地址,也就是kernel_entry的地址

kernel_entry函数的传参

  • 向汇编函数传递参数要使用 r0、r1 和 r2寄存器
    • r0 = 0
    • r1 = 机器类型ID(machid)
      • Linux 内核会在自己的机器 ID 列表里面查找是否存在与 uboot 传递进来的 machid 匹配的项目,如果存在就说明 Linux 内核支持这个机器,那么 Linux 就会启动!
      • 如果使用设备树的话这个 machid 就无效了,设备树存有一个“兼容性”这个属性,Linux 内核会比较“兼容性”属性的值(字符串)来查看是否支持这个机器。
    • r2 =
      • 如果使用设备树的话,r2 应该是设备树的起始地址
      • 如果不使用设备树的话,r2 应该是 uboot 传递给 Linux 的参数起始地址,也就是环境变量 bootargs 的值

如何从uboot跳转到内核
直接修改PC寄存器的值为Linux内核所在的地址,CPU从内核所在的地址去取指令,从而执行内核代码
为什么要传参数给内核
在此之前, uboot已经完成了硬件的初始化,可以说已经噎适应了“这块开发板。然而,内核并不是对于所有的开发板都能完美适配的(如果适配了,可想而知这个内核有多庞大,又或者有新技术发明了,可以完美的适配各种开发板),此时,对于开发板的环境一无所知。所以,要想启动 Linux内核, uboot必须要给内核传递一些必要的信息来告诉内核当前所处的环境。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/繁依Fanyi0/article/detail/130019
推荐阅读
  

闽ICP备14008679号