ARMv8架构u-boot启动流程详细分析

Posted Elpm1s

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ARMv8架构u-boot启动流程详细分析相关的知识,希望对你有一定的参考价值。

1. u-boot armv8链接脚本

在进行源码分析之前,首先看看u-boot的链接脚本,通过链接脚本可以从整体了解一个u-boot的组成,并且可以在启动分析中知道某些逻辑是在完成什么工作。在armv8中,u-boot使用arch/arm/cpu/armv8/u-boot.lds进行链接。u-boot-spl和u-boot-tpl使用arch/arm/cpu/armv8/u-boot-spl.lds进行链接,因为每个board的情况可能不同,所以u-boot可以通过Kconfig来自定义u-boot-spl.lds和u-boot-tpl.lds。

1.1 u-boot.lds

/* SPDX-License-Identifier: GPL-2.0+ */
/*
 * (C) Copyright 2013
 * David Feng <fenghua@phytium.com.cn>
 *
 * (C) Copyright 2002
 * Gary Jennejohn, DENX Software Engineering, <garyj@denx.de>
 */

#include <config.h>
#include <asm/psci.h>

OUTPUT_FORMAT("elf64-littleaarch64", "elf64-littleaarch64", "elf64-littleaarch64")
OUTPUT_ARCH(aarch64)
ENTRY(_start) -------------------------------------------------------------------- (1)
/*
 *(1)首先定义了二进制程序的输出格式为"elf64-littleaarch64",
 *    架构是"aarch64",程序入口为"_start"符号;
 */
SECTIONS

#ifdef CONFIG_ARMV8_SECURE_BASE -------------------------------------------------- (2)
/*
 *(2)ARMV8_SECURE_BASE是u-boot对PSCI的支持,在定义时可以将PSCI的文本段,
 *    数据段,堆栈段重定向到指定的内存,而不是内嵌到u-boot中。
 *    不过一般厂商实现会使用atf方式使其与bootloader分离,这个功能不常用;
 */
	/DISCARD/ :  *(.rela._secure*) 
#endif
	. = 0x00000000; -------------------------------------------------------------- (3)
/*
 *(3)定义了程序链接的基地址,默认是0,通过配置CONFIG_SYS_TEXT_BASE可修改
 *    这个默认值。
 */
	. = ALIGN(8);
	.text :
	
		*(.__image_copy_start) --------------------------------------------------- (4)
/*
 *(4)__image_copy_start和__image_copy_end用于定义需要重定向的段,
 *    u-boot是一个分为重定向前初始化和重定向后初始化的bootloader,
 *    所以此处会定义在完成重定向前初始化后需要搬运到ddr中数据的起始地址和结束地址;
 *
 *    大多数时候u-boot是运行在受限的sram或者只读的flash上,
 *    u-boot为了启动流程统一会在ddr未初始化和重定位之前不去访问全局变量,
 *    但是又为了保证u-boot能够正常读写全局变量,内存,调用各类驱动能力,
 *    所以u-boot将启动初始化分为了两个部分,重定向前初始化board_f和
 *    重定向后初始化  board_r,在重定向之前完成一些必要初始化,
 *    包括可能的ddr初始化,然后通过__image_copy_start和__image_copy_end
 *    将u-boot搬运到ddr中,并在ddr中进行重定向后初始化,这个时候的u-boot就可以
 *    正常访问全局变量等信息了。
 * 
 *    如果想要在board_f过程中读写一些全局变量信息该怎么办呢?
 *    u-boot通过定义global_data(gd)来完成此功能,
 *    后续在分析到时会详细讲解实现方式。
 */
		CPUDIR/start.o (.text*) -------------------------------------------------- (5)
/*
 *(5)定义了链接程序的头部文本段,armv8就是
 *    arch/arm/cpu/armv8/start.S, 
 *    start.S中所有文本段将会链接到此段中并且段入口符号就是_start;
 */
	

	/* This needs to come before *(.text*) */
	.efi_runtime :  ------------------------------------------------------------ (6)
/*
 *(6)在定义了efi运行时相关支持时才会出现使用的段,一般不用关心;
 */
        __efi_runtime_start = .;
		*(.text.efi_runtime*)
		*(.rodata.efi_runtime*)
		*(.data.efi_runtime*)
        __efi_runtime_stop = .;
	

	.text_rest : ---------------------------------------------------------------- (7)
/*
 *(7)除了start.o,其他的所有文本段将会链接到此段中;
 */
	
		*(.text*)
	

#ifdef CONFIG_ARMV8_PSCI -------------------------------------------------------- (8)
/*
 *(8)同(2),是PSCI相关功能的支持,一般不会使用;
 */
	.__secure_start :
#ifndef CONFIG_ARMV8_SECURE_BASE
		ALIGN(CONSTANT(COMMONPAGESIZE))
#endif
	
		KEEP(*(.__secure_start))
	

#ifndef CONFIG_ARMV8_SECURE_BASE
#define CONFIG_ARMV8_SECURE_BASE
#define __ARMV8_PSCI_STACK_IN_RAM
#endif
	.secure_text CONFIG_ARMV8_SECURE_BASE :
		AT(ADDR(.__secure_start) + SIZEOF(.__secure_start))
	
		*(._secure.text)
		. = ALIGN(8);
		__secure_svc_tbl_start = .;
		KEEP(*(._secure_svc_tbl_entries))
		__secure_svc_tbl_end = .;
	

	.secure_data : AT(LOADADDR(.secure_text) + SIZEOF(.secure_text))
	
		*(._secure.data)
	

	.secure_stack ALIGN(ADDR(.secure_data) + SIZEOF(.secure_data),
			    CONSTANT(COMMONPAGESIZE)) (NOLOAD) :
#ifdef __ARMV8_PSCI_STACK_IN_RAM
		AT(ADDR(.secure_stack))
#else
		AT(LOADADDR(.secure_data) + SIZEOF(.secure_data))
#endif
	
		KEEP(*(.__secure_stack_start))

		. = . + CONFIG_ARMV8_PSCI_NR_CPUS * ARM_PSCI_STACK_SIZE;

		. = ALIGN(CONSTANT(COMMONPAGESIZE));

		KEEP(*(.__secure_stack_end))
	

#ifndef __ARMV8_PSCI_STACK_IN_RAM
	. = LOADADDR(.secure_stack);
#endif

	.__secure_end : AT(ADDR(.__secure_end)) 
		KEEP(*(.__secure_end))
		LONG(0x1d1071c);	/* Must output something to reset LMA */
	
#endif

	. = ALIGN(8);
	.rodata :  *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.rodata*)))  ------------------- (9)
/*
 *(9)所有仅读数据将会在这个段中对齐排序存放好;
 */

	. = ALIGN(8);
	.data :  -------------------------------------------------------------------- (10)
/*
 *(10)所有数据段将会链接到此段中;
 */
		*(.data*)
	

	. = ALIGN(8);

	. = .;

	. = ALIGN(8);
	.u_boot_list :  ------------------------------------------------------------- (11)
/*
 *(11)u_boot_list段定义了系统中当前支持的所有命令和设备驱动,此段把散落在各个文件中
 *     通过U_BOOT_CMD的一系列拓展宏定义的命令和U_BOOT_DRIVER的拓展宏定义的设备驱动收集到一起,
 *     并按照名字排序存放,以便后续在命令行快速检索到命令并执行和检测注册的设备和设备树匹配
 *     probe设备驱动初始化;(设备驱动的probe只在定义了dm模块化驱动时有效)
 */
		KEEP(*(SORT(.u_boot_list*)));
	

	. = ALIGN(8);

	.efi_runtime_rel : 
                __efi_runtime_rel_start = .;
		*(.rel*.efi_runtime)
		*(.rel*.efi_runtime.*)
                __efi_runtime_rel_stop = .;
	

	. = ALIGN(8);

	.image_copy_end :
	
		*(.__image_copy_end)
	

	. = ALIGN(8);

	.rel_dyn_start : -------------------------------------------------------- (12)
/*
 *(12)一般u-boot运行时是根据定义的基地址开始执行,如果加载地址和链接地址
 *     不一致则会出现不能执行u-boot的问题。通过一个
 *     配置CONFIG_POSITION_INDEPENDENT即可打开地址无关功能,
 *     此选项会在链接u-boot时添加-PIE参数。此参数会在u-boot ELF文件中
 *     生成rela*段,u-boot通过读取此段中表的相对地址值与实际运行时地址值
 *     依次遍历进行修复当前所有需要重定向地址,使其可以实现地址无关运行;
 *     即无论链接基地址如何定义,u-boot也可以在任意ram地址
 *     运行(一般需要满足最低4K或者64K地址对齐);
 * 
 *     注意此功能只能在sram上实现,因为此功能会在运行时修改文本段数据段中的地址,
 *     如果此时运行在片上flash,则不能写flash,导致功能失效无法实现地址无关;
 */
	
		*(.__rel_dyn_start)
	

	.rela.dyn : 
		*(.rela*)
	

	.rel_dyn_end :
	
		*(.__rel_dyn_end)
	

	_end = .;

	. = ALIGN(8);

	.bss_start :  -------------------------------------------------------- (13)
/*
 *(13)众所周知的bbs段;
 */
		KEEP(*(.__bss_start));
	

	.bss : 
		*(.bss*)
		 . = ALIGN(8);
	

	.bss_end : 
		KEEP(*(.__bss_end));
	

	/DISCARD/ :  *(.dynsym)  -------------------------------------------- (14)
/*
 *(14)一些在链接时无用需要丢弃的段;
 */
	/DISCARD/ :  *(.dynstr*) 
	/DISCARD/ :  *(.dynamic*) 
	/DISCARD/ :  *(.plt*) 
	/DISCARD/ :  *(.interp*) 
	/DISCARD/ :  *(.gnu*) 

#ifdef CONFIG_LINUX_KERNEL_IMAGE_HEADER ----------------------------------- (15)
/*
 *(15)在efi加载时会很有用,主要在u-boot的二进制头部添加了一些头部信息,
 *     包括大小端,数据段文本段大小等,以便于efi相关的加载器读取信息,
 *     此头部信息来自于Linux arm64的Image的头部信息;该头部也不属于u-boot的
 *     一部分只是被附加上去的;
 */
#include "linux-kernel-image-header-vars.h"
#endif

1.2 u-boot-spl.lds

此链接脚本是标准的spl链接脚本,还包含了u_boot_list段,如果对应自己board不需要命令行或者模块化驱动设备,只作为一个加载器则可以自定义更简略的链接脚本。

/* SPDX-License-Identifier: GPL-2.0+ */
/*
 * (C) Copyright 2013
 * David Feng <fenghua@phytium.com.cn>
 *
 * (C) Copyright 2002
 * Gary Jennejohn, DENX Software Engineering, <garyj@denx.de>
 *
 * (C) Copyright 2010
 * Texas Instruments, <www.ti.com>
 *	Aneesh V <aneesh@ti.com>
 */

MEMORY  .sram : ORIGIN = IMAGE_TEXT_BASE, ---------------------------------------- (1)
/*
 *(1)\\>XXX 的形式可以将指定段放入XXX规定的内存中;一般u-boot-spl只有
 *    很小的可运行内存块,所以spl中会舍去大量不需要用的段只保留关键的
 *    文本段数据段等,并且通过>.sram的形式将不在ddr初始化前用到的段定义到sdram中,
 *    后续只需在完成ddr初始化后将这些段搬运到ddr中即可,而不需要额外的
 *    地址修复逻辑,如下:有一个sram 0x18000-0x19000,
 *    一个sdram 0x80000000 - 0x90000000,
 *    那么通过>.sram方式则map文件可能如下:
 *       0x18000 stext
 *       ...
 *       0x18100 sdata
 *       ...
 *       0x80000000 sbss
 *       ...
 */
		LENGTH = IMAGE_MAX_SIZE 
MEMORY  .sdram : ORIGIN = CONFIG_SPL_BSS_START_ADDR,
		LENGTH = CONFIG_SPL_BSS_MAX_SIZE 

OUTPUT_FORMAT("elf64-littleaarch64", "elf64-littleaarch64", "elf64-littleaarch64")
OUTPUT_ARCH(aarch64)
ENTRY(_start) -------------------------------------------------------------------- (2)
/*
 *(2)同u-boot.lds一致,共用一套逻辑入口_start;
 */
SECTIONS

	.text : 
		. = ALIGN(8);
		*(.__image_copy_start) -------------------------------------------------- (3)
/*
 *(3)同样的,如果spl需要重定向则会使用此段定义,大多数情况下spl中会用上重定向;
 */
		CPUDIR/start.o (.text*)
		*(.text*)
	 >.sram

	.rodata : 
		. = ALIGN(8);
		*(SORT_BY_ALIGNMENT(SORT_BY_NAME(.rodata*)))
	 >.sram

	.data : 
		. = ALIGN(8);
		*(.data*)
	 >.sram

#ifdef CONFIG_SPL_RECOVER_DATA_SECTION ---------------------------------------- (4)
/*
 *(4)SPL_RECOVER_DATA_SECTION段用于保存数据段数据,
 *    一些board在初始化时修改data段数据,并在后续某个阶段
 *    从此段中恢复data的原始数据;
 */
	.data_save : 
		*(.__data_save_start)
		. = SIZEOF(.data);
		*(.__data_save_end)
	 >.sram
#endif

	.u_boot_list : 
		. = ALIGN(8);
		KEEP(*(SORT(.u_boot_list*)));
	 >.sram

	.image_copy_end : 
		. = ALIGN(8);
		*(.__image_copy_end)
	 >.sram

	.end : 
		. = ALIGN(8);
		*(.__end)
	 >.sram

	_image_binary_end = .;

	.bss_start (NOLOAD) : 
		. = ALIGN(8);
		KEEP(*(.__bss_start));
	 >.sdram -------------------------------------------------------------- (5)
/*
 *(5)将bss段数据定义到>.sdram中,即可在初始化ddr后直接对此段地址清零
 *    即可使用全局未初始化变量,并且不会带来副作用。
 */

	.bss (NOLOAD) : 
		*(.bss*)
		 . = ALIGN(8);
	 >.sdram

	.bss_end (NOLOAD) : 
		KEEP(*(.__bss_end));
	 >.sdram

	/DISCARD/ :  *(.rela*) 
	/DISCARD/ :  *(.dynsym) 
	/DISCARD/ :  *(.dynstr*) 
	/DISCARD/ :  *(.dynamic*) 
	/DISCARD/ :  *(.plt*) 
	/DISCARD/ :  *(.interp*) 
	/DISCARD/ :  *(.gnu*) 


2. u-boot在汇编启动阶段对系统的一些初始化

2.1 启动前为后续流程做的一些平台相关操作

当cpu交由u-boot接管进入u-boot后,首先会到_start符号处开始执行初始化,并在此期间完成一些必要的系统寄存器相关的初始化,包括保存boot参数,进行地址无关fixed,系统寄存器复位,底层平台相关初始化等,启动代码位于arch/arm/cpu/armv8/start.S,入口地址为_start

/*************************************************************************
 *
 * Startup Code (reset vector)
 *
 *************************************************************************/

.globl	_start
_start: ------------------------------------------------------------------------ (1)
#if defined(CONFIG_LINUX_KERNEL_IMAGE_HEADER)
#include <asm/boot0-linux-kernel-header.h>
#elif defined(CONFIG_ENABLE_ARM_SOC_BOOT0_HOOK) 
/*
 * Various SoCs need something special and SoC-specific up front in
 * order to boot, allow them to set that in their boot0.h file and then
 * use it here.
 */
#include <asm/arch/boot0.h>
#else
	b	reset ----------------------------------------------------------------- (2)
#endif

	.align 3

.globl	_TEXT_BASE ------------------------------------------------------------ (3)
_TEXT_BASE:
	.quad	CONFIG_SYS_TEXT_BASE

/*
 * These are defined in the linker script.
 */
.globl	_end_ofs 
_end_ofs:
	.quad	_end - _start

.globl	_bss_start_ofs
_bss_start_ofs:
	.quad	__bss_start - _start

.globl	_bss_end_ofs
_bss_end_ofs:
	.quad	__bss_end - _start

reset:
	/* Allow the board to save important registers */
	b	save_boot_params ----------------------------------------------------- (5)
.globl	save_boot_params_ret
save_boot_params_ret:

... /* 此处省略无关代码,待分析到时再展开代码 */
...
WEAK(save_boot_params)
	b	save_boot_params_ret	/* back to my caller */
ENDPROC(save_boot_params)
#endif

(1)_start段标记为全局可见并在链接脚本中被声明为入口地址,表示u-boot的入口地址为_start;
(2)_start段是一条最简单的跳转指令b reset跳转到reset处继续启动流程初始化;
(3)在_start到reset之间,有一个.align 3用于8字节对齐,因为可能在读取常量地址之前各自平台做了自己代码逻辑导致当前地址并不是8字节对齐的,这里不管是否对齐都强制对齐了一下,之后还保存了一些常量信息,其中包括_TEXT_BASE保存了链接地址,用于在启动地址无关功能时进行对运行时地址的偏移计算,其他几个偏移值目前未使用
(4) save_boot_params用于保存一些board相关的重要寄存器,此处定义为了一个弱函数,为直接跳转回save_boot_params_ret继续往下执行,如果某些board需要保存寄存器参数则可以在自己的lowlevel.S文件中实现此函数。一般由atf,bl2或者rom跳转到spl或u-boot时厂商可能需要在两个固件之间传递参数,比如由bl2在寄存器x0,x1,x2中分别存入了一些固件的地址信息,那么u-boot则可以在早期通过此函数保存这些信息,并在后续某个时机中使用。

2.2 开启地址无关后的重定位地址操作

save_boot_params_ret:

#if CONFIG_POSITION_INDEPENDENT
	/* Verify that we're 4K aligned.  */
	adr	x0, _start
	ands	x0, x0, #0xfff --------------------------------------------------------- (1)
	b.eq	1f
0:
	/*
	 * FATAL, can't continue.
	 * U-Boot needs to be loaded at a 4K aligned address.
	 *
	 * We use ADRP and ADD to load some symbol addresses during startup.
	 * The ADD uses an absolute (non pc-relative) lo12 relocation
	 * thus requiring 4K alignment.
	 */
	wfi
	b	0b
1:

	/*
	 * Fix .rela.dyn relocations. This allows U-Boot to be loaded to and
	 * executed at a different address than it was linked at.
	 */
pie_fixup:
	adr	x0, _start		/* x0 <- Runtime value of _start */
	ldr	x1, _TEXT_BASE		/* x1 <- Linked value of _start */
	subs	x9, x0, x1		/* x9 <- Run-vs-link offset */
	beq	pie_fixup_done ------------------------------------------------------------- (2)
	adrp    x2, __rel_dyn_start     /* x2 <- Runtime &__rel_dyn_start */ ----------- (3)
	add     x2, x2, #:lo12:__rel_dyn_start
	adrp    x3, __rel_dyn_end       /* x3 <- Runtime &__rel_dyn_end */
	add     x3, x3, #:lo12:__rel_dyn_end
pie_fix_loop: ---------------------------------------------------------------------- (4)
	ldp	x0, x1, [x2], #16	/* (x0, x1) <- (Link location, fixup) */
	ldr	x4, [x2], #8		/* x4 <- addend */
	cmp	w1, #1027		/* relative fixup? */
	bne	pie_skip_reloc
	/* relative fix: store addend plus offset at dest location */
	add	x0, x0, x9
	add	x4, x4, x9
	str	x4, [x0]
pie_skip_reloc:
	cmp	x2, x3
	b.lo	pie_fix_loop
pie_fixup_done:
#endif

2.3 进入_main之前系统寄存器初始化和从核的引导

#ifdef CONFIG_SYS_RESET_SCTRL
	bl reset_sctrl --------------------------------------------------------------------- (1)
#endif

#if defined(CONFIG_ARMV8_SPL_EXCEPTION_VECTORS) || !defined(CONFIG_SPL_BUILD) ---------- (2)
.macro	set_vbar, regname, reg
	msr	\\regname, \\reg
.endm
	adr	x0, vectors
#else
.macro	set_vbar, regname, reg
.endm
#endif
	/*
	 * Could be EL3/EL2/EL1, Initial State:
	 * Little Endian, MMU Disabled, i/dCache Disabled
	 */
	switch_el x1, 3f, 2f, 1f ---------------------------------------------------------- (3)
3:	set_vbar vbar_el3, x0
	mrs	x0, scr_el3
	orr	x0, x0, #0xf			/* SCR_EL3.NS|IRQ|FIQ|EA */
	msr	scr_el3, x0
	msr	cptr_el3, xzr			/* Enable FP/SIMD */
	b	0f
2:	mrs	x1, hcr_el2
	tbnz	x1, #34, 1f			/* HCR_EL2.E2H */
	set_vbar vbar_el2, x0
	mov	x0, #0x33ff
	msr	cptr_el2, x0			/* Enable FP/SIMD */
	b	0f
1:	set_vbar vbar_el1, x0
	mov	x0, #3 << 20
	msr	cpacr_el1, x0			/* Enable FP/SIMD */
0:

#ifdef COUNTER_FREQUENCY -------------------------------------------------------------- (4)
	branch_if_not_highest_el x0, 4f
	ldr	x0, =COUNTER_FREQUENCY
	msr	cntfrq_el0, x0			/* Initialize CNTFRQ */
#endif

4:	isb ------------------------------------------------------------------------------- (5)
.
.
.
.
.
#ifdef CONFIG_SYS_RESET_SCTRL
reset_sctrl:
	switch_el x1, 3f, 2f, 1f
3:
	mrs	x0, sctlr_el3
	b	0f
2:
	mrs	x0, sctlr_el2
	b	0f
1:
	mrs	x0, sctlr_el1

0:
	ldr	x1, =0xfdfffffa
	and	x0, x0, x1

	switch_el x1, 6f, 5f, 4f
6:
	msr	sctlr_el3, x0
	b	7f
5:
	msr	sctlr_el2, x0
	b	7f
4:
	msr	sctlr_el1, x0

7:
	dsb	sy
	isb
	b	__asm_invalidate_tlb_all ----------------------------------------------------- (6)
	ret
#endif

(1)一般情况下此功能不需要使用,但是一些由其他固件引导启动的u-boot,board希望系统行为能按照自己预期行为执行而不受上一级加载器的影响,所以使用CONFIG_SYS_RESET_SCTRL来决定是否重置系统控制寄存器,包括保证处理器处于小端,关闭data cache,关闭mmu。其中switch_el是一个宏,用于读取当前所处的异常级别,根据所处异常级别调用对应的系统控制寄存器。某些时候u-boot的加载并不是一定在el3级别,当存在atf等时,el3由atf控制,atf会将u-boot的运行级别切换到el2,以便保证自己的控制级别,所以u-boot通过switch_el来选择自己能够控制的系统寄存器。
(2)定义设置异常向量表的宏,将异常向量表的地址写入/reg设置的系统寄存器即可完成异常向量表的设置,这里u-boot是需要设置异常向量表的,而spl默认是不需要设置异常向量表的,毕竟spl只是一个加载器只会运行一次,不过当定义了CONFIG_ARMV8_SPL_EXCEPTION_VECTORS时可以为spl也设置一个异常向量表。
(3)同样的使用switch_el来跳转到对应级别的路径上去执行,在进行系统寄存器设置时,因为在这之前已经由SYS_RESET_SCTRL或者board自己保证处理器处于小端,mmu关,i-cache和d-cache处于关闭状态了,所以这里直接进行对应级别系统寄存器设置,首先是跳转到对应表设置对应级别的异常向量表。接着会有如下三种情况:

当处于EL3时,会设置安全配置系统寄存器(scr_el3),会将低四位bit设置为0xf,表示设置处理器处于非安全模式,任何级别的物理irq中断,物理fiq,异常abort中断,异常SError中断都将被路由到el3级别。后续这些设置将在启动Linux时被修改,这些设置仅用于在u-boot阶段。接着将cptr_el3清零,使用xzr是可以快速操作寄存器为零。这里保证任何级别下访问SIMD和floating-point指令不会导致触发异常陷入el3。

当处于EL2时,首先根据HCR_EL2.E2H判断系统是一个虚拟机管理器还是主机系统,当E2H = 0时,表示系统处于主机系统只需要做el3一样的操作配置SIMD和FP指令不会陷入el2即可。

当系统处于EL1时,则什么也不需要操作只需要配置SIMD和FP指令不会陷入el1。
(4)u-boot在启动时系统的时钟频率不一定配置了,所以当在include/configs/xxxxxx.h中定义了COUNTER_FREQUENCY的频率值时,说明需要在此处配置系统时钟,所以根据宏 branch_if_not_highest_el判断当系统不处于EL3时则需要设置系统的时钟工作频率cntfrq_el0,后续Linux或者u-boot根据读出的这个值计算出系统每纳秒的滴答数从而供软件获取时间流逝值。
(5)isb指令用于确保上述操作指令被正确真正的执行了,属于同步指令的一种。
(6)在进行系统控制器复位时,dsb sy,isb,__asm_invalidate_tlb_all三个操作在这里的意义是,因为对处理器的小端,mmu,d-cache进行了复位,所以这里必须通过dsb和isb确保数据和指令全部执行和写入,这里进行了mmu和cache关闭操作,那么如果有缓存的tlb在这个时候这些缓存的tlb数据就是无效的,这里对可能缓存的tlb进行全部无效化,确保后续任何可能的mmu开启操作不会使用到这些无用的tlb条目而导致系统异常。
第二段代码如下:

	/*
	 * Enable SMPEN bit for coherency.
	 * This register is not architectural but at the moment
	 * this bit should be set for A53/A57/A72.
	 */
#ifdef CONFIG_ARMV8_SET_SMPEN -------------------------------------------------------- (1)
	switch_el x1, 3f, 1f, 1f
3:
	mrs     x0, S3_1_c15_c2_1               /* cpuectlr_el1 */
	orr     x0, x0, #0x40
	msr     S3_1_c15_c2_1, x0
	isb
1:
#endif

	/* Apply ARM core specific erratas */
	bl	apply_core_errata ------------------------------------------------------------ (2)

	/*
	 * Cache/BPB/TLB Invalidate
	 * i-cache is invalidated before enabled in icache_enable()
	 * tlb is invalidated before mmu is enabled in dcache_enable()
	 * d-cache is invalidated before enabled in dcache_enable()
	 */

	/* Processor specific initialization */
	bl	lowlevel_init ---------------------------------------------------------------- (3)
...
...
WEAK(apply_core_errata)
...
...
WEAK(lowlevel_init)
	mov	x29, lr			/* Save LR */

#if defined(CONFIG_GICV2) || defined(CONFIG_GICV3) ----------------------------------- (4)
	branch_if_slave x0, 1f
	ldr	x0, =GICD_BASE
	bl	gic_init_secure
1:
#if defined(CONFIG_GICV3)
	ldr	x0, =GICR_BASE
	bl	gic_init_secure_percpu
#elif defined(CONFIG_GICV2)
	ldr	x0, =GICD_BASE
	ldr	x1, =GICC_BASE
	bl	gic_init_secure_percpu
#endif
#endif

#ifdef CONFIG_ARMV8_MULTIENTRY ------------------------------------------------------ (5)
	branch_if_master x0, x1, 2f

	/*
	 * Slave should wait for master clearing spin table.
	 * This sync prevent salves observing incorrect
	 * value of spin table and jumping to wrong place.
	 */
#if defined(CONFIG_GICV2) || defined(CONFIG_GICV3)
#ifdef CONFIG_GICV2
	ldr	x0, =GICC_BASE
#endif
	bl	gic_wait_for_interrupt
#endif

	/*
	 * All slaves will enter EL2 and optionally EL1.
	 */
	adr	x4, lowlevel_in_el2
	ldr	x5, =ES_TO_AARCH64
	bl	armv8_switch_to_el2

lowlevel_in_el2:
#ifdef CONFIG_ARMV8_SWITCH_TO_EL1
	adr	x4, lowlevel_in_el1
	ldr	x5, =ES_TO_AARCH64
	bl	armv8_switch_to_el1

lowlevel_in_el1:
#endif

#endif /* CONFIG_ARMV8_MULTIENTRY */

2:
	mov	lr, x29			/* Restore LR */
	ret
ENDPROC(lowlevel_init)

(1)首先解释S3_1_c15_c2_1,有一些架构定义系统寄存器是不能被编译器识别的,如果要访问则只能直接通过如下定义的形式被ARM汇编器识别并编译成二进制:S3__ 。这里使用S3_1_C15_C2_0,则不需要将编码交给汇编程序,如果直接使用对应的CPUACTLR_EL1则必须把它的编码交给汇编程序,而由于arm汇编程序并不识别此系统器从而指导编译报错退出。这里是当定义了CONFIG_ARMV8_SET_SMPEN并处于EL3级别时设置cpuectlr_el1的SMPEN位用于开启多核之间的数据一致性功能,当然这个功能是因为设计的问题导致只是一些a系列处理器需要设置,如a53,a57,a72。
(2)因为一些基于armv8架构设计的处理器本身会存在一些bug,所以这里对特定处理器进行勘误设置,相当于打补丁的意思,感兴趣的可以去看看每个a系列有哪些勘误需要被设置。
(3)低平台初始化,这里可以在汇编阶段对对应平台board进行初始化,lowlevel_init是一个弱符号,可以由厂商自己实现,这里分析armv8的标准lowlevel_init进行的初始化。(4)中详述。
(4)首先是对GIC的一些初始化,同样的需要根据使用的gic版本在Kconfig中选中CONFIG_GICV2或者CONFIG_GICV3,当定义了这两个宏其中一个时就会对gic进行相应初始化。

当定义的是GICV2时,则需要在include/configs/xxxxx.h中定义GICD_BASE和GICC_BASE,分别说明GIC的分发器基地址和cpu接口寄存器基地址。

当定义的是GICV3时,则需要在include/configs/xxxxx.h中定义GICD_BASE,说明GIC的主分发器基地址,因为cpu接口在使用gicv3时可通过armv8拓展的系统寄存器接口访问,所以这里不用定义cpu接口的基地址,gicv3使用系统寄存器访问cpu接口的原因,主要是因为cpu对gic cpu接口寄存器的访问是频繁的,为了少一些对系统总线的访问,直接在v3中使用系统寄存器访问,大大提高了cpu接口的读写速度,提高整体性能。

当是boot cpu执行时则会调用gic_init_secure对应安全组等相关的初始化,具体代码在arch/arm/lib/gic_64.S中,这里不展开只说明具体做了哪些事情:
如果是gicv3则主要是对分发器的初始化:激活group0,激活非安全的group1,激活安全的group1,激活安全和非安全的亲和性路由(绑定中断到某些cpu功能)。读取支持的最大spi中断数量,并配置。
如果是gicv2则首先对分发器进行初始化,激活group0和group1,和读取spi的支持数并配置。接着初始化cpu接口寄存器,激活group0和group1,并配置允许非安全模式下对GICC_PMR的访问,GICC_PMR是中断优先级的配置,在linux上没有对优先级有过多使用,只有一次性配置。

当是从核cpu执行到这里时则会根据branch_if_slave跳转到1标号处进行percpu的gic初始化,branch_if_slave的实现是读取cpu各自的标识符寄存器mpidr_el1获取自己的簇号进行区分的,细节可以查看此系统寄存器的描述。
同样的对percpu的gic初始化是gic_init_secure_percpu,意思就是每个cpu都要进行相应的设置,比如gicv3会对每个cpu的标识进行识别,以便精确ppi中断的分发,所以会将每个cpu的mpidr_el1值写入对应的每个cpu的重分发器里,所以称为percpu初始化,这也是gicv3能支持到128个cpu的原因,而gicv2只能支持8个cpu核心,具体实现可看arch/arm/lib/gic_64.S。
(这里说一个之前遇到的bug,在进行bring up时,linux无法产生任何中断,包括核间中断,后续发现就是u-boot中这段初始化没有调用,导致没有配置中断在非安全状态下路由到哪个group,所以即便在el1或者el0产生了中断,中断也被路由到了group0,而linux的gic设置为:只处理非安全状态下的group1中断,所以linux无法产生中断)
(5)u-boot虽然是运行单个cpu的程序,但也允许从核cpu进入u-boot,这里典型的例子就是使用SPIN_TABLE方式启动从核在u-boot中自选等待进入linux。当要使用spin_table时则必须开启CONFIG_ARMV8_MULTIENTRY 选项,用于让从核进入u-boot进行基本初始化。首先如果是主核自然不用走从核流程则直接在这里返回完成lowlevel_init的调用。如果是从核并且定义了gicv2或者gicv3则首先是当从核进入u-boot后,进入gic_wait_for_interrupt使用wfi休眠,等待主核的事件唤醒,也就是smp_kick_all_cpus操作,让从核cpu可以继续往下执行。bl armv8_switch_to_el2因为u-boot处于el3级别时,当启动linux时会将异常级别降低到el2或者el1来启动linux,根据具体设置来切换,而处于el3时则会将异常级别切换到el2,所以这里的操作是与主核一致,从核要进入linux,首先就是要将自己的异常级别从el3切换到el2。是否真实的能切到el2还要根据自己当前的级别,如果已经是el1了自然无法切换到el2。
当然如果定义了CONFIG_ARMV8_SWITCH_TO_EL1,说明还得切换从核到el1,这里将从核切换到el1级别去。
第三段代码如下:

#if defined(CONFIG_ARMV8_SPIN_TABLE) && !defined(CONFIG_SPL_BUILD) --------------------- (1)
	branch_if_master x0, x1, master_cpu
	b	spin_table_secondary_jump
	/* never return */
#elif defined(CONFIG_ARMV8_MULTIENTRY)
	branch_if_master x0, x1, master_cpu

	/*
	 * Slave CPUs
	 */
slave_cpu: ----------------------------------------------------------------------------- (2)
	wfe
	ldr	x1, =CPU_RELEASE_ADDR
	ldr	x0, [x1]
	cbz	x0, slave_cpu
	br	x0			/* branch to the given address */
#endif /* CONFIG_ARMV8_MULTIENTRY */
master_cpu:
	bl	_main

... /* arch/arm/cpu/armv8/spin_table_v8.S */
ENTRY(spin_table_secondary_jump) ------------------------------------------------------ (3)
.globl spin_table_reserve_begin
spin_table_reserve_begin:
0:	wfe
	ldr	x0, spin_table_cpu_release_addr
	cbz	x0, 0b
	br	x0
.globl spin_table_cpu_release_addr
	.align	3
spin_table_cpu_release_addr:
	.quad	0
.globl spin_table_reserve_end
spin_table_reserve_end:
ENDPROC(spin_table_secondary_jump)

(1)首先是当定义了CONFIG_ARMV8_SPIN_TABLE启动从核时并且不处于SPL中,spl是不需要启动从核的。当是主核运行时直接跳转到_main离开系统寄存器初始化,开始下一段board_f等的u-boot初始化流程,如果是从核则会跳转到spin_table_secondary_jump(3),在这里,从核cpu将会开始自旋,使用wfe指令休眠,并在接收到中断或者sev事件时醒来并读取spin_table_cpu_release_addr地址里的值,如果是0说明并不是引导进入linux的事件发生则继续调用wfe休眠等待唤醒,如果spin_table_cpu_release_addr地址里不是0说明这时候已经由linux唤醒了,从核需要进入linux开始下一段旅程,则直接跳转到指定的地址里去继续执行。spin_table_cpu_release_addr地址将会在u-boot加载linux的设备树时直接写入到linux使用的设备树的对应节点中去,以便不需要由人为指定地址,linux和u-boot即可完成从核启动的交接,这就是spin_table的工作原理。
(2)当不是spin_table启动从核时,说明这是由用户定义需要在u-boot或者其他地方使用从核,所以直接通过CPU_RELEASE_ADDR宏定义了一个跳转地址的存储地址,在用户任何时候需要从核执行时往CPU_RELEASE_ADDR地址写入对应要跳转的地址后开始执行从核。

综上在完成系统寄存器配置和从核引导启动后,u-boot将会跳转至_main离开系统寄存的初始化,开始u-boot本身的初始化。

以上是关于ARMv8架构u-boot启动流程详细分析的主要内容,如果未能解决你的问题,请参考以下文章

U-Boot 之七 详解 Driver Model 架构配置命令初始化流程

Linux嵌入式驱动学习之路⑤u-boot启动流程分析

Tiny4412 u-boot分析u-boot启动流程

u-boot源码汇编段简要分析

Tiny4412 u-boot分析u-boot 引导内核流程

u-boot.2012.10——mini2440(启动流程分析)