Bootstrap

深度探索uboot

概要        

        Uboot 是操作系统启动前的运行的一段引导程序,他主要负责初始化部分硬件,包括时钟、内存等等,加载内核、文件系统、设备树等到内存上,启动操作系统。当然uboot作用远不止这些,比如由于uboot是裸机单任务运行,我们也可以在这里面对硬件进行初步的测试、升级系统等等。

        嵌入式开发中我们多多少少会涉及到uboot,所以还是有必要对这块做一些功课。我也是最近移植系统,遇到一些麻烦,对uboot做了一些研究,把他记录下来。本篇主要基于rockchip px30平台的uboot。

Rk平台固件概述

固件分区排列

        在开放源代码支持中,Rockchip使用 GPT作为其主要分区表。我们将GPT存储在LBA0〜LBA63中。下图为rk的存储图:

        

PartitionStart SectorNumber of SectorsPartition SizePartNum in GPTRequirements
MBR0000000001000000015120.5KB
Primary GPT100000001630000003F3225631.5KB
loader16400000040710400001bc040960002.5MB1preloader (miniloader or U-Boot SPL)
Vendor Storage716800001c0051200000200262144256KBSN, MAC and etc.
Reserved Space768000001e0038400000180196608192KBNot used
reserved1806400001f80128000000806553664KBlegacy DRM key
U-Boot ENV812800001fc064000000403276832KB
reserved281920000200081920000200041943044MBlegacy parameter
loader2163840000400081920000200041943044MB2U-Boot or UEFI
trust245760000600081920000200041943044MB3trusted-os like ATF, OP-TEE
boot(bootable must be set)327680000800022937600038000117440512112MB4kernel, dtb, extlinux.conf, ramdisk
rootfs26214400040000----MB5Linux system
Secondary GPT1677718300FFFFDF33000000211689616.5KB

如果preloader是miniloader,则loader2分区可用于uboot.img,trust分区可用于trust.img; 如果preloader是不带trust支持的SPL,则loader2分区可用于u-boot.bin,而trust分区不可用;如果preloader是具有trust支持的SPL(ATF或OPTEE),则loader2可用于u-boot.itb(包括u-boot.bin和trust二进制文件),而trust分区不可用。

写入分区表方法

  • 通过rkdeveloptool编写GPT分区表

rkdeveloptool db rkxx_loader_vx.xx.bin
rkdeveloptool gpt parameter_gpt.txt

 其中parameter_gpt.txt包含分区信息:

CMDLINE:mtdparts=rk29xxnand:0x00001f40@0x00000040(loader1),0x00000080@0x00001f80(reserved1),0x00002000@0x00002000(reserved2),0x00002000@0x00004000(loader2),0x00002000@0x00006000(atf),0x00038000@0x00008000(boot:bootable),@0x0040000(rootfs)

  • 通过U-boot写入GPT分区表

        在u-boot console中,“ gpt”命令可用于写入gpt分区表:

gpt - GUID Partition Table
 
Usage:
gpt <command> <interface> <dev> <partitions_list>
 - GUID partition table restoration and validity check
 Restore or verify GPT information on a device connected
 to interface
 Example usage:
 gpt write mmc 0 $partitions
 gpt verify mmc 0 $partitions</code>

 例如:

=> env set partitions name=rootfs,size=-,type=system
=> gpt write mmc 0 $partitions
Writing GPT: success!

注意:可以在u-boot console(使用“ env set”命令)或在u-boot的源代码中设置分区env
例如:

        

include/configs/kylin_rk3036.h
#define PARTS_DEFAULT \
        "uuid_disk=${uuid_gpt_disk};" \
...
 
#undef CONFIG_EXTRA_ENV_SETTINGS
#define CONFIG_EXTRA_ENV_SETTINGS \
        "partitions=" PARTS_DEFAULT \
  • 通过U-Boot的fastboot写入GPT分区表

        

启动介绍

首先,让我们弄清楚这个概念,当我们启动 Linux 操作系统时,有很多启动阶段;

然后,我们需要知道 image 应该如何打包,image 位于何处;

最后,我们将解释如何写入不同的媒体和从那里 boot 。

以下是 Rockchip 预发布的二进制文件,稍后可能会提到:

GitHub - rockchip-linux/rkbin: Firmware and Tool Binarys


1.1 启动流程

本章介绍了 Rockchip 应用处理器的一般启动流程,包括在Rockchip平台上使用什么 image 作为启动路径的细节:

- 使用来自 Upstream 或 Rockchip U-Boot 的 U-Boot TPL/SPL ,它们完全是源代码;

- 使用 Rockchp idbLoader,它由 Rockchip rkbin project 的 Rockchip ddr init bin 和 miniloader bin 组合而成;

+--------+----------------+----------+-------------+---------+
| Boot   | Terminology #1 | Actual   | Rockchip    | Image   |
| stage  |                | program  |  Image      | Location|
| number |                | name     |   Name      | (sector)|
+--------+----------------+----------+-------------+---------+
| 1      |  Primary       | ROM code | BootRom     |         |
|        |  Program       |          |             |         |
|        |  Loader        |          |             |         |
|        |                |          |             |         |
| 2      |  Secondary     | U-Boot   |idbloader.img| 0x40    | pre-loader
|        |  Program       | TPL/SPL  |             |         |
|        |  Loader (SPL)  |          |             |         |
|        |                |          |             |         |
| 3      |  -             | U-Boot   | u-boot.itb  | 0x4000  | including u-boot and atf
|        |                |          | uboot.img   |         | only used with miniloader
|        |                |          |             |         |
|        |                | ATF/TEE  | trust.img   | 0x6000  | only used with miniloader
|        |                |          |             |         |
| 4      |  -             | kernel   | boot.img    | 0x8000  |
|        |                |          |             |         |
| 5      |  -             | rootfs   | rootfs.img  | 0x40000 |
+--------+----------------+----------+-------------+---------+

当我们谈到从 eMMC/SD/U-Disk/Net 启动时,它们的概念不同:

  • 阶段1总是在 boot rom 中,它加载阶段2并可能加载阶段3(当启用 SPL_BACK_TO_BROM 选项时)。
  • 从 SPI Flash 启动是指 SPI Flash 中的第2和第3阶段(仅限 SPL 和 U-Boot )的固件,在其他地方是指阶段4/5的固件;
  • 从 eMMC 启动是指 eMMC 中的所有固件(包括阶段2、3、4、5);
  • 从 SD Card 启动是指 SD Card 中的所有固件(包括阶段2、3、4、5);
  • 从 U-Disk 启动是指磁盘中第4阶段和第5阶段(不包括 SPL和 U-Boot )的固件,可选仅包括第5阶段;
  • 从 Net/Tftp 启动是指网络上第4阶段和第5阶段的固件(不包括 SPL 和 U-Boot );

Rockchip bootflow.jpg

Boot Flow 1 是典型的使用 Rockchip miniloader 的 Rockchip 启动流程;

Boot Flow 2 用于大多数 SOCs,U-Boot TPL 用于 ddr 初始化,SPL用于 trust(ATF/OP-TEE)加载并运行到下一阶段;

注意1:若 loader1 有一个以上的阶段,程序将回到 bootrom 和 bootrom 加载并运行到下一个阶段。例如,如果 loader1 是 tpl 和 spl,bootrom 将首先运行到 tpl,tpl init ddr,然后返回 bootrom,然后加载并运行到 spl。

注意2:如果启用 trust ,loader1 需要同时加载 trust和 u-boot,然后在安全模式下运行 trust(armv8中为EL3),trust 执行初始化并在非安全模式下运行到 u-boot(armv8中的EL2)。

注意3:对于 trust 是 trust.img 或者 u-boot.itb,armv7 只有一个 tee.bin,而 armv8 有 bl31.elf 和 bl32 选项。

注意4:在 boot.img 中,内容可以是 zImage 及其Linux dtb,也可以是 grub.efi,也可以是AOSP boot.img,ramdisk为可选项;


1.2 打包选项

在我们知道启动阶段之后,

以下是第2~4阶段打包前的文件列表:

  • 源代码:
    • u-boot:u-boot-spl.binu-boot.bin(可使用 u-boot-nodtb.bin 和 u-boot.dtb 代替)
    • kernel:kernel Image/zImage 文件、kernel dtb,
    • ATF:bl31.elf
  • Rockchip binary:
    • ddr、usbplug、miniloader、bl31/op-tee(均带有“rkxx_”前缀和“_x.xx.bin”版本后缀);

我们为不同的解决方案提供了两种不同的 boot-loader 方法,它们的步骤和请求文件也完全不同。但并非所有平台都支持这两种引导加载程序方法。以下是要从这些文件打包 image 的类型:


1.2.1 The Pre-bootloader(idbloader)

什么是 idbloader?

idbloader.img文件是一个 Rockchip 格式的预加载程序,假设在SoC启动时工作,它包含:

- 由 Rockchip BootRom 知道的 IDBlock 头;

- DRAM init 程序,由 MaskRom 加载,运行在 SRAM 内部;

- 下一级加载程序,由 MaskRom 加载并在 DDR SDRAM 上运行;

您可以使用以下方法获取 idbloader。

从 Rockchip release loader 获取用于 eMMC 的 idbloader

对于eMMC,不需要打包 idbloader.img 文件,如果您使用的是 Rockchip release loader,则可以使用以下命令在 eMMC 上获取 idbloader:

rkdeveloptool db rkxx_loader_vx.xx.bin
rkdeveloptool ul rkxx_loader_vx.xx.bin

打包 idbloader.img 文件来自 Rockchip binary:

对于SD boot 或 eMMC(使用 rockusb wl 命令更新),您需要一个idbloader(与 ddr 和 miniloader 结合使用)。

tools/mkimage -n rkxxxx -T rksd -d rkxx_ddr_vx.xx.bin idbloader.img
cat rkxx_miniloader_vx.xx.bin >> idbloader.img

打包 idbloader.img 文件从 U-Boot TPL/SPL(它们完全开源):

tools/mkimage -n rkxxxx -T rksd -d tpl/u-boot-tpl.bin idbloader.img
cat spl/u-boot-spl.bin >> idbloader.img

下载 idbloader.img 到包含第2阶段的 0x40 地址偏移处,同时您需要一个 uboot.img 启动第3阶段。


1.2.2 U-Boot

1.2.2.1 uboot.img

使用 Rockchip miniloader 的 idbloader 时,需要软件包 u-boot.bin 通过Rockchip tool loaderimage转换为可加载的 miniloader 格式。

tools/loaderimage --pack --uboot u-boot.bin uboot.img $SYS_TEXT_BASE

其中 SoCs 可能有不同的 $SYS_TEXT_BASE。

1.2.2.2 u-boot.itb

使用 SPL 加载 ATF/OP-TEE 时,打包bl31.bin、u-boot-nodtb.bin 以及 uboot.dtb 为一个 FIT image。您可以跳过打包 Trust image 的步骤,并在下一节中下载该 image 。

make u-boot.itb

注意:请将 trust binary 复制到 u-boot 根目录并将其重命名为 tee.bin(armv7)或 bl31.elf(armv8)。


1.2.3 Trust

1.2.3.1 trust.img

使用 Rockchip miniloader 的 idbloader 时,需要使用 Rockchip tool trustmerge 将 bl31.bin 打包成可加载的 miniloader 格式。

tools/trustmerge tools/rk_tools/RKTRUST_RKXXXXTRUST.ini

下载 trust.img 到 0x6000 地址偏移处,用于使用Rockchip miniloader。


1.2.4 boot.img

此 image 将 kernel Image 和 dtb 文件打包到已知的文件系统(FAT 或 EXT2)image 中,以便进行启动发行版。

有关从 kernel zImage/Image、dtb 生成 boot.img 的详细信息,请参见的 Install kernel 。

下载 boot.img 到第4阶段的 0x8000 地址偏移处。


1.2.5 rootfs.img

下载 rootfs.img 到第5阶段的 0x40000 地址偏移处。只要您选择的 kernel 能够支持该文件系统,image 的格式就没有限制。


1.2.6 rkxx_loader_vx.xx.xxx.bin

这是 Rockchip 以 binary 方式提供的,用于使用 rkdeveloptool 升级固件到 eMMC,不能直接连接到媒体设备。

这是来自 ddr.bin, usbplug.bin, miniloader.bin 的包,Rockchip tool DB 命令将使 usbplug.bin 作为 Rockusb 设备在目标中运行。您可以跳过打包此 image,Rockchip 将在大多数时间提供此 image。



2 下载与从媒体设备启动

这里我们介绍如何将 image 写入不同的媒体设备。

准备好 image:

  • 使用 SPL:
    • idbloader.img
    • u-boot.itb
    • boot.img 或里面有 Image、dtb 与 exitlinux 的 boot 文件夹
    • rootfs.img
  • 使用 miniloader
    • idbloader.img
    • uboot.img
    • trust.img
    • boot.img 或里面有 Image、dtb 与 exitlinux 的 boot 文件夹
    • rootfs.img


2.1 从eMMC启动

eMMC在硬件板上,因此我们需要:

以下是下载 image 到目标的命令示例。

将 GPT 分区表下载到目标:

rkdeveloptool db rkxx_loader_vx.xx.bin
rkdeveloptool gpt parameter_gpt.txt
  • SPL:
rkdeveloptool db rkxx_loader_vx.xx.bin
rkdeveloptool wl 0x40 idbloader.img
rkdeveloptool wl 0x4000 u-boot.itb
rkdeveloptool wl 0x8000 boot.img
rkdeveloptool wl 0x40000 rootfs.img
rkdeveloptool rd
  • miniloader:
rkdeveloptool db rkxx_loader_vx.xx.bin
rkdeveloptool ul rkxx_loader_vx.xx.bin
rkdeveloptool wl 0x4000 uboot.img
rkdeveloptool wl 0x6000 trust.img
rkdeveloptool wl 0x8000 boot.img
rkdeveloptool wl 0x40000 rootfs.img
rkdeveloptool rd


2.2 从SD/TF Card启动

我们可以很容易地用 Linux-PC 的 dd 命令烧写 SD/TF card。

将 SD card 插入 PC,我们假设 /dev/sdb 是 SD card 设备。

  • SPL:
dd if=idbloader.img of=sdb seek=64
dd if=u-boot.itb of=sdb seek=16384
dd if=boot.img of=sdb seek=32768
dd if=rootfs.img of=sdb seek=262144
  • miniloader:
dd if=idbloader.img of=sdb seek=64
dd if=uboot.img of=sdb seek=16384
dd if=trust.img of=sdb seek=24576
dd if=boot.img of=sdb seek=32768
dd if=rootfs.img of=sdb seek=262144

为了确保拔出前所有东西都已写入 SD card ,建议运行以下命令:

sync

注意:使用从 SD card boot 时,需要更新 kernel 命令行到正确的 root 值(它位于 extlinux.conf)。

append  earlyprintk console=ttyS2,115200n8 rw root=/dev/mmcblk1p7 rootwait rootfstype=ext4 init=/sbin/init

在 u-boot 中将 GPT 分区表写入 SD card,u-boot 可以找到 boot 分区并运行到 kernel 中。

gpt write mmc 0 $partitions


2.3 从U-Disk启动

它与 boot-from-sdcard 相同,但请注意 U-Disk 只支持第4阶段和第5阶段,有关详细信息,请参阅 Boot Stage

如果 U-Disk 用于第4阶段和第5阶段,请将 U-Disk 格式化为 GPT 格式,并且至少有2个分区,写入 boot.img 和 rootfs.img 到它们对应的分区;

如果 U-Disk 只用于第5阶段,我们可以直接 dd rootfs.img 到 U-Disk 设备。

注意:需要更新 kernel 命令行到正确的 root 值(它位于 extlinux.conf)。

append  earlyprintk console=ttyS2,115200n8 rw root=/dev/sda1 rootwait rootfstype=ext4 init=/sbin/init

Uboot 编译打包

        我的项目里面使用的是Boot Flow 1 是典型的使用 Rockchip miniloader 的 Rockchip 启动流程,所以以后会忽略uboot-spl,都是uboot

        

Uboot 源码分析

uboot的任务

CPU初始刚上电的状态。需要小心的设置好很多状态,包括cpu状态、中断状态、MMU状态等等。其次,就是要根据硬件资源进行板级的初始化,代码重定向等等。最后,就是进入命令行状态,等待处理命令。
uboot,主要需要做如下事情

arch级的初始化

  • 关闭中断,设置svc模式
  • 禁用MMU、TLB
  • 关键寄存器的设置,包括时钟、看门狗的寄存器


板级的初始化

  • 堆栈环境的设置
  • 代码重定向之前的板级初始化,包括串口、定时器、环境变量、I2C\SPI等等的初始化
  • 进行代码重定向
  • 代码重定向之后的板级初始化,包括板级代码中定义的初始化操作、emmc、nand flash、网络、中断等等的初始化。
  • 进入命令行状态,等待终端输入命令以及对命令进行处理

上述工作,也就是uboot流程的核心。
 

uboot之前

        如前面章节所述,在uboot 之前有CPU内部的bootrom和rockchip的miniload,我看一下启动流程

        

从图中可以得到以下几个结论:

  • 1.px30上电后,会从0xffff0000获取romcode并运行;
  • 2.然后依次从Nor Flash、Nand Flash、eMMC、SD/MMC获取ID BLOCKID BLOCK正确则启动,都不正确则从USB端口下载;
  • 3.如果emmc启动,则先读取SDRAM(DDR)初始化代码到内部SRAM,然后初始化DDR,再将emmc上的代码(剩下的用户代码)复制到DDR运行;
  • 4.如果从USB下载,则先获取DDR初始化代码,下载到内部SRAM中,然后运行代码初始化DDR,再获取loader代码(用户代码),放到DDR中并运行;
  • 5.无论是何种方式,都需要DDR的初始化代码,结合前面RK3288的经验,就是向自己写的代码加上”头部信息”,这个”头部信息”就包含DDR初始化操作;

 uboot启动分析

        从u-boot.lds 可以看出来代码开始地方是start.s 中的_start

        


 

        接下来我们开始分析相关的代码

        arch/arm/cpu/armv8/start.s

.globl	_start
_start:
#ifdef 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
#endif

#if !CONFIG_IS_ENABLED(TINY_FRAMEWORK)
	.align 3

.globl	_TEXT_BASE
_TEXT_BASE:
#if defined(CONFIG_SPL_BUILD)
	.quad   CONFIG_SPL_TEXT_BASE
#else
	.quad	CONFIG_SYS_TEXT_BASE
#endif

/*
 * 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

这段主要指示bss各段的偏移地址,并调转到reset处


 

reset:
	/* Allow the board to save important registers */
	b	save_boot_params
.globl	save_boot_params_ret
save_boot_params_ret:
	/*
	 * Could be EL3/EL2/EL1, Initial State:
	 * Little Endian, MMU Disabled, i/dCache Disabled
	 */
	adr	x0, vectors            //将中断向量地址保存到x0
	switch_el x1, 3f, 2f, 1f       //根据CurrentEL的bit[3:2]位得知当前的EL级别,跳转到不同的分支进行处理,这里实测跳到3f,即上电为EL3
3:	msr	vbar_el3, x0           //将中断向量保存到vbar_el3(Vector Base Address Register (EL3))
	mrs	x0, scr_el3            //获取scr_el3(Secure Configuration Register)的值
	orr	x0, x0, #0xf           //将低四位设置为1:EA|FIQ|IRQ|NS  
	msr	scr_el3, x0            //写入scr_el3
	msr	cptr_el3, xzr          //清除cptr_el3(Architectural Feature Trap Register (EL3)),Enable FP/SIMD
	ldr	x0, =COUNTER_FREQUENCY //晶振频率:24000000hz
	msr	cntfrq_el0, x0         //将晶振频率写入cntfrq_el0(Counter-timer Frequency register) 
#ifdef CONFIG_ROCKCHIP
	msr	cntvoff_el2, xzr       /* clear cntvoff_el2 for kernel */
#endif
	b	0f                     //跳到本段结尾的0f,后面的未执行
2:	msr	vbar_el2, x0
	mov	x0, #0x33ff            //FP为Float Processor(浮点运算器);SIMD为Single Instruction Multiple Data(采用一个控制器来控制多个处理器)
	msr	cptr_el2, x0           /* Enable FP/SIMD */
	b	0f
1:	msr	vbar_el1, x0
	mov	x0, #3 << 20
	msr	cpacr_el1, x0          /* Enable FP/SIMD */
0:

注:
1.switch_el这一宏定义伪指令在u-boot/arch/arm/include/asm/macro.h定义;
2.vbar_el3等寄存器定义在文档ARMv8-A_Architecture_Reference_Manual_(Issue_A.a).pdf[2]中;
3.XZR/WZR(word zero rigiser)分别代表64/32位,zero register的作用就是0,写进去代表丢弃结果,拿出来是0;


 

中断向量的定义在文件u-boot/arch/arm/cpu/armv8/exceptions.S中,内容如下:

/*
 * Exception vectors.
 */
	.align	11 //注意这里的对齐11,是因为vbar_el3的低11为是Reserved,需要为0
                //因此需要从2^11=2k的倍数位置起存放vectors
	.globl	vectors
vectors:
	.align	7		/* Current EL Synchronous Thread */
	stp	x29, x30, [sp, #-16]!
	bl	_exception_entry //保存相关寄存器
	bl	do_bad_sync
	b	exception_exit //恢复相关寄存器

	.align	7		/* Current EL IRQ Thread */
	stp	x29, x30, [sp, #-16]!
	bl	_exception_entry
	bl	do_bad_irq
	b	exception_exit

	.align	7		/* Current EL FIQ Thread */
	stp	x29, x30, [sp, #-16]!
	bl	_exception_entry
	bl	do_bad_fiq
	b	exception_exit

	.align	7		/* Current EL Error Thread */
	stp	x29, x30, [sp, #-16]!
	bl	_exception_entry
	bl	do_bad_error
	b	exception_exit

	.align	7		 /* Current EL Synchronous Handler */
	stp	x29, x30, [sp, #-16]!
	bl	_exception_entry
	bl	do_sync
	b	exception_exit

	.align	7		 /* Current EL IRQ Handler */
	stp	x29, x30, [sp, #-16]!
	bl	_exception_entry
	bl	do_irq
	b	exception_exit

	.align	7		 /* Current EL FIQ Handler */
	stp	x29, x30, [sp, #-16]!
	bl	_exception_entry
	bl	do_fiq
	b	exception_exit

	.align	7		 /* Current EL Error Handler */
	stp	x29, x30, [sp, #-16]!
	bl	_exception_entry
	bl	do_error
	b	exception_exit

/*
 * Enter Exception.
 * This will save the processor state that is ELR/X0~X30
 * to the stack frame.
 */
_exception_entry:
	stp	x27, x28, [sp, #-16]!
	stp	x25, x26, [sp, #-16]!
	stp	x23, x24, [sp, #-16]!
	stp	x21, x22, [sp, #-16]!
	stp	x19, x20, [sp, #-16]!
	stp	x17, x18, [sp, #-16]!
	stp	x15, x16, [sp, #-16]!
	stp	x13, x14, [sp, #-16]!
	stp	x11, x12, [sp, #-16]!
	stp	x9, x10, [sp, #-16]!
	stp	x7, x8, [sp, #-16]!
	stp	x5, x6, [sp, #-16]!
	stp	x3, x4, [sp, #-16]!
	stp	x1, x2, [sp, #-16]!

	/* Could be running at EL3/EL2/EL1 */
	switch_el x11, 3f, 2f, 1f
3:	mrs	x1, esr_el3
	mrs	x2, elr_el3
	mrs	x3, daif
	mrs	x4, vbar_el3
	mrs	x5, spsr_el3
	sub	x6, sp, #(8*30)
	mrs	x7, sctlr_el3
	mrs	x8, scr_el3
	mrs	x9, ttbr0_el3
	b	0f
2:	mrs	x1, esr_el2
	mrs	x2, elr_el2
	mrs	x3, daif
	mrs	x4, vbar_el2
	mrs	x5, spsr_el2
	sub	x6, sp, #(8*30)
	mrs	x7, sctlr_el2
	mrs	x8, hcr_el2
	mrs	x9, ttbr0_el2
	b	0f

1:	mrs	x1, esr_el1
	mrs	x2, elr_el1
	mrs	x3, daif
	mrs	x4, vbar_el1
	mrs	x5, spsr_el1
	sub	x6, sp, #(8*30)
	mrs	x7, sctlr_el1
	mov	x8, #0	/* Not used, EL1 don't have register, like 'scr_el1' */
	mrs	x9, ttbr0_el1
0:
	stp     x2, x0, [sp, #-16]!
	stp	x3, x1, [sp, #-16]!
	stp	x5, x4, [sp, #-16]!
	stp	x7, x6, [sp, #-16]!
	stp	x9, x8, [sp, #-16]!
	mov	x0, sp
	ret


exception_exit:
	add	sp, sp, #(8*8)/* see: sys registers size of struct pt_regs */
	ldp	x2, x0, [sp],#16
	switch_el x11, 3f, 2f, 1f
3:	msr	elr_el3, x2
	b	0f
2:	msr	elr_el2, x2
	b	0f
1:	msr	elr_el1, x2
0:
	ldp	x1, x2, [sp],#16
	ldp	x3, x4, [sp],#16
	ldp	x5, x6, [sp],#16
	ldp	x7, x8, [sp],#16
	ldp	x9, x10, [sp],#16
	ldp	x11, x12, [sp],#16
	ldp	x13, x14, [sp],#16
	ldp	x15, x16, [sp],#16
	ldp	x17, x18, [sp],#16
	ldp	x19, x20, [sp],#16
	ldp	x21, x22, [sp],#16
	ldp	x23, x24, [sp],#16
	ldp	x25, x26, [sp],#16
	ldp	x27, x28, [sp],#16
	ldp	x29, x30, [sp],#16
	eret

 这一部分功能就是根据当前的EL级别,配置中断向量、MMU、Endian、i/d Cache等,比较重要。


lowlevel_init 

        看 start.S 的源码中,还有对是否是主CPU的区分,此处貌似配置的是单核启动,所以反汇编中就没有对主从CPU判断代码了。没有新的指令,这一段读起来就非常顺畅了。做的事情就是调用 gic_init_secure 和 gic_init_secure_percpu 2个函数,从函数名称就可以看出,这是初始化主CPU的中断寄存器和其他各个CPU的中断寄存器。

	/* Processor specific initialization */
	bl	lowlevel_init
	……
WEAK(lowlevel_init)
	mov	x29, lr		       /* Save LR */
#if defined(CONFIG_ROCKCHIP)
	/* switch to el1 secure */
#if defined(CONFIG_SWITCH_EL3_TO_EL1)  //实测没有定义,不需要从EL3切换到EL1,从前面可以看出,现在已经是EL1
	/*
	 * Switch to EL1 from EL3
	 */
	mrs	x0, CurrentEL	       /* check currentEL */
	cmp	x0, 0xc 
	b.ne	el1_start	       /* currentEL != EL3 */
	ldr	x0, =0xd00	       /* ST, bit[11] | RW, bit[10] | HCE, bit[8] */
	msr	scr_el3, x0
	ldr	x0, =0x3c5	       /* D, bit[9] | A, bit[8] | I, bit[7] | F, bit[6] | 0b0101 EL1h */
	msr	spsr_el3, x0
	ldr	x0, =el1_start
	msr	elr_el3, x0
	eret
el1_start:
	nop
#endif /* CONFIG_SWITCH_EL3_TO_EL1 */
#endif /* CONFIG_ROCKCHIP */
#if defined(CONFIG_GICV2) || defined(CONFIG_GICV3)  //实测定义的是CONFIG_GICV3
	branch_if_slave x0, 1f 	       //通过mpidr_el1寄存器,判断当前处理器是否是从属CPU,如果是选择所有affinity为0的作为主CPU
	ldr	x0, =GICD_BASE         //把GICD基地址作为参数传给gic_init_secure 
	bl	gic_init_secure        //初始化主CPU的中断寄存器
1:
#if defined(CONFIG_GICV3)
	ldr	x0, =GICR_BASE         //把GICR基地址作为参数传给gic_init_secure_percpu
	bl	gic_init_secure_percpu //初始化其它各个CPU的中断寄存器
#elif defined(CONFIG_GICV2)            //未执行
	ldr	x0, =GICD_BASE
	ldr	x1, =GICC_BASE
	bl	gic_init_secure_percpu
#endif
#if defined(CONFIG_ROCKCHIP)
	/*
	 * Setting HCR_EL2.TGE AMO IMO FMO for exception rounting to EL2
	 */
	mrs	x0, CurrentEL	       /* check currentEL */
	cmp	x0, 0x8                //根据CurrentEL的bir[3:2]判断当前运行级别,0xC(EL3)、0x8(EL2)、0x4(EL1)、0x0(EL0),实测并没处于EL2,后面的内容不执行
	b.ne	endseting	       /* currentEL != EL2 */
	mrs	x9, hcr_el2            //hceng:hcr_el2(Hypervisor Configuration Register)
	orr	x9, x9, #(7 << 3)      /* HCR_EL2.AMO IMO FMO set */
	orr	x9, x9, #(1 << 27)     /* HCR_EL2.TGE set */
	msr	hcr_el2, x9
endseting:
	nop
#endif /* CONFIG_ROCKCHIP */
	branch_if_master x0, x1, 2f    //通过mpidr_el1寄存器,判断当前处理器是否是主CPU,如果是选择所有affinity为0的作为主CPU;实测跳到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.
	 */
	bl	armv8_switch_to_el2  
#ifdef CONFIG_ARMV8_SWITCH_TO_EL1
	bl	armv8_switch_to_el1
#endif
#endif /* CONFIG_ARMV8_MULTIENTRY */
2:                                   //前面的都没执行,跳到这,返回
	mov	lr, x29		     /* Restore LR */
	ret
ENDPROC(lowlevel_init)

注:
1.branch_if_slavebranch_if_masteru-boot/arch/arm/include/asm/macro.h定义;
2.gic_init_securegic_init_secure_percpu这两个中断初始化的关键函数在u-boot/arch/arm/lib/gic_64.S定义;
3.armv8_switch_to_el2armv8_switch_to_el1u-boot/arch/arm/cpu/armv8/exceptions.S定义; 


 crt0_64.S

  _main 函数的定义在 arch/arm/lib/crt0_64.S 文件中。文件头部的注释对这个函数的说明非常详细。

ENTRY(_main)
/*
 * Set up initial C runtime environment and call board_init_f(0).
 */

	ldr	x0, =(CONFIG_SYS_INIT_SP_ADDR)
	bic	sp, x0, #0xf	/* 16-byte alignment for ABI compliance */
	mov	x0, sp
	bl	board_init_f_alloc_reserve
	mov	sp, x0
	/* set up gd here, outside any C code */
	mov	x18, x0
	bl	board_init_f_init_reserve
	bl	board_init_f_init_serial

	mov	x0, #0
	bl	board_init_f

#if (defined(CONFIG_SPL_BUILD) && !defined(CONFIG_TPL_BUILD) && !defined(CONFIG_SPL_SKIP_RELOCATE)) || \
	!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.
 */
	ldr	x0, [x18, #GD_START_ADDR_SP]	/* x0 <- gd->start_addr_sp */
	bic	sp, x0, #0xf	/* 16-byte alignment for ABI compliance */
	ldr	x18, [x18, #GD_NEW_GD]		/* x18 <- gd->new_gd */

#ifndef CONFIG_SKIP_RELOCATE_UBOOT
	adr	lr, relocation_return
#if CONFIG_POSITION_INDEPENDENT
	/* Add in link-vs-runtime offset */
	adr	x0, _start		/* x0 <- Runtime value of _start */
	ldr	x9, _TEXT_BASE		/* x9 <- Linked value of _start */
	sub	x9, x9, x0		/* x9 <- Run-vs-link offset */
	add	lr, lr, x9
#endif
	/* Add in link-vs-relocation offset */
	ldr	x9, [x18, #GD_RELOC_OFF]	/* x9 <- gd->reloc_off */
	add	lr, lr, x9	/* new return address after relocation */
	ldr	x0, [x18, #GD_RELOCADDR]	/* x0 <- gd->relocaddr */
	b	relocate_code
#endif

relocation_return:

/*
 * Set up final (full) environment
 */
	bl	c_runtime_cpu_setup		/* still call old routine */
#endif /* !CONFIG_SPL_BUILD */


/*
 * Clear BSS section
 */
	ldr	x0, =__bss_start		/* this is auto-relocated! */
	ldr	x1, =__bss_end			/* this is auto-relocated! */
clear_loop:
	str	xzr, [x0], #8
	cmp	x0, x1
	b.lo	clear_loop

	/* call board_init_r(gd_t *id, ulong dest_addr) */
	mov	x0, x18				/* gd_t */
	ldr	x1, [x18, #GD_RELOCADDR]	/* dest_addr */
	b	board_init_r			/* PC relative jump */

	/* NOTREACHED - board_init_r() does not return */

ENDPROC(_main)

1、设置C函数运行环境。此处主要是设置栈地址,栈地址的值通过宏 CONFIG_SYS_INIT_SP_ADDR(0x82bffe80) 定义。通过位清除指令 BIC ,使栈地址16字节(64位)对齐。 设置好栈之后,就终于可以开始调用C函数了。第一个调用的C函数就是 board_init_f_alloc_reserve ,它位于 common/board_init.c 中。其参数通过X0传入,即SP的值。函数执行的操作是将SP的值减去一个 GD(global_data) 的大小,然后返回。此返回值作为新的栈地址写入到SP中。栈由高地址向低地址移动,而数据指针是指向数据的低地址,所以这个返回的地址即是新的栈地址,同时也是指向GD的指针。所以GD的指针被存放到了X18寄存器之后,还作为函数 board_init_f_init_reserve 的入参被调用

2、调用 board_init_f 。这个函数的定义是在 /common/board_f.c 中,其作用是初始化系统RAM。结合反汇编的结果,将各种define去掉,得到的函数定义如下,入参由X0寄存器传入,值为0。对于上一步骤中将GD的指针存入了X18寄存器,在C代码中,通过宏的形式,以 gd 这个符号来获取存储在X18寄存器中的值。

void board_init_f(ulong boot_flags)
{
	gd->flags = boot_flags;
	gd->have_console = 0;

#if defined(CONFIG_DISABLE_CONSOLE)
	gd->flags |= GD_FLG_DISABLE_CONSOLE;
#endif

	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
}

board_init_f 函数本身非常简单,其调用的 initcall_run_list 就是将 init_sequence_f 数组中存放的各种初始化函数都运行一遍。将各种配置选项删除后,剩下一些通用的初始化操作如下。

static const init_fnc_t init_sequence_f[] = {
	setup_mon_len, // 计算整个镜像的长度gd->mon_len
#ifdef CONFIG_OF_CONTROL
	fdtdec_setup,
#endif
	initf_malloc,// early malloc的内存池的设定
	log_init,
	initf_bootstage,	/* uses its own timer, so does not need DM */
	initf_console_record,// console的log的缓存
#if defined(CONFIG_HAVE_FSP)
	arch_fsp_init,
#endif
	arch_cpu_init,		/* basic arch cpu dependent setup */
	mach_cpu_init,		/* SoC/machine dependent CPU setup */
	initf_dm,
	arch_cpu_init_dm,
#if defined(CONFIG_BOARD_EARLY_INIT_F)
	board_early_init_f,
#endif
#if defined(CONFIG_PPC) || defined(CONFIG_SYS_FSL_CLK) || defined(CONFIG_M68K)
	/* get CPU and bus clocks according to the environment variable */
	get_clocks,		/* get CPU and bus clocks (etc.) */
#endif
#if !defined(CONFIG_M68K)
	timer_init,		/* initialize timer */
#endif
#if defined(CONFIG_BOARD_POSTCLK_INIT)
	board_postclk_init,
#endif
	env_init,		/* initialize environment */
	init_baud_rate,		/* initialze baudrate settings */
	serial_init,		/* serial communications setup */
	console_init_f,		/* stage 1 init of console */
	display_options,	/* say that we are here */
	display_text_info,	/* show debugging info if required */
#if defined(CONFIG_DISPLAY_CPUINFO)
	print_cpuinfo,		/* display cpu info (and speed) */
#endif
#if defined(CONFIG_DISPLAY_BOARDINFO)
	show_board_info,
#endif
	INIT_FUNC_WATCHDOG_INIT
#if defined(CONFIG_MISC_INIT_F)
	misc_init_f,
#endif
	INIT_FUNC_WATCHDOG_RESET
#if defined(CONFIG_SYS_I2C)
	init_func_i2c,
#endif
#if defined(CONFIG_HARD_SPI)
	init_func_spi,
#endif
#if defined(CONFIG_ROCKCHIP_PRELOADER_SERIAL)
	announce_pre_serial,
#endif
	announce_dram_init,
	dram_init,		/* configure available RAM banks */
// ddr的初始化,最重要的是ddr ram size的设置!!!!gd->ram_size
#ifdef CONFIG_POST
	post_init_f,
#endif
	INIT_FUNC_WATCHDOG_RESET
#if defined(CONFIG_SYS_DRAM_TEST)
	testdram,
#endif /* CONFIG_SYS_DRAM_TEST */
	INIT_FUNC_WATCHDOG_RESET

#ifdef CONFIG_POST
	init_post,
#endif
	INIT_FUNC_WATCHDOG_RESET
	/*
	 * Now that we have DRAM mapped and working, we can
	 * relocate the code and continue running from DRAM.
	 *
	 * Reserve memory at end of RAM for (top down in that order):
	 *  - area that won't get touched by U-Boot and Linux (optional)
	 *  - kernel log buffer
	 *  - protected RAM
	 *  - LCD framebuffer
	 *  - monitor code
	 *  - board info struct
	 */
//========================================
	setup_dest_addr,
#ifdef CONFIG_PRAM
	reserve_pram,
#endif
	reserve_round_4k,
#ifdef CONFIG_ARM
	reserve_mmu,
#endif
	reserve_video,
	reserve_trace,
	reserve_uboot,
	reserve_malloc,
#ifdef CONFIG_SYS_NONCACHED_MEMORY
	reserve_noncached,
#endif
	reserve_board,
	setup_machine,
	reserve_global_data,
	reserve_fdt,
	reserve_bootstage,
	reserve_arch,
	reserve_stacks,
// ==以上部分是对relocate区域的规划
	dram_init_banksize,
	show_dram_config,
#ifdef CONFIG_SYSMEM
	sysmem_init,		/* Validate above reserve memory */
#endif
	display_new_sp,
#ifdef CONFIG_OF_BOARD_FIXUP
	fix_fdt,
#endif
	INIT_FUNC_WATCHDOG_RESET
	reloc_fdt,
	reloc_bootstage,
	setup_reloc,
	NULL,
};

经过这一系列的初始化操作,SDRAM被初始化OK,gd 中有关u-boot程序的相关地址已经由FLASH指向SDRAM。虽然此时,gd 本身还是在芯片的SRAM中。


 3、这是u-boot完成自举阶段最重要的一个步骤,将存储在FLASH中的u-boot拷贝到SDRAM中。指令如下:

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

        adr     lr, relocation_return
        ldr     x9, [x18, #GD_RELOC_OFF]        /* x9 <- gd->reloc_off */
        add     lr, lr, x9      /* new return address after relocation */
        ldr     x0, [x18, #GD_RELOCADDR]        /* x0 <- gd->relocaddr */
        b       relocate_code

核心函数是 relocate_code ,这之前的操作与第1步的操作非常相似——设置SP和GD的地址,此时这2个值都已经是指向了SDRAM中了。reloacate_code 函数的定义在 /arch/arm/lib/relocate_64.S 中。 


4、这一步骤是进入SDRAM中运行程序最后所做的环境准备工作。给 .bss 块中存储的变量分配空间——也就是将 .bss 块指向的内存区域进行清0操作。(.bss 代码块中存放的未初始化的全局变量和未初始化的局部静态变量,因为它们不需要存储初始值,所以在程序中只存储 .bss 块的大小,直到运行时才在内存中分配空间)

relocation_return:
/*
 * Set up final (full) environment
 */
        bl      c_runtime_cpu_setup             /* still call old routine */
/* TODO: For SPL, call spl_relocate_stack_gd() to alloc stack relocation */
/*
 * Clear BSS section
 */
        ldr     x0, =__bss_start                /* this is auto-relocated! */
        ldr     x1, =__bss_end                  /* this is auto-relocated! */
        mov     x2, #0
clear_loop:
        str     x2, [x0]
        add     x0, x0, #8
        cmp     x0, x1
        b.lo    clear_loop

这一步骤与前面不同的地方在于,它已经是运行在SDRAM中,这之前的程序还是在SRAM中运行。所以在第3步的最后,将LR指向了已经移动到SDRAM中的 relocation_return 。于是,在 relocate_code 执行完毕,调用返回指令 RET 时,它就跳转到了SDRAM中了。而在进入SDRAM的起始阶段,此处留了一个钩子 c_runtime_cpu_setup 用于给某些CPU在进入前进行某些必要的操作。


5、最后,将新的 GD 和 u-boot 在SDRAM中的起始地址作为入参,调用 board_init_r 。

        /* call board_init_r(gd_t *id, ulong dest_addr) */
        mov     x0, x18                         /* gd_t */
        ldr     x1, [x18, #GD_RELOCADDR]        /* dest_addr */
        b       board_init_r                    /* PC relative jump */

 board_init_r     所在文件路径:u-boot/common/board_f.c
与前面的board_init_f类似,board_init_r中调用initcall_run_list(init_sequence_r)init_sequence_r是个数组,里面是将要进行初始化的函数列表,又是一系列的初始化操作。之前遇到的LCD初始化就是在这里。
初始化数组列表最后一个成员是run_main_loop,将最终跳到主循环main_loop

crt0_64.S主要就是为C语言运行设置栈和进行了重定位,以及两个阶段的初始化:board_init_f(front)和board_init_r(rear),最后进入主循环。

总结

U-Boot启动流程示意图

 启动kernel部分

/* 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 */
#endif /* CONFIG_VERSION_VARIABLE */

	cli_init();

	run_preboot_environment_command();

#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);
	autoboot_command_fail_handle();

	cli_loop();
	panic("No CLI available");
}

这段函数是

        设置命令运行环境

        尝试启动kernel,如果启动成功就不返回了

        不成功进入命令行,自己使用命令行查找问题

主要分析启动kernel部分,如下是我的启动命令

+       "setenv resin_kernel_load_addr ${kernel_addr_r};" \
+       "run resin_set_kernel_root;" \
+       "run set_os_cmdline;" \
+       "setenv bootargs ${resin_kernel_root} rootwait console=ttyFIQ0,1500000 console=tty1  ${os_cmdline} panic=10 loglevel=7;" \
+       "load mmc ${resin_dev_index}:${resin_root_part} ${kernel_addr_r} /boot/Image;" \
+       "load mmc ${resin_dev_index}:${resin_root_part} ${fdt_addr_r} /boot/px30-evb-ddr3-v10-linux.dtb;" \
+       "booti ${kernel_addr_r} - ${fdt_addr_r}"

        主要是设置启动参数,加载Image和dtb,booti,通过uuid指示文件系统所在


booti

int do_booti(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[])
{
	int ret;

	/* Consume 'booti' */
	argc--; argv++;
	if (booti_start(cmdtp, flag, argc, argv, &images))
		return 1;

	/*
	 * We are doing the BOOTM_STATE_LOADOS state ourselves, so must
	 * disable interrupts ourselves
	 */
	bootm_disable_interrupts();

	images.os.os = IH_OS_LINUX;
	images.os.arch = IH_ARCH_ARM64;
	ret = do_bootm_states(cmdtp, flag, argc, argv,
#ifdef CONFIG_SYS_BOOT_RAMDISK_HIGH
			      BOOTM_STATE_RAMDISK |
#endif
			      BOOTM_STATE_OS_PREP | BOOTM_STATE_OS_FAKE_GO |
			      BOOTM_STATE_OS_GO,
			      &images, 1);

	return ret;
}

 

参考

        研究过程一些优秀的文章帮助很大,本篇不详尽的地方可以阅读

        https://blog.csdn.net/ooonebook/category_6484145.html

        u-boot源码阅读(一) | linkthinking

        Rockchip | 启动引导的各个阶段及其对应固件_Systemcall驿站-CSDN博客

        RK3399——裸机大全 | hceng blog

;