深度探索uboot

概要        

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

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

Rk平台固件概述

固件分区排列

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

        

Partition Start Sector Number of Sectors Partition Size PartNum in GPT Requirements
MBR 0 00000000 1 00000001 512 0.5KB
Primary GPT 1 00000001 63 0000003F 32256 31.5KB
loader1 64 00000040 7104 00001bc0 4096000 2.5MB 1 preloader (miniloader or U-Boot SPL)
Vendor Storage 7168 00001c00 512 00000200 262144 256KB SN, MAC and etc.
Reserved Space 7680 00001e00 384 00000180 196608 192KB Not used
reserved1 8064 00001f80 128 00000080 65536 64KB legacy DRM key
U-Boot ENV 8128 00001fc0 64 00000040 32768 32KB
reserved2 8192 00002000 8192 00002000 4194304 4MB legacy parameter
loader2 16384 00004000 8192 00002000 4194304 4MB 2 U-Boot or UEFI
trust 24576 00006000 8192 00002000 4194304 4MB 3 trusted-os like ATF, OP-TEE
boot(bootable must be set) 32768 00008000 229376 00038000 117440512 112MB 4 kernel, dtb, extlinux.conf, ramdisk
rootfs 262144 00040000 - - - -MB 5 Linux system
Secondary GPT 16777183 00FFFFDF 33 00000021 16896 16.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    
 - 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

 例如:

=> 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 );

深度探索uboot_第1张图片

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在硬件板上,因此我们需要:

  • 把硬件板切换到 maskrom mode;
  • 用 USB 线将目标连接到 PC 机;
  • 使用 rkdeveloptool 将 image 下载到 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,我看一下启动流程

        深度探索uboot_第2张图片

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

  • 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

        深度探索uboot_第3张图片


 

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

        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 
#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启动流程示意图

深度探索uboot_第4张图片

 启动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

你可能感兴趣的:(深度探索嵌入式linux系统,uboot)