uboot启动流程(armv7)
uboot介绍
uboot就是一段引导程序,在加载系统内核之前,完成硬件初始化,内存映射,为后续内核的引导提供一个良好的环境。
uboot是bootloader的一种,全称为universal boot loader。
第一阶段前(Boot rom)
Boot Rom是芯片内部ROM固化程序,是uboot 的引导代码(firmware)。Boot room读硬件的启动信息(拨码开关设置),从指定的启动介质(SD、MMC等)中读取uboot-spl代码。
uboot已经是一个bootloader了,那么为什么还多一个uboot spl呢?
这个主要原因是对于一些SOC来说,它的内部SRAM可能会比较小,小到无法装载下一个完整的uboot镜像,那么就需要spl,它主要负责初始化外部RAM和环境,并加载真正的uboot镜像到外部RAM(DDR)中来执行。
所以由此来看,SPL应该是一个非常小的loader程序,可以运行于SOC的内部SRAM中,它的主要功能就是加载真正的uboot并运行之。
第一阶段(uboot-spl)
在arch级初始化(架构体系级)
_start———–>reset————–>关闭中断,设置SVC模式
………………………………|
………………………………———->cpu_init_cp15———–>关闭MMU,TLB
………………………………|
………………………………———->cpu_init_crit————->lowlevel_init————->关键寄存器的配置和初始化
………………………………|
………………………………———->_main————–>进入板级初始化,具体看下面
板级初始化
_main————–>board_init_f_alloc_reserve —————>堆栈、GD、early malloc空间的分配
…………|
…………————->board_init_f_init_reserve —————>堆栈、GD、early malloc空间的初始化
…………|
…………————->board_init_f —————>uboot relocate前的板级初始化以及relocate的区域规划
…………|
…………————->relocate_code、relocate_vectors —————>进行uboot和异常中断向量表的重定向
…………|
…………————->旧堆栈的清空
…………|
…………————->board_init_r —————>uboot relocate后的板级初始化
…………|
…………————->run_main_loop —————>进入命令行状态,等待终端输入命令以及对命令进行处理
uboot启动入口为 _start (在u-boot.lds链接文件中可找到),_start函数跳转到reset函数(reset函数为spl的核心,结束进入第二阶段)
reset函数会设置CPU为SVC32模式,关闭FIQ和IRQ中断
resset函数跳转cpu_init_cp15 , 控制cp15协处理器,启动ICACHE,关闭DCACHE,关闭MMU和TLB
resset函数跳转cpu_init_crit,再进入lowlevel_init函数。内部RAM初始化,设置临时堆栈(芯片级初始化)
resset函数跳转_main函数,进入板级初始化 ,设置c语言运行环境。
_main 函数首先设置堆栈基地址,为GD(全局变量)结构体分配内存
_main函数调用board_init_f (c语言开始函数) ,重定位前板级初始化。设置GD各成员内存地址、初始化部分外设接口(串口)、打印板块信息。
_main函数调用relocate_code、relocate_vectors函数,对uboot和中断向量表进行重定位
_main函数清除.BSS段
_main函数调用board_init_r ,重定位后板级初始化。设置外设接口、环境变量、中断等初始化。
board_init_r函数调用run_main_loop 函数,倒计时等待中断输入命令,超时执行bootcmd命令启动内核
第二阶段(uboot)
1.进入run_main_loop 函数,再进入main_loop函数。开始进行boot delay倒计时
2.倒计时结束前,hush shell有输入命令。进入cmd_process 函数查找并执行命令,执行函数do_xxx()
3.倒计时结束时没有输入,uboot执行bootcmd命令,启动内核
4.bootcmd保持默认命令,读取Linux镜像文件和设备树文件(从MMC、SD卡、网络读取等)到DARM(DDR)中,然后启动内核(bootz、bootm)。
5.bootz命令启动内核,调用kernel_entry函数。是内核的入口函数,汇编函数。
6.kernel_entry函数有三个参数,为uboot向内核的传递参数。使用R0,R1和R2三个寄存器传参。
R0 = 0
R1 = 机器类型ID(machid,开发板CPU的ID)
Linux 内核会在自己的机器 ID 列表里面查找是否存在与 uboot 传递进来的 machid 匹配的项目,如果存在就说明 Linux 内核支持这个机器,内核定义一个 machine_desc 结构体来描述这个设备。
如果使用设备树的话这个 machid 就无效了(为~0),设备树存有一个“兼容性”这个属性,Linux 内核会比较“兼容性”属性的值(字符串)来查看是否支持这个机器。
根节点/下面的compatible属性(兼容性),内核启动的时候会检查是否支持此平台。而各个设备的compatible属性是匹配驱动程序
R2 =
如果不使用设备树的话,r2 应该是启动参数标记列表起始地址,也就是环境变量 bootargs 的值
如果使用设备树的话,r2 应该是设备树的起始地址,设备树中有一个特殊chosen 子节点,chosen 子节点中有bootargs属性,为uboot在booz的过程中,把bootargs传递进设备树中
uboot相关问题
- 为什么要设置成svc模式?
- svc模式属于特权模式,可以访问所有硬件受控资源。相对于其他的模式,SVC模式可以访问的资源更多。
- 如何设置SVC模式
- 设置CPSR(当前程序状态寄存器)寄存器
- 为什么关闭中断?
- 在启动过程中,中断环境并没有完全准备好,也就是中断向量表和中断处理函数并没有完成设置,一旦有中断产生,可能会导致预想不到的问题,或者是程序跑飞。
- 如何关闭中断?
- 设置CPSR(当前程序状态寄存器)寄存器
- CPSR寄存器
- 条件标志位
控制位
- 为什么关闭MMU
MMU是用于虚拟地址向物理地址进行映射的一个结构。在 uboot阶段操作的就直接是 物理地址,所以不需要转换。
为什么启动ICACHE(指令),关闭DCACHE(数据)
启动指令CACHE课可以加快指令读取的速度,但是数据CACHE 必须 要关闭,因为它本身是一个CPU的二级缓存,在运行程序的时候可能会往里面去取数据,但是此时ram里面的数据可能并没有存入到里面,这就可能导致读取到错误的数据。
为什么清理.BBS段
bss段都是未初始化的全局变量或者已经初始化为零的变量,本来就是零,直接清零就好。不清零的话未初始化的变量可能会存在未知的数值。
global_data(GD)功能?
在某些情况下,uboot是在某些只读存储器上运行,比如ROM、nor flash等等。在uboot被重定向到RAM(可读可写)之前,我们都无法写入数据,更无法通过全局变量来传递数据。uboot把global_data放在RAM区,并且使用global_data来存储全局数据。由此来解决上述场景中无法使用全局变量的问题。
global_data数据结构结构体定义为struct global_data,被typedef为gd_t。重要成员如下:
bd_t *bd:board info数据结构定义,位于文件 include/asm-arm/u-boot.h定义,主要是保存开发板的相关参数。
unsigned long env_addr:环境变量的地址。
unsigned long ram_top:RAM空间的顶端地址
unsigned long relocaddr:UBOOT重定向后地址 phys_size_t ram_size:物理ram的size
unsigned long irq_sp:中断的堆栈地址
unsigned long start_addr_sp:堆栈地址
unsigned long reloc_off:uboot的relocation的偏移
struct global_data *new_gd:重定向后的struct global_data结构体
const void fdt_blob:我们设备的dtb地址
voidnew_fdt:relocation之后的dtb地址
unsigned long fdt_size:dtb的长度
struct udevice *cur_serial_dev:当前使用的串口设备。
global_data内存分布
——————————————————— <—–(gd->ram_top) 高地址
| 最高的区域
———————————————————
| ……
———————————————————
| uboot代码区域
——————————————————— <—–(gd->relocaddr)
| ……
———————————————————
| Board Info区域
——————————————————— <—–(gd->bd)
| 新global_data区域
——————————————————— <—–(gd->new_gd)
| fdt区域
——————————————————— <—–(gd->new_fdt)
| ……
——————————————————— <—–(gd->start_addr_sp)
| 堆栈区域
———————————————————低地址
注意:最终global_data的地址存放在r9中了。
为什么要进行uboot重定位
uboot 会将自己重定位到 DRAM(DDR) 最后面的地址区域,也就是将自己拷贝到 DRAM 最后面的内存区域中。这么做的目的是给 Linux 腾出空间,防止 Linuxkernel 覆盖掉 uboot,将 DRAM 前面的区域完整的空出来。
重定位流程?
对relocate进行空间规划
计算uboot代码空间到relocation的位置的偏移
relocate旧的global_data到新的global_data的空间上
relocate旧的uboot代码空间到新的空间上去
修改relocate之后全局变量的label。(不懂的话参考第二节)
relocate中断向量表
程序编译时候我们会指定一个链接地址(也就是程序定位的运行地址),编译后,cpu的pc指针指向这个地址开始执行。但是如何解决uboot重定位后运行地址和链接地址不同。
uboot 对于重定位后链接地址和运行地址不一致的解决方法就是采用位置无关码,在使用 ld 进行链接的时候使用选项“-pie”生成位置无关的可执行文件。
“位置无关代码”是指无论代码加载到内存上的什么地址上,都可以被正常运行。也就是当加载地址和连接地址不一样时,CPU也可以通过相对寻址获得到正确的指令地址。
位置无关码的原理为:跳转目的函数地址存储在Label中,函数之间的地址相互是不变的,是一个确定相对关系。跳转时候使用地址无关跳转(固定的偏移量跳转)到Label中,然后Label中存储是真正的地址。在链接的过程中,会把这些Label的地址统一维护在.rel.dyn段中
如果发生uboot重定义,Label中存储真实地址都加上uboot的整体偏移量即可。
如何从uboot跳转到内核?
直接修改PC寄存器的值为Linux内核所在的地址,CPU从内核所在的地址去取指令,从而执行内核代码
为什么要传参数给内核
在此之前, uboot已经完成了硬件的初始化,可以说已经适应了“这块开发板。然而,内核并不是对于所有的开发板都能完美适配的。
此时,对于开发板的环境一无所知。所以,要想启动 Linux内核, uboot必须要给内核传递一些必要的信息来告诉内核当前所处的环境。