uboot启动流程(armv7)
uboot介绍
uboot就是一段引导程序,在加载系统内核之前,完成硬件初始化,内存映射,为后续内核的引导提供一个良好的环境。
uboot是bootloader的一种,全称为universal boot loader。
第一阶段前(Boot rom)
Boot Rom是芯片内部ROM固化程序,是uboot 的引导代码。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)
uboot相关问题
-
为什么要设置成svc模式?
- svc模式属于特权模式,可以访问所有硬件受控资源。相对于其他的模式,SVC模式可以访问的资源更多。
-
如何设置SVC模式
- 设置CPSR(当前程序状态寄存器)寄存器
-
为什么关闭中断?
- 在启动过程中,中断环境并没有完全准备好,也就是中断向量表和中断处理函数并没有完成设置,一旦有中断产生,可能会导致预想不到的问题,或者是程序跑飞。
-
如何关闭中断?
- 设置CPSR(当前程序状态寄存器)寄存器
-
CPSR寄存器
- 条件标志位
- 控制位
- 条件标志位
-
为什么关闭MMU
- MMU是用于虚拟地址向物理地址进行映射的一个结构。在 uboot阶段操作的就直接是 物理地址,所以不需要转换。
-
为什么启动ICACHE(指令),关闭DCACHE(数据)
- 启动指令CACHE课可以加快指令读取的速度,但是数据CACHE 必须 要关闭,因为它本身是一个CPU的二级缓存,在运行程序的时候可能会往里面去取数据,但是此时ram里面的数据可能并没有存入到里面,这就可能导致读取到错误的数据。
-
为什么清理.BBS段
- bss段都是未初始化的全局变量或者已经初始化为零的变量,本来就是零,直接清零就好。不清零的话未初始化的变量可能会存在未知的数值。
-
为什么要进行uboot重定位
- uboot 会将自己重定位到 DRAM(DDR) 最后面的地址区域,也就是将自己拷贝到 DRAM 最后面的内存区域中。这么做的目的是给 Linux 腾出空间,防止 Linuxkernel 覆盖掉 uboot,将 DRAM 前面的区域完整的空出来。
-
程序编译时候我们会指定一个链接地址(也就是程序定位的运行地址),编译后,cpu的pc指针指向这个地址开始执行。但是如何解决uboot重定位后运行地址和链接地址不同。
- uboot 对于重定位后链接地址和运行地址不一致的解决方法就是采用位置无关码,在使用 ld 进行链接的时候使用选项“-pie”生成位置无关的可执行文件。
- 位置无关码的原理为:跳转目的函数地址存储在Label中,函数之间的地址相互是不变的,是一个确定相对关系。跳转时候使用地址无关跳转(固定的偏移量跳转)到Label中,然后Label中存储是真正的地址。
- 如果发生uboot重定义,Label中存储真实地址都加上uboot的整体偏移量即可。
uboot 启动函数解析
uboot.lds(u-boot/arch/arm/cpu/u-boot.lds)
|---->vectors.S (arch/arm/lib/vectors.S)
|---->start.S (arch/arm/cpu/armv7/start.S)
|---|---->reset
|---|---->save_boot_params
|---|---->save_boot_params_ret
/*将处理器设置为svc模式,并关闭FIQ和IRQ :
1) sys模式和usr模式相比,所用的寄存器组,都是一样的,但是增加了一些访问一些在usr模式下不能访问的资源.
而svc模式本身就输入特权模式,本身就可以访问那些受控资源,而且比sys模式还多了写自己模式下的影子寄存器,所以,相对于sys模式来说,可以访问资源的能力相同,但是拥有更多的硬件资源.所以从理论上说,虽然可以设置为sys和svc模式的任一种,但是从uboot方面考虑,其要做的事情是初始化系统相关硬件资源,需要获取尽量多的权限,以方便操作硬件,初始化硬件.从uboot的目的是初始化硬件的角度来说,设置为svc模式,更有利于工作.
2) uboot作为一个bootloader来说,最终目的是为了启动linux的kernel,在做好准备工作(即初始化硬件,准备好kernel和rootfs等)跳转到kernel之前,
本身就要满足一些条件,其中一个条件,就是要求cpu处于svc模式的,所以,uboot在最初的初始化阶段,就将cpu设置为svc模式,也是最合适的.*/
|---|---->cpu_init_crit /* 仅调用函数lowlevel_init */
|---|---|---->lowlevel_init /* 主要完成Ram的初始化,对寄存器的存取方式进行控制 ,我们将的第一个函数*/
|---|---->main(arch/arm/lib/crt0.S)
|---|---|---->board_init_f_alloc_reserve(common/init/board_init.c)
/* 留出早期的malloc内存区域和gd_t内存区域,这个的返回值就是gd_t的结构体的指针gd,
这个指针保存在一个寄存器中,后续的传递,都是通过保存在寄存器的指针实现 */
|---|---|---->board_init_f_init_reserve(common/init/board_init.c)
/* 初始化gd,指向gd_t结构体的指针(gd_t就是global_data的结构体,成员主要是一些全局的系统初始化参数),初始化就是清零.另外,还设置了early malloc的起始地址.在gd_t结构体中,还有一个重要的结构体成员,bd_t.主要是保存开发板的相关参数.uboot启动内存kernel是要传递参数给它,这个时候就要使用到gd_t,bd_t的信息来设置标记列表.所以这2个结构体非常重要 */
|---|---|---->board_init_f(common/board_f.c) /* 我们将的第二个函数 */
/*1) 初始化DDR,串口,定时器,i2c,打印开发板cpu等信息.
2) 初始化gd的成员变量,uboot会将自己重定位到dram最后面的地址区域,也就是将自己拷贝到dram最后面的内存区域中,也就是将自己拷贝到dram最后面的内存区域中.这么做的目的是给linux腾空间,防止kernel覆盖掉uboot,将dram前面的区域完整的空出来.在拷贝之前肯定要给uboot各部分分配好内存位置和大小,比如gd应该存放到那个位置,malloc内存放到哪个位置(这些都放到gd中,指针保存到寄存器R9)。因此要对gd的成员变量初始化。在这里就将sp和gd存放到ddr,而不是内部的ram中,形成一个完整的内存图,后面重定位的时候要用到这个分配图。*/
|---|---|---|---->initcall_run_list(init_sequence_f) (lib/initcall.c)
/* 运行初始化序列init_swquence_f里面的一系列的函数,函数序列在init_sequence_f中 */
|---|---|---|---|---->init_sequence_f[] (common/board_f.c)
/* 初始化gd结构相关的参数 */
|---|---|---|---|---|---->setup_ram_buf
|---|---|---|---|---|---->setup_mon_len /* 设置gd的mon,表示uboot代码的长度 */
|---|---|---|---|---|---->initf_malloc /* 初始化gd中malloc成员,设置malloc=0x400 */
/*初始化arm架构相关的东西 */
|---|---|---|---|---|---->arch_cpu_init /* basic arch cpu dependent setup */
|---|---|---|---|---|---->initf_dm /*初始化驱动模式 */
|---|---|---|---|---|---->mark_bootstage /*定义某些标志 */
|---|---|---|---|---|---->board_early_init_f /*与板子的早期初始化匹配,imx6ull用来初始化debug串口,主要完成相关引脚的复用配置和电气属性配置 */
|---|---|---|---|---|---->get_clocks /* 获取芯片内某些外设的时钟 */
|---|---|---|---|---|---->timer->init /* 初始化cortex-a7中的定时器 */
/* 和环境变量相关,设置gd的env_addr成员 */
|---|---|---|---|---|---->env_init /* 初始化gd结构中的env相关的成员,设置gd->env_addr,环境变量的保存地址 */
|---|---|---|---|---|---->init_baud_rate /*初始化串口波特率,设置gd->baudrate */
|---|---|---|---|---|---->serial_init /*初始化debug串口,会调用串口驱动设备中注册的start函数完成初始化 */
|---|---|---|---|---|---->console_init_f /*设置console标志位=1,标明前面定义的debug串口可以使用了 */
|---|---|---|---|---|---->display_options /*第一次开始在debug串口写(显示)信息,显示uboot版本和编译时间,时区等 ,uboot启动显示在终端的第一句*/
|---|---|---|---|---|---->display_text_info /*显示一些debug调试信息,前提是定义宏DEBUG,在这里我们可以在出现问题的时候开启调试功能 */
|---|---|---|---|---|---->print_cpuinfo /* 显示cpu信息,速度 */
|---|---|---|---|---|---->show_board_info /*显示板子的信息 包含evk信息 */
|---|---|---|---|---|---->init_func_i2c /* 初始化i2c接口,实际上初始化的是spd_bus */
|---|---|---|---|---|---->announce_dram_init /* 显示 DRAM: 字符串 */
|---|---|---|---|---|---->dram_init /*获取ddr大小,实际上没有初始化dram,仅仅设置gd->ram_size */
|---|---|---|---|---|---->setup_dest_addr /* 设置目的地址,gd_ram_top,gd->relocaddr */
|---|---|---|---|---|---->reserve_uboot
|---|---|---|---|---|---->reserve_round_4k /* gd->relocaddr 内存做4K对齐 */
|---|---|---|---|---|---->reserve_mmu /* 留出mmu的TLB表位置 */
|---|---|---|---|---|---->reserve_video
|---|---|---|---|---|---->reserve_lcd
|---|---|---|---|---|---->reserve_trace /* 留出ddr调试追踪的内存位置 */
|---|---|---|---|---|---->reserve_uboot /* 留出重定位uboot占用的位置 */
|---|---|---|---|---|---->reserve_malloc /* 留出malloc的内存位置和env的内存大小 */
|---|---|---|---|---|---->reserve_board /* 留出bd所占用的内存大小(80字节) */
|---|---|---|---|---|---->setup_machine /*对于6ull 无效 */
|---|---|---|---|---|---->reserve_global_data /* 留出gd_t结构的内存大小 248字节 */
|---|---|---|---|---|---->reserve_fdt /* 留出设备树的内存大小,6ul无效 */
|---|---|---|---|---|---->reserve_arch /* */
|---|---|---|---|---|---->reserve_stacks /* 留出栈空间(16字节)并做16字节对齐 */
|---|---|---|---|---|---->setup_dram_config /* 设置dram信息,显示dram位置,运行到这里就知道预留多大的内存,同时显示内存数据,
和上面的announce_dram_init函数共同显示: DRAM : 256 MiB*/
|---|---|---|---|---|---->display_new_sp /* 显示新的sp位置,一般为 New Stack Pointer is: 0x8ef1ae90 */
|---|---|---|---|---|---->reloc_fdt /* 重定位fdt ,无效 */
|---|---|---|---|---|---->setup->reloc /* 设置gd结构体的一些其他成员,到这里内存分配就完成了 */
|---|---|---|---|---|---->copy_uboot_to_ram /* 拷贝uboot代码到内存 */
|---|---|---|---|---|---->clear_bss /* 做一些清除收尾工作 */
|---|---|---|---|---|---->jump_to_copy /* 跳转到下面的relocate->code函数 */
|---|---|---->relocate_code(arch/arm/lib/relocate.S)
/* 代码重定位,首先获取镜像的起始地址R0,然后根据计算,得到镜像的结束地址R2 */
|---|---|---|---->copy_loop
/* 完成代码拷贝工作,将uboot拷贝的ddr中 */
|---|---|---->relocate_vectors
/* 中断向量表做重定位,将中断向量表的地址地址放到寄存器里面,重定位后,首地址就是uboot的首地址 */
/* 到这里,真正的C环境就建立起来了,接下来的所有工作都是ddr中了. */
|---|---|---->board_init_r(common/board_r.c) /* 第三个函数开始 */
/* 调用一系列的函数来完成board_f. c没有完成的初始化工作 ,顺序放到init_sequencd_f[]中*/
|---|---|---|---->initcall_run_list(init_sequence_f) (lib/initcall.c)
|---|---|---|---|---->initr_trace /* 初始化buffer和跟踪相关,如果定义了config_trace宏的话,后面会执行trace_init函数,初始化与调试跟踪相关参数 */
|---|---|---|---|---->initr_reloc(common/board_r.c)/* 设置gd->flag表示relocation重定位完成 */
|---|---|---|---|---->initr_caches /* 调用initr_caches 使能芯片caches */
|---|---|---|---|---->initr_reloc_global_data /* 初始化重定位后gd的一些成员变量 */
|---|---|---|---|---->initr_malloc /* 初始化malloc内存区域 */
|---|---|---|---|---->initr_console_record /* 初始化控制台相关参数,对于6ul为空 */
|---|---|---|---|---->bootstage_relocate /* 启动状态重定位,用于重定位bootstage相关的东西 */
|---|---|---|---|---->initr_bootstage /* 初始化bootstage相关的东西 */
|---|---|---|---|---->board_init(board/freescale/mx6ul_14X14_evk/mx6ul_14x14_evk.c) ()
/* Setup chipselects,判定宏CONFIG_ARM是否定义,6ull有定义,调用board_init函数,该函数与板级相关, 定义文件为uboot/board/freescale/mx6ul_comp6ul_nand/mx6ul_comp6ul_nand.c,通过函数可以看到包含函数,setup_i2c,setup_fec,setup_usb,board_qspi_init,setup_gpmi_nand主要是用来初始化板子上的一些外设,如gpio,i2c,网络等相关接口 */
|---|---|---|---|---->set_cpu_clk_info /* 设置与始终相关的信息 */
|---|---|---|---|---->stdio_init_tables /* stdio相关的初始化 */
|---|---|---|---|---->initr_serial /* serial接口初始化 */
|---|---|---|---|---->initr_announce /* 用于调试相关的内容,通知已经在dram中运行 */
|---|---|---|---|---->initr_nand /* 初始化nand flash 并在串口以字符串型式输出nand flash大小,根据宏定义,来判断是否初始化 -> initr_nand()*/
|---|---|---|---|---->initr_mmc /* 初始化sd/mmc相关的接口,宏定义键/include/imx6_common.h */
|---|---|---|---|---->initr_env /* 初始化环境变量 */
|---|---|---|---|---->initr_secondary_cpu /* 初始化其他的cpu,6ul只有一个cpu无效 */
|---|---|---|---|---->stdio_add_devices /* 初始化各种输入输出设备,例如LCD */
|---|---|---|---|---->initr_jumptable /* 初始化跳转表相关的内容 */
|---|---|---|---|---->console_init_r /* 完成控制台的初始化,调用后,uboot输出
例如: In: serial
Out: serial
Err: serial
*/
|---|---|---|---|---->show_board_info /* 显示开发板相关信息 */
|---|---|---|---|---->interrupt_init (arch/arm/lib/interrupts.c) /* 初始化中断相关 */
|---|---|---|---|---->initr_enable_interrupts /* 使能中断 */
|---|---|---|---|---->timer_init /* 初始化定时器 */
|---|---|---|---|---->initr_status_led /*初始化运行状态灯 */
|---|---|---|---|---->initr_ethaddr /* 初始化网络,获得mac地址 */
|---|---|---|---|---->board_late_init /* 后期外设初始化,紧跟着调用initr_net 初始化开发板的网络设备,并在串口输出信息 */
|---|---|---|---|---->initr_fastboot_setup /* 初始化快速启动信息 */
|---|---|---|---|---->initr_check_fastboot /*检查快速启动配置 */
|---|---|---|---|---->lt8618_init /* 初始化8618芯片驱动 */
|---|---|---|---|---->run_main_loop(common/board_r.c) /* 执行一个死循环,循环执行main_loop */
|---|---|---|---|---|---->main_loop(common/main.c)
|---|---|---|---|---|---|---->setenv /* 设置环境变量,后面会用到 */
|---|---|---|---|---|---|---|---->version_string[] (cmd/version.c) /* 版本号,编译日期和时间,时区 */
|---|---|---|---|---|---|---|---|---->include/version.h
|---|---|---|---|---|---|---->run_preboot_environment_command(common/main.c) /* 从环境变量中获得preboot的定义,一般不用 */
|---|---|---|---|---|---|---->bootstage_mark_name /* 用于打印启动进度 */
|---|---|---|---|---|---|---->cli_init /* 命令初始化 ,初始化shell相关的变量,配置以后就可以像操作终端一样,操作uboot */
|---|---|---|---|---|---|---->bootdelay_process(common/autoboot.c)
/* 读取环境变量bootdelay 和bootcmd内容,将bootdelay赋值给全局变量stored_bootdelay,将环境变量bootcmd的值作为返回值 */
|---|---|---|---|---|---|---|---->getenv('bootcmd') (common/autoboot.c)
|---|---|---|---|---|---|---|---|---->bootcmd=CONFIG_BOOTCOMMAND
|---|---|---|---|---|---|---->autoboot_command(common/autoboot.c)
/* 倒计时按下执行,没有操作执行bootcmd的参数 */
|---|---|---|---|---|---|---|----> abortboot (common/autoboot.c line 385)
|---|---|---|---|---|---|---|---|---->abortboot_normal
/* 倒计时处理函数,没有键盘输入,倒计时返回0,开始启动内核,如果有键盘输入返回1 ,不进入内核,走cli_loop函数 */
|---|---|---|---|---|---|---|----> run_command_list(common/cli.c line 72)
/*根据abortboot返回的值,判断是否执行函数run_command_list,执行的command指令从上面的bootdelay_process函数获得,实际上就是uboot的env的值,从这里就启动内核了,如6ull-S2的env如下:
bootcmd=run nandargs;run bootnand;
nandargs=setenv bootargs console=ttymxc0,115200 root=/dev/mtdblock5 rw rootfstype=yaffs2
bootnand=nand read ${loadaddr} 0xa00000 0x800000;nand read ${fdt_addr} ${nand_addr} 0x40000;bootz ${loadaddr} - ${fdt_addr};
从这里就看到,设定串口属性,设定地址,然后使用bootz启动内核*/
|---|---|---|---|---|---|---->cli_loop(common/cli.c)
/* 倒计时按下space键,执行用户输入命令 */
|---|---|---|---|---|---|---|---->parse_file_outer
/* 如果倒计时结束前有按键输入,执行cli_loop函数 */
|---|---|---|---|---|---|---|---|---->setup_file_in_str /* 初始化input的成员变量 */
|---|---|---|---|---|---|---|---|---->parse_stream_outer /* hush shell的命令解释器 ,负责接收命令行输入,并解析执行命令 */
|---|---|---|---|---|---|---|---|---|---->parse_stream /* 解析并得到命令 */
|---|---|---|---|---|---|---|---|---|---->run_list /* 运行命令 */
|---|---|---|---|---|---|---|---|---|---|---->cmd_process /* 处理命令 */
|---|---|---|---|---|---|---|---|---|---|---|---->find_cmd /* 在命令列表中找到指定的指令(从u_boot_list里面查找,找到后,返回对应的结构体变量,cmd_tbl_t) */
|---|---|---|---|---|---|---|---|---|---|---|---->cmd_call
/* 执行具体的指令,通过直接引用命令的结构体变量cmd_tbl_t中的成员变量cmd,
cmd的成员变量就是命令出来函数do_XXX,
在命令行中,通过bootz命令启动内核 */
/* 内核其中后,控制权移交给内核 */