uboot详解和启动流程

Exynos4412、uboot

Posted by 咖啡泡泡茶 on August 15, 2018

UBoot启动分析

UBoot更新速度很快是为了兼容更多的设备和芯片,但是一味地追新不一定能很好的解决问题,在2014版本以后uboot采用和内核编译相似的编译方式,这里用的是2013版本的UBOOT。

可利用grep XXX System.map 查找地址,再利用addr2line -e u-boot 地址 查找在哪个文件中。

uboot启动流程分析如下,主要分为两个大的阶段:

第一阶段:

设置cpu工作模式为SVC模式 关闭中断,mmu,cache 关看门狗 初始化内存,串口 设置栈 代码自搬移 清bss 跳c

第二阶段

初始化外设,进入超循环

超循环处理用户命令(延时呀,等待用户输入命令)

可见, U-Boot 属于两阶段的Bootloader。

第一阶段的文件:

arch/arm/cpu/armv7 /start.S 平台相关,CPU工作模式设为SVC模式,关MMU,关icahce(CPU相关)

board/samsung/fs4412/lowlevel_init.S 开发板相关:关看门狗,内存初始化,时钟初始化,串口初始化(board相关,初始化最基本设备)

第二阶段的文件:

arch/arm/lib/crt0.S _main 函数所在处,初始化SP,为C语言准备,代码重定位,清BSS,设置R0 R1 R2 R8相应寄存器

arch/arm/lib/board.c board_init_f 函数 ,填充GD结构体,初始化外设, main_loop()函数超循环

arch/arm/cpu/armv7 /start.S 代码自搬移时会用到。

下面是具体分析:

U-Boot 第一阶段代码分析

通常我们通过连接文件知晓程序入口点,入口查看 u-boot.lds(部分)

OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
OUTPUT_ARCH(arm)
ENTRY(_start)
SECTIONS
{
 . = 0x00000000; //链接地址
 . = ALIGN(4);   //4K对齐
 .text :                         
 {
  __image_copy_start = .;         
  arch/arm/cpu/armv7/start.o (.text*)  //起始代码段
  *(.text*)
 }
 . = ALIGN(4);
 .rodata : { *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.rodata*))) }
 . = ALIGN(4);
 .data : {
  *(.data*)
 }
 . = ALIGN(4);
 . = .;
 . = ALIGN(4);

通过链接脚本可知入口为_start,位于arch/arm/cpu/armv7/start.o。

第一阶段开始:

1、进入arch/arm/cpu/armv7/start.S
A – 异常向量表设置
.globl _start
_start: 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
B – 设置CPU处于SVC工作模式
reset:
	bl	save_boot_params
	/*
	 * set the cpu to SVC32 mode
	 */
	mrs	r0, cpsr
	bic	r0, r0, #0x1f
	orr	r0, r0, #0xd3
	msr	cpsr,r0
C – 协处理器 p15 的 c12 寄存器来重新定位
#if !defined(CONFIG_TEGRA20)
	/* Set vector address in CP15 VBAR register */
	ldr     r0, =_start
	add     r0, r0, r9
	mcr     p15, 0, r0, c12, c0, 0  @Set VBAR
#endif /* !Tegra20 */
D、Bl  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

关闭数据预取功能;

DSB:多核CPU对数据处理指令

ISB:流水线清空指令;

/*
	 * 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
	mov	pc, lr			@ back to my caller
ENDPROC(cpu_init_cp15)

关闭MMU,使能I-cache

NOTE:

分支预测:在流水线里,会将后面的代码优先加载到处理器中,由于是循环,会使后面加载的代码无效,故出现了分支预测技术。(统计跳的次数来选择装载循环的代码还是下面的代码)。

解释一下:uboot详解—为什么要关闭缓存和mmu

​ 当设置完时钟分频以后,uboot就会执行cpu_init_crit汇编函数,这个函数的主要作用就是关闭缓存和mmu,然后调用lowlevel_init函数进行系统总线的初始化。

​ 为什么启动的时候,需要关闭缓存和mmu呢?我们先了解一下他们的作用。

​ 缓存是主存(内存)和CPU通用寄存器之间设置的一个高速的、容量相对较小的存储器,把正在执行的指令地址附近的一部分指令或数据从主存调入这个存储器,供CPU在一段时间内使用,以提高程序的运行速度。

​ mmu可以实现虚拟内存和内存保护等功能,完成对内存的操作和管理。 CACHE是高速缓冲存储器。CPU工作速度是很快的,而外部内存是工作很慢的,所以当CPU对内存访问的时候,是要等待内存访问结束的。所以中间CPU就在等待,这就浪费了时间。所以在CPU和内存之间加一个CACHE,当CPU写数据到内存中的时候,就先写入到CACHE中,然后CACHE再写入到内存中。CPU写CACHE是很快的,所以就提高了写数据的效率。读数据的话,CPU先在CACHE中去找数据,没有找到的话,CACHE将数据从内存中取出来,再给CPU,同时把这个数据存起来。当CPU在CACHE中找到数据的话,就直接使用这个数据,就不用再去内存中取数据了。

​ Caches是CPU内部的一个2级缓存,它的作用是将常用的数据和指令放在CPU内部。Caches是通过CP15管理的,刚上电的时候,CPU还不能管理Caches。上电的时候指令Cache可关闭,也可不关闭,但数据Cache一定要关闭,否则可能导致刚开始的代码里面,去取数据的时候,从Cache里面取,而这时候RAM中数据还没有Cache过来,导致数据预取异常 。说到Caches就必须提到一个关键字Volatile,它的本质:是告诉编译器不要对我的代码进行优化,作用是让编写者感觉变量的变化情况。因为在优化时,会将常用的代码取出来放到Caches中,它没有从实际的物理地址去取,它直接从CPU的缓存中去取,但常用的代码就是为了检测一些常用变量的变化,如果正在取数据的时候发生跳变,那么就检测不到变量的变化了,所以在这种情况下要用Volatile关键字告诉编译器不要做优化,让cpu每次都从实际的物理地址中去取指令。其实这也是为什么要关闭数据缓存的原因,如果汇编指令读取的时候缓存中的数据,而实际物理地址的数据发生了变化,将导致cpu读取不到真实的最新的值。然而在C语言中是不会关闭Caches的,如果编写者要检测外界物理数据的变化,或变化太快,从Caches中取数据会有误差,就加一个关键字Volatile。

​ 同样,在板子启动的时候是没有对mmu进行初始化的,而且这个时候也用不到mmu,为了避免他们影响启动时的初始化,所以需要先关闭mmu和缓存。

E、Bl  cpu_init_crit
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)

2、跳到Low_level_init,位于board/samsung/fs4412/lowlevel_init.S

a、关闭看门狗

b、比较当前pc指针域TEXT_BASE的高8位是否一样来判断,当前代码是否在内存中

c、对系统时钟初始化

d、对内存初始化

e、对串口初始化

结束后返回 start.S

/*
	 * If U-boot is already running in ram, no need to relocate U-Boot.
	 * Memory controller must be configured before relocating U-Boot
	 * in ram.
	 */
	ldr	r0, =0x0ffffff		/* r0 <- Mask Bits*/
	bic	r1, pc, r0		/* pc <- current addr of code */
					/* r1 <- unmasked bits of pc */
	ldr	r2, _TEXT_BASE		/* r2 <- original base addr in ram */
	bic	r2, r2, r0		/* r2 <- unmasked bits of r2*/
	cmp	r1, r2			/* compare r1, r2 */
	beq	1f			/* r0 == r1 then skip sdram init */

	/* init system clock */
	bl system_clock_init

	/* Memory initialize */
	bl mem_ctrl_asm_init

1:
	/* for UART */
	bl uart_asm_init
	pop	{pc}

第一阶段结束,总结如下:

1 前面总结过的部分,初始化异常向量表,设置svc模式

2 配置cp15,初始化mmu cache tlb

3 板级初始化,clk,memory,uart初始化

二、第二阶段开始:

1、arch/arm/cpu/armv7 /start.S 中 bl _main 开始

跳转到arch/arm/lib/crt0.S

A、初始c运行环境(看注释就知道,初始化C运行环境,并调用board_init_f 函数)
_main:
/*
 * Set up initial C runtime environment and call board_init_f(0).
 */
#if defined(CONFIG_NAND_SPL)
	/* deprecated, use instead CONFIG_SPL_BUILD */
	ldr	sp, =(CONFIG_SYS_INIT_SP_ADDR)
#elif defined(CONFIG_SPL_BUILD) && defined(CONFIG_SPL_STACK)
	ldr	sp, =(CONFIG_SPL_STACK)
#else
	ldr	sp, =(CONFIG_SYS_INIT_SP_ADDR)
#endif
	bic	sp, sp, #7	/* 8-byte alignment for ABI compliance */
	sub	sp, #GD_SIZE	/* allocate one GD above SP */
	bic	sp, sp, #7	/* 8-byte alignment for ABI compliance */
	mov	r8, sp		/* GD is above SP */
	mov	r0, #0
	bl	board_init_f          //重要哦

功能:

初始化sp ,为支持C语言做准备;

保存128B 放GD结构体,存放全局信息,GD的地址存放在r8中;

跳转到 board_init_f 函数,其在arch/arm/lib/board.c 处定义;

B、跳转到arch/arm/lib/board.c
void board_init_f(ulong bootflag)
{
	bd_t *bd;
	init_fnc_t **init_fnc_ptr;
	gd_t *id;
	ulong addr, addr_sp;
#ifdef CONFIG_PRAM
	ulong reg;
#endif
	void *new_fdt = NULL;
	size_t fdt_size = 0;

	memset((void *)gd, 0, sizeof(gd_t));

	gd->mon_len = _bss_end_ofs;
#ifdef CONFIG_OF_EMBED
	/* Get a pointer to the FDT */
	gd->fdt_blob = _binary_dt_dtb_start;
#elif defined CONFIG_OF_SEPARATE
	/* FDT is at end of image */
	gd->fdt_blob = (void *)(_end_ofs + _TEXT_BASE);
#endif
	/* Allow the early environment to override the fdt address */
	gd->fdt_blob = (void *)getenv_ulong("fdtcontroladdr", 16,
						(uintptr_t)gd->fdt_blob);

	for (init_fnc_ptr = init_sequence; *init_fnc_ptr; ++init_fnc_ptr) {
		if ((*init_fnc_ptr)() != 0) {
			hang ();
		}
	}
	}

功能:

对全局信息GD结构体进行填充:

291行:mon_len 通过链接脚本可以知道存放的是uboot代码大小;

294行:fdt_blob 存放设备数地址;

303行:循环执行init_fnc_t数组的函数,作硬件初始化;

init_sequence 可以修改一些启动参数

a – init_fnc_t数组的函数定义

​ 初始化硬件

init_fnc_t *init_sequence[] = {
	arch_cpu_init,		/* basic arch cpu dependent setup */
	mark_bootstage,
#ifdef CONFIG_OF_CONTROL
	fdtdec_check_fdt,
#endif
#if defined(CONFIG_BOARD_EARLY_INIT_F)
	board_early_init_f,
#endif
	timer_init,		/* initialize timer */
#ifdef CONFIG_BOARD_POSTCLK_INIT
	board_postclk_init,
#endif
#ifdef CONFIG_FSL_ESDHC
	get_clocks,
#endif
	env_init,		/* initialize environment */
	init_baudrate,		/* initialze baudrate settings */
	serial_init,		/* serial communications setup */
	console_init_f,		/* stage 1 init of console */
	display_banner,		/* say that we are here */
#if defined(CONFIG_DISPLAY_CPUINFO)
	print_cpuinfo,		/* display cpu info (and speed) */
#endif
#if defined(CONFIG_DISPLAY_BOARDINFO)
	checkboard,		/* display board info */
#endif
#if defined(CONFIG_HARD_I2C) || defined(CONFIG_SOFT_I2C)
	init_func_i2c,
#endif
	dram_init,		/* configure available RAM banks */
	NULL,
};

b – Dram_init初始化成功之后,剩余代码将会对sdram空间进行规划。

	gd->ram_size -= CONFIG_SYS_MEM_TOP_HIDE;
#endif
	addr = CONFIG_SYS_SDRAM_BASE + gd->ram_size;

可以看到addr的值由CONFIG_SYS_SDRAM_BASE加上ram_size。也就是到了可用sdram的顶端。

e–继续对gd结构体填充

	/* reserve TLB table */
gd->tlb_size = 4096 * 4;
	addr -= gd->tlb_size;
	/* round down to next 64 kB limit */
	addr &= ~(0x10000 - 1);
	gd->tlb_addr = addr;

如果icahe 与 dcache 是打开的,就留出 64K 的空间作为 tlb 空间,最后 addr 就是tlb 地址,4K对齐。

f –填充完成将信息拷贝到内存指定位置

memcpy(id, (void *)gd, sizeof(gd_t));

2、回到跳转前的函数,即 arch/arm/lib/crt0.S

/*
 * Set up intermediate environment (new sp and gd) and call
 * relocate_code(addr_sp, gd, addr_moni). Trick here is that
 * we'll return 'here' but relocated.
 */
	ldr	sp, [r8, #GD_START_ADDR_SP]	/* r8 = gd->start_addr_sp */
	bic	sp, sp, #7	/* 8-byte alignment for ABI compliance */
	ldr	r8, [r8, #GD_BD]		/* r8 = gd->bd */
	sub	r8, r8, #GD_SIZE		/* new GD is below bd */

	adr	lr, here
	ldr	r0, [r8, #GD_RELOC_OFF]		/* lr = gd->start_addr_sp */
	add	lr, lr, r0
	ldr	r0, [r8, #GD_START_ADDR_SP]	/* r0 = gd->start_addr_sp */
	mov	r1, r8				/* r1 = gd */
	ldr	r2, [r8, #GD_RELOCADDR]		/* r2 = gd->relocaddr */
	b	relocate_code
here:

功能:

将 r8 指向新的 gd 地址;

代码重定位;

对lr 的操作为了让返回时,返回的是重定位的here处

3、代码自搬移

代码自搬移,防止与内核冲突,代码位于arch/arm/cpu/armv7/start.S

/*
 * void relocate_code (addr_sp, gd, addr_moni)
 *
 * This "function" does not return, instead it continues in RAM
 * after relocating the monitor code.
 *
 */
ENTRY(relocate_code)
	mov	r4, r0	/* save addr_sp */
	mov	r5, r1	/* save addr of gd */
	mov	r6, r2	/* save addr of destination */

	adr	r0, _start
	cmp	r0, r6
	moveq	r9, #0		/* no relocation. relocation offset(r9) = 0 */
	beq	relocate_done		/* skip relocation */
	mov	r1, r6			/* r1 <- scratch for copy_loop */
	ldr	r3, _image_copy_end_ofs
	add	r2, r0, r3		/* r2 <- source end address	    */
copy_loop:
	ldmia	r0!, {r9-r10}		/* copy from source address [r0]    */
	stmia	r1!, {r9-r10}		/* copy to   target address [r1]    */
	cmp	r0, r2			/* until source end address [r2]    */
	blo	copy_loop

循环将代码搬移到指定高地址

这里只是将链接脚本中_image_copy_end到_start中的代码,其它段还没有操作。

在这里我们有疑惑就是将代码重定位到高地址,那运行的地址不就和链接地址不一样了。

4、重定位到高地址之后,再次回到 _main(arch/arm/lib/crt0.S)

​ 此时回到的是刚才的重定位的 here 处

/* Set up final (full) environment */
	bl	c_runtime_cpu_setup	/* we still call old routine here */
	ldr	r0, =__bss_start	/* this is auto-relocated! */
	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 */
	strlo	r2, [r0]		/* clear 32-bit BSS word */
	addlo	r0, r0, #4		/* move to next */
	blo	clbss_l

关 icache,保证数据从SDRAM中更新,更新异常向量表,因为代码被重定位了;

清BBS;

/* call board_init_r(gd_t *id, ulong dest_addr) */
	mov	r0, r8			/* gd_t */
	ldr	r1, [r8, #GD_RELOCADDR]	/* dest_addr */
	/* call board_init_r */
	ldr	pc, =board_init_r	/* this is auto-relocated! */

调用board_init_r主要是对外设的初始化。

R0=gd

R1=RELOCADDR

5、Main_loop 函数进入超循环(arch/arm/lib/board.c)

for (;;) 
{
		main_loop();
}

Main_loop函数主要功能是处理环境变量,解析命令

install_auto_complete(); //安装自动补全的函数,分析如下 。

getenv(bootcmd)

bootdelay(自启动)

如果延时大于等于零,并且没有在延时过程中接收到按键,则引导内核。