概要
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 <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 );
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.bin, u-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,我看一下启动流程
从图中可以得到以下几个结论:
- 1.px30上电后,会从
0xffff0000
获取romcode
并运行; - 2.然后依次从Nor Flash、Nand Flash、eMMC、SD/MMC获取
ID BLOCK
,ID 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_slave
和branch_if_master
在u-boot/arch/arm/include/asm/macro.h
定义;
2.gic_init_secure
和gic_init_secure_percpu
这两个中断初始化的关键函数在u-boot/arch/arm/lib/gic_64.S
定义;
3.armv8_switch_to_el2
和armv8_switch_to_el1
在u-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