uboot介绍
uboot就是一段引导程序,在加载系统内核之前,完成硬件初始化,内存映射,为后续内核的引导提供一个良好的环境。uboot是bootloader的一种,全称为universal boot loader。
一、uboot的makefile
1.1 makefile整体解析过程
为了生成u-boot.bin这个文件,首先要生成构成u-boot.bin的各个库文件、目标文件。为了各个库文件、目标文件就必须进入各个子目录执行其中的Makefile。由此,确定了整个编译的命令和顺序。
1.2 makefile整体编译过程
- 首先,根据各个库文件、目标文件出现的先后顺序,依次进入各个子目录编译从而生成这些目标
- 然后,回到顶层目录,继续执行顶层Makefile的总目标,最后生成u-boot.bin。
uboot的编译分为两步:配置、编译。
(1)第一步:配置,执行make pangu_basic_defconfig进行配置,生成.config文件
(2)第二步:编译,执行make进行编译,生成u-boot.bin。
二、uboot启动流程
- uboot分为 uboot-spl 和 uboot 两个组成部分。
uboot启动分三个阶段
-
BL0
ROM上的固化程序(Boot Rom) -
BL1(u-boot-spl)
- 初始化部分时钟(和SDRAM相关)
- 初始化DDR(外部SDRAM)
- 从存储介质上(比如SD\eMMC\nand flash)将BL2镜像加载到SDRAM上
- 验证BL2镜像的合法性
- 跳转到BL2镜像所在的地址上
- BL2 (uboot)
- 初始化部分硬件,包括时钟、内存等等
- 加载内核到内存上
- 加载文件系统、atags或者dtb到内存上
- 根据操作系统启动要求正确配置好一些硬件
启动操作系统
2.1 uboot的链接文件(u-boot.lds)
链接文件的作用
-
指定代码段和数据段、只读数据段在内存中的存放地址;(地址具体为i.m6ull , 其他芯片可能不是 0X87800000)
-
u-boot.map 是 uboot 的映射文件,看到某个文件或者函数链接到了哪个地址,
-
__image_copy_start 为 0X87800000,而.text 的起始地址也是0X87800000。
-
vectors 段保存中断向量表,vectors 段的起始地址也是 0X87800000,说明整个 uboot 的起始地址就是 0X87800000,
-
这也是为什么我们裸机例程的链接起始地址选择 0X87800000 了,目的就是为了和 uboot 一致。
-
-
指定代码的入口地址;
- 连接文件中找到程序的入口点:_start, 其中_start 在文件 arch/arm/lib/vectors.S 。
2.2 uboot启动流程
第一阶段(uboot-spl , 入口是上述lds文件中分析的_start)
- SPL是Secondary Program Loader的简称,第二阶段程序加载器,这里所谓的第二阶段是相对于SOC中的Boot ROM来说的
Boot ROM会通过检测启动方式来加载第二阶段bootloader。uboot已经是一个bootloader了,那么为什么还多一个uboot spl呢?
-
这个主要原因是对于一些SOC来说,它的内部SRAM可能会比较小,小到无法装载下一个完整的uboot镜像,那么就需要spl,它主要负责初始化外部RAM和环境,并加载真正的uboot镜像到外部RAM(DDR)中来执行。
-
所以由此来看,SPL应该是一个非常小的loader程序,可以运行于SOC的内部SRAM中,它的主要功能就是加载真正的uboot并运行之。
2.2.1. 进入_start函数:
_start:
#ifdef CONFIG_SYS_DV_NOR_BOOT_CFG
.word CONFIG_SYS_DV_NOR_BOOT_CFG
#endif
b reset
ldr pc, _undefined_instruction
ldr pc, _software_interrupt
ldr pc, _prefetch_abort
ldr pc, _data_abort
ldr pc, _not_used
ldr pc, _irq
ldr pc, _fiq
有一条跳转指令b reset跳转到reset函数处去执行
注意,spl的流程在reset中就应该被结束,也就是说在reset中,就应该转到到BL2,也就是uboot中了。
2.2.2. 进入reset函数
reset:
/* Allow the board to save important registers */
b save_boot_params @进入reset第一步跳转到save_boot_params
save_boot_params_ret: @ save_boot_params 内部通过cpsr 设置cpu为SVC模式,关闭FIQ和IRQ中断
/*
* disable interrupts (FIQ and IRQ), also set the cpu to SVC32 mode,
* except if in HYP mode already
*/
mrs r0, cpsr
and r1, r0, #0x1f @ mask mode bits
teq r1, #0x1a @ test for HYP mode
bicne r0, r0, #0x1f @ clear all mode bits
orrne r0, r0, #0x13 @ set SVC mode
orr r0, r0, #0xc0 @ disable FIQ and IRQ
msr cpsr,r0
/* the mask ROM code should have PLL and others stable */
#ifndef CONFIG_SKIP_LOWLEVEL_INIT
bl cpu_init_cp15 @ 跳转到cpu_init_cp15 ,初始化协处理器CP15,从而禁用MMU和TLB。
bl cpu_init_crit @ 跳转到cpu_init_crit ,进行一些关键的初始化动作,也就是平台级和板级的初始化
#endif
bl _main @ 跳转到_main ,加载BL2以及跳转到BL2的主体部分
进入reset函数,首先设置cpu为SVC模式,关闭中断。然后跳转到cpu_init_cp15 ,初始化协处理器CP15,从而禁用MMU和TLB。跳转到cpu_init_crit ,进行一些关键的初始化动作,也就是平台级和板级的初始化。最后跳转到**_main**,加载BL2以及跳转到BL2的主体部分
- 关闭中断。
uboot引导linux起到的过程中本身就是一个完成的过程,不需要中断机制。
2.2.3 cpu_init_cp15函数
ENTRY(cpu_init_cp15)
/*
* Invalidate L1 I/D
*/
mov r0, #0 @ set up for MCR
mcr p15, 0, r0, c8, c7, 0 @ invalidate TLBs
mcr p15, 0, r0, c7, c5, 0 @ invalidate icache
mcr p15, 0, r0, c7, c5, 6 @ invalidate BP array
mcr p15, 0, r0, c7, c10, 4 @ DSB
mcr p15, 0, r0, c7, c5, 4 @ ISB
@@ 这里只需要知道是对CP15处理器的部分寄存器清零即可。
@@ 将协处理器的c7\c8清零等等,各个寄存器的含义请参考《ARM的CP15协处理器的寄存器》
/*
* disable MMU stuff and caches
*/
mrc p15, 0, r0, c1, c0, 0
bic r0, r0, #0x00002000 @ clear bits 13 (--V-)
bic r0, r0, #0x00000007 @ clear bits 2:0 (-CAM)
orr r0, r0, #0x00000002 @ set bit 1 (--A-) Align
orr r0, r0, #0x00000800 @ set bit 11 (Z---) BTB
#ifdef CONFIG_SYS_ICACHE_OFF
bic r0, r0, #0x00001000 @ clear bit 12 (I) I-cache
#else
orr r0, r0, #0x00001000 @ set bit 12 (I) I-cache
#endif
mcr p15, 0, r0, c1, c0, 0
@@ 通过上述的文章的介绍,我们可以知道cp15的c1寄存器就是MMU控制器
@@ 上述对MMU的一些位进行清零和置位,达到关闭MMU和cache的目的,具体的话去看一下上述文章吧。
ENDPROC(cpu_init_cp15)
cpu_init_cp15 用来设置 CP15 相关的内容,完成启动ICACHE,关闭DCACHE,关闭MMU和TLB 。
- 关闭MMU
,MMU是用于虚拟地址向物理地址进行映射的一个结构。在 uboot阶段操作的就直接是 物理地址,所以不需要转换。 - 启动ICACHE(指令),关闭DCACHE(数据)
启动指令CACHE课可以加快指令读取的速度,但是数据CACHE 必须 要关闭,因为它本身是一个CPU的二级缓存,在运行程序的时候可能会往里面去取数据,但是此时ram里面的数据可能并没有存入到里面,这就可能导致读取到错误的数据。
2.2.4 cpu_init_crit函数(包含lowlevel_init函数)
ENTRY(cpu_init_crit)
/*
* Jump to board specific initialization...
* The Mask ROM will have already initialized
* basic memory. Go here to bump up clock rate and handle
* wake up conditions.
*/
b lowlevel_init @ go setup pll,mux,memory
ENDPROC(cpu_init_crit)
cpu_init_crit函数的内容是跳转到lowlevel_init函数。
2.2.5. lowlevel_init 函数(平台级和板级的初始化)
lowlevel_init主要完成平台级和板级的初始化
在lowlevel_init中,我们要实现如下:
- 检查一些复位状态
- 关闭看门狗
- 系统时钟的初始化
- 内存、DDR的初始化
- 串口初始化(可选)
- Nand flash的初始化
在im6ull中
此时初始化SP指向 内存空间为IRAM(内部ram ,OCRAM 128K ,0x00900000),(初始化内存空间,为第二阶段准备ram)
2.2.6. _main函数
** _main函数的主要工作是 **
- 设置c语言的运行环境
- 设置sp和gd的中间环境
- 重定义代码
- 设置最终的环境
ENTRY(_main)
/*
* Set up initial C runtime environment and call board_init_f(0).
*/
@ 为c语言环境准备
@ uboot-spl和uboot代码共用,CONFIG_SPL_BUILD来区分是谁调用
#if defined(CONFIG_SPL_BUILD) && defined(CONFIG_SPL_STACK)
ldr sp, =(CONFIG_SPL_STACK) @ 在spl中为 c语言环境设置栈
#else
ldr sp, =(CONFIG_SYS_INIT_SP_ADDR)
#endif
#if defined(CONFIG_CPU_V7M) /* v7M forbids using SP as BIC destination */
mov r3, sp
bic r3, r3, #7
mov sp, r3
#else
bic sp, sp, #7 /* 8-byte alignment for ABI compliance */
#endif
mov r0, sp
bl board_init_f_alloc_reserve @ 把栈前面的空间分配给GD
mov sp, r0 @重新设置指针SP
/* set up gd here, outside any C code */
mov r9, r0 @ 保存GD地址到r9寄存器
bl board_init_f_init_reserve @ 初始化GD空间
mov r0, #0
bl board_init_f @ 跳转到板级前期初始化函数,
#if ! 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.
*/
@ 设置sp和gd的中间环境
ldr sp, [r9, #GD_START_ADDR_SP] /* sp = gd->start_addr_sp */
#if defined(CONFIG_CPU_V7M) /* v7M forbids using SP as BIC destination */
mov r3, sp
bic r3, r3, #7
mov sp, r3
#else
bic sp, sp, #7 /* 8-byte alignment for ABI compliance */
#endif
ldr r9, [r9, #GD_BD] /* r9 = gd->bd */
sub r9, r9, #GD_SIZE /* new GD is below bd */
adr lr, here
ldr r0, [r9, #GD_RELOC_OFF] /* r0 = gd->reloc_off */
add lr, lr, r0
#if defined(CONFIG_CPU_V7M)
orr lr, #1 /* As required by Thumb-only */
#endif
ldr r0, [r9, #GD_RELOCADDR] /* r0 = gd->relocaddr */
b relocate_code @ 重定位代码
here:
/*
* now relocate vectors
*/
bl relocate_vectors @重定位向量表
/* Set up final (full) environment */
@设置最终的环境
bl c_runtime_cpu_setup /* we still call old routine here */
#endif
#if !defined(CONFIG_SPL_BUILD) || defined(CONFIG_SPL_FRAMEWORK)
# ifdef CONFIG_SPL_BUILD
/* Use a DRAM stack for the rest of SPL, if requested */
bl spl_relocate_stack_gd
cmp r0, #0
movne sp, r0
movne r9, r0
# endif
ldr r0, =__bss_start /* this is auto-relocated! */
#ifdef CONFIG_USE_ARCH_MEMSET
ldr r3, =__bss_end /* this is auto-relocated! */
mov r1, #0x00000000 /* prepare zero to clear BSS */
subs r2, r3, r0 /* r2 = memset len */
bl memset
#else
ldr r1, =__bss_end /* this is auto-relocated! */
mov r2, #0x00000000 /* prepare zero to clear BSS */
clbss_l:cmp r0, r1 /* while not at end of BSS */
#if defined(CONFIG_CPU_V7M)
itt lo
#endif
strlo r2, [r0] /* clear 32-bit BSS word */
addlo r0, r0, #4 /* move to next */
blo clbss_l
#endif
#if ! defined(CONFIG_SPL_BUILD)
bl coloured_LED_init
bl red_led_on
#endif
/* call board_init_r(gd_t *id, ulong dest_addr) */
mov r0, r9 /* gd_t */
ldr r1, [r9, #GD_RELOCADDR] /* dest_addr */
/* call board_init_r */
@ 跳转board_init_r 设置最终环境
#if defined(CONFIG_SYS_THUMB_BUILD)
ldr lr, =board_init_r /* this is auto-relocated! */
bx lr
#else
ldr pc, =board_init_r /* this is auto-relocated! */
#endif
/* we should not return here. */
#endif
ENDPROC(_main)
-
因为后面是C语言环境,首先是设置堆栈
-
初始化gd(下图中global date,内部ram) , 进行清零(同上内部ram)
-
调用 board_init_f 函数(将SP指针从内部IRAM,转移到外部DDR),主要用来初始化 DDR,定时器,完成代码拷贝等等
-
调用函数 relocate_code,也就是代码重定位函数,此函数负责将 uboot 拷贝到新的地方去
-
调用函数 relocate_vectors,对中断向量表做重定位
-
清除 BSS 段 , 。
bss段不占用空间,都是未初始化的全局变量或者已经初始化为零的变量,本来就是零,直接清零就好。不清零的话未初始化的变量可能会存在未知的数值。 -
设置函数 board_init_r 的两个参数 , 调用 board_init_r 函数 ,
-
board_init_r 函数打印一些列的信息到串口,然后会进入main_loop() 。main_loop会进行倒计时,如果此时按下回车就会进入uboot的shell交互界面,否则就会自动引导启动OS系统。
2.2.7. board_init_f 函数
- 初始化一系列外设,比如串口、定时器,或者打印一些消息等。
- 重新设置环境(sp 和 gd) , 新的 sp 和 gd 将会存放到 DDR 中(外部),而不是内部的 RAM 了
- uboot 会将自己重定位到 DRAM(DDR) 最后面的地址区域,也就是将自己拷贝到 DRAM 最后面的内存区域中。这么做的目的是给 Linux 腾出空间,防止 Linuxkernel 覆盖掉 uboot,将 DRAM 前面的区域完整的空出来。
- 在拷贝之前肯定要给 uboot 各部分分配好内存位置和大小,比这些信息都保存在 gd 的成员变量中(从板子配置文件里读取),因此要对 gd 的这些成员变量做初始化。最终形成一个完整的内存“分配图”,在后面重定位 uboot 的时候就会用到这个内存“分配图”(外部)。
- 注:上电后芯片内部Boot ROM把uboot搬移到DRAM头部(0x87800000),重定位则再搬移到DDR后部
2.2.8. relocate_code 函数
- relocate_code 函数是用于代码拷贝
重定位就是 uboot 将自身拷贝到 DRAM 的另一个地放去继续运行(DRAM 的高地址处)。我们知道,一个可执行的 bin 文件,其链接地址和运行地址要相等,也就是链接到哪个地址,在运行之前就要拷贝到哪个地址去。现在我们重定位以后,运行地址就和链接地址不同了,这样寻址的时候不会出问题吗?
-
分析问题产生原因
r0 = gd->relocaddr = 0x9ff47000 , uboot重定位后的首地址
r1 = 0x87800000 源地址的首地址
r2 = 0x8785dc6c 源地址的结束地址
r4 = 0x9ff46000 - 0x87800000 = 0x18747000 偏移量
拷贝是从r1复制往r0粘贴 , 一次两个32位
当r1等于r2,拷贝完成当简单粗暴的将uboot从0x87800000拷贝到0x9ff47000 ,程序运行时地址和连接地址不同,发生错误。uboot解决方法是使用位置无关码,借用 .rel.dyn 段
-
使用位置无关码解重定位后和连接地址不同问题的原理
举例: board_init 函数会调用 rel_test,rel_test 会调用全局变量 rel_a
源代码static int rel_a = 0; void rel_test(void) { rel_a = 100; printf("rel_test\r\n"); } int board_init(void) { ... rel_test(); ... }
反汇编代码
8785dcf8 <rel_a>: 8785dcf8: 00000000 andeq r0, r0, r0 878042b4 <rel_test>: 878042b4: e59f300c ldr r3, [pc, #12] ; 878042c8 <rel_test+0x14> 878042b8: e3a02064 mov r2, #100 ; 0x64 878042bc: e59f0008 ldr r0, [pc, #8] ; 878042cc <rel_test+0x18> 878042c0: e5832000 str r2, [r3] 878042c4: ea00d64c b 87839bfc <printf> 878042c8: 8785dcf8 ; <UNDEFINED> instruction: 0x8785dcf8 878042cc: 87842aaf strhi r2, [r4, pc, lsr #21]
从反汇编代码中分析:
- 想要找到 rel_a 的地址,首先 r3 = pc + 12 = 0x878042b4 + 8 + 12 = 0x878042c8 。(由ARM 流水线决定 pc = 当前地址 + 8)
- 之后 r3 在0x878042c8 中存储数据为0x8785dcf8,即为rel_a地址
- 这里没直接读取rel_a , 而是借助0x878042c8 。 0x878042c8 就是Label
重定位后,地址变化
9ffa4cf8 <rel_a>: 9ffa4cf8: 00000000 andeq r0, r0, r0 9ff4b2b4<rel_test>: 9ff4b2b4: e59f300c ldr r3, [pc, #12] ; 878042c8 <rel_test+0x14> 9ff4b2b8: e3a02064 mov r2, #100 ; 0x64 9ff4b2bc: e59f0008 ldr r0, [pc, #8] ; 878042cc <rel_test+0x18> 9ff4b2c0: e5832000 str r2, [r3] 9ff4b2c4: ea00d64c b 87839bfc <printf> 9ff4b2c8: 8785dcf8 ; <UNDEFINED> instruction: 0x8785dcf8 9ff4b2cc: 87842aaf strhi r2, [r4, pc, lsr #21]
- 这时 Label中的值还是重定位之前的,必须要将8785dcf8换为重定位后的rel_a地址
- 重定位后的Label中的数据 = 0x878042c8(老的Label) + 0x18747000(uboot整体的偏移量) = 0x9ffa4cf8;
- 读取 rel_a 地址为 ,r3 = 0x9ff4b2b4 + 8 + 12 = 0x9ff4b2c8 ,即为Label地址,然后从Label中读取到 0x9ffa4cf8,为rel_a重定义后真是地址
-
uboot 中使用 .rel.dyn 段具体实现位置无关码的原理
完成这个功能在连接的时候需要加上”-pie”
.rel.dyn 段代码段
8785dcec: 87800020 strhi r0, [r0, r0, lsr #32] 8785dcf0: 00000017 andeq r0, r0, r7, lsl r0 …… 8785e2fc: 878042c8 strhi r4, [r0, r8, asr #5] 8785e300: 00000017 andeq r0, r0, r7, lsl r0
- .rel.dyn 段的格式,也就是两个 4 字节数据为一组。
高 4 字节是 Label 地址标识 0X17,低 4 字节就是 Label 的地址, - 第三行是878042c8,第四行是00000017 。说明878042c8是一个Label。正是上述分析中 存放 rel_a 地址的Label
- 在重定位后,rel_a 真是地址 = Label内数据 + offset(0x18747000)
- .rel.dyn 段的格式,也就是两个 4 字节数据为一组。
2.2.9 relocate_vectors函数
- 中断向量表重定位后的地址,就是重定位后uboot的首地址
2.2.10. board_init_r 函数(板级初始化,进入第二阶段)
board_init_f 并没有初始化所有的外设,还需要做一些后续工作,这些后续工作就是由函数 board_init_r 来完成的
uboot relocate后的板级初始化 ,最后执行run_main_loop
第二阶段
2.2.11 run_main_loop函数
- run_main_loop-> main_loop->
- -> bootdelay_process 获取bootdelay的值,然后保存到stored_bootdelay
全局变量里面,获取bootcmd环境变量值,并且将其
返回 - -> autoboot_command 参数是bootcmd的值。检查倒计时内有无打断
- -> abortboot 参数为boot delay,此函数会处理倒计时
- -> abortboot_normal 参数为boot delay,此函数会处理倒计时
- ->run_command_list函数,参数为s(bootcmd的命令)倒计时结束时hush shell没输入执行,启动内核
- -> abortboot 参数为boot delay,此函数会处理倒计时
- -> cli_loop 是uboot命令模式处理函数。
- -> parse_file_outer
- -> parse_stream_outer
- -> parse_stream 解析输入的字符,得到命令
- -> run_list 运行命令
- -> run_list_real
- -> run_pipe_real
- -> cmd_process 处理命令,也就是执行命令
- -> run_pipe_real
- -> run_list_real
- -> parse_stream_outer
- -> parse_file_outer
- -> bootdelay_process 获取bootdelay的值,然后保存到stored_bootdelay
static int run_main_loop(void)
{
/* main_loop() can return to retry autoboot, if so just run it again */
for (;;)
main_loop();
// 这里进入了主循环,而autoboot也是在主循环里面实现
return 0;
}
进入了main_loop()函数。
2.2.11 main_loop()函数
void main_loop(void)
{
const char *s;
bootstage_mark_name(BOOTSTAGE_ID_MAIN_LOOP, "main_loop");
// 这里用于标记uboot的进度,对于tiny210来说起始什么都没做
cli_init();
// cli的初始化,主要是hush模式下的初始化
run_preboot_environment_command();
// preboot相关的东西,后续有用到再说明
s = bootdelay_process();
if (cli_process_fdt(&s))
cli_secure_boot_cmd(s);
autoboot_command(s);
// autoboot的东西,后续使用autoboot的时候再专门说明
cli_loop();
// 进入cli的循环模式,也就是命令行模式
panic("No CLI available");
}
- 调用 bootstage_mark_name 函数,打印出启动进度。
- 调用 cli_init 函数,跟命令初始化有关,初始化 hush shell 相关的变量。( hush shell 为uboot倒计时结束的输入shell)
- run_preboot_environment_command 函数,获取环境变量 perboot 的内容,perboot
是一些预启动命令 - bootdelay_process 函数,此函数会读取环境变量 bootdelay (延迟时间)和 bootcmd(启动内核命令) 的内容,
然后将 bootdelay 的值赋值给全局变量 stored_bootdelay,返回值为环境变量 bootcmd 的值。 - autoboot_command(s)函数,此函数就是检查倒计时是否结束?倒计时结束之前有
没有被打断?输入参数s为bootcmd的命令- 倒计时自然结束执行函数run_command_list,此函数会执行参数 s 指定的一系列命令,也就是环境变量 bootcmd 的命令,bootcmd 里面保存着默认的启动命令,因此 linux 内核启动
- 倒计时结束之前按下了键盘上的按键,那么 run_command_list函数就不会执行,执行后面的cli_loop函数
- cli_loop函数,是 uboot 的命令行处理函数
2.2.12 cli_loop函数
cli_loop 函数是 uboot 的命令行处理函数,我们在 uboot 中输入各种命令,进行各种操作就是有 cli_loop 来处理的
void cli_loop(void)
{
#ifdef CONFIG_SYS_HUSH_PARSER
parse_file_outer();
/* This point is never reached */
for (;;);
#else
cli_simple_loop(); //永远不会执行
#endif /*CONFIG_SYS_HUSH_PARSER*/
}
- 调用 parse_file_outer函数
2.2.13 parse_file_outer函数
int parse_file_outer(void)
{
int rcode;
struct in_str input;
setup_file_in_str(&input);
rcode = parse_stream_outer(&input, FLAG_PARSE_SEMICOLON);
return rcode;
}
- 调用函数 setup_file_in_str 初始化变量 input 的成员变量
- 调用函数 parse_stream_outer,这个函数就是 hush shell 的命令解释器,负责接收命令行输入,然后解析并执行相应的命令
2.3.14 函数 parse_stream_outer
static int parse_stream_outer(struct in_str *inp, int flag)
{
struct p_context ctx;
o_string temp=NULL_O_STRING;
int rcode;
int code = 1;
do {
......
rcode = parse_stream(&temp, &ctx, inp,
flag & FLAG_CONT_ON_NEWLINE ? -1 : '\n');
......
if (rcode != 1 && ctx.old_flag == 0) {
......
run_list(ctx.list_head);
......
} else {
......
}
b_free(&temp);
/* loop on syntax errors, return on EOF */
} while (rcode != -1 && !(flag & FLAG_EXIT_FROM_LOOP) &&
(inp->peek != static_peek || b_peek(inp)));
return 0;
}
- do-while 循环就是处理输入命令的
- 函数 parse_stream 进行命令解析
- 调用 run_list 函数来执行解析出来的命令,run_list 调用 run_list_real 函数,run_list_real 函数调用 run_pipe_real 函数,run_pipe_real 函数调用 cmd_process 函数。最终通过函数 cmd_process 来处理命令
2.3.15 补充:uboot的命令
uboot把所有命令的数据结构都放在一个表格中,我们后续称之为命令表。表中的每一项代表着一个命令,其项的类型是cmd_tbl_t。
struct cmd_tbl_s {
char *name; /* Command Name */
int maxargs; /* maximum number of arguments */
int repeatable; /* autorepeat allowed? */
/* Implementation function */
int (*cmd)(struct cmd_tbl_s *, int, int, char * const []);
char *usage; /* Usage message (short) */
#ifdef CONFIG_SYS_LONGHELP
char *help; /* Help message (long) */
#endif
};
typedef struct cmd_tbl_s cmd_tbl_t;
参数说明如下:
name:定义一个命令的名字。 其实就是执行的命令的字符串。这个要注意。
maxargs:这个命令支持的最大参数
repeatable:是否需要重复
cmd:命令处理函数的地址
usage:字符串,使用说明
help:字符串,帮助
Uboot使用U_BOOT_CMD来定义一个命令。CONFIG_CMD_XXX来使能uboot中的某个命令。
//uboot命令定义代码
#define U_BOOT_CMD(_name, _maxargs, _rep, _cmd, _usage, _help)
U_BOOT_CMD最终是定义了一个cmd_tbl_t类型的结构体变量,所有的命令最终都是存放在.u_boot_list段里面。cmd_tbl_t结构体中的cmd成员变量就是具体的命令执行函数,命令执行函数都是do_xxx。
// bootm就是我们的命令字符串
// 在tiny210.h中定义了最大参数数量是64
// #define CONFIG_SYS_MAXARGS 64 /* max number of command args */
// 1表示重复一次
// 对应命令处理函数是do_bootm
// usage字符串是"boot application image from memory"
// help字符串是bootm_help_text定义的字符串。
U_BOOT_CMD(
bootm, CONFIG_SYS_MAXARGS, 1, do_bootm,
"boot application image from memory", bootm_help_text
);
//并且命令处理函数的格式如下:
//当命令处理函数执行成功时,需要返回0.返回非0值
int do_bootm(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[])
2.3.16 函数 cmd_process
函数 cmd_process 来处理命令并执行
- cmd_process
- ->find_cmd 从.u_boot_list段里面查找命令,当找到对应的命令以后以返回值的
形式给出,为cmd_tbl_t类型 - ->cmd_call
->cmdtp->cmd 直接引用cmd成员变量,执行do_xxx函数
- ->find_cmd 从.u_boot_list段里面查找命令,当找到对应的命令以后以返回值的
函数 cmd_process代码
enum command_ret_t cmd_process(int flag, int argc, char * const argv[],
int *repeatable, ulong *ticks)
{
enum command_ret_t rc = CMD_RET_SUCCESS;
cmd_tbl_t *cmdtp;
/* Look up command in command table */
cmdtp = find_cmd(argv[0]);
// 第一个参数argv[0]表示命令,调用find_cmd获取命令对应的表项cmd_tbl_t。
if (cmdtp == NULL) {
printf("Unknown command '%s' - try 'help'\n", argv[0]);
return 1;
}
/* found - check max args */
if (argc > cmdtp->maxargs)
rc = CMD_RET_USAGE;
// 检测参数是否正常
/* If OK so far, then do the command */
if (!rc) {
if (ticks)
*ticks = get_timer(0);
rc = cmd_call(cmdtp, flag, argc, argv);
// 调用cmd_call执行命令表项中的命令,成功的话需要返回0值
if (ticks)
*ticks = get_timer(*ticks);
// 判断命令执行的时间
*repeatable &= cmdtp->repeatable;
// 这个命令执行的重复次数存放在repeatable中的
}
if (rc == CMD_RET_USAGE)
rc = cmd_usage(cmdtp);
// 命令格式有问题,打印帮助信息
return rc;
}
- find_cmd函数, 从.u_boot_list段获取命令对应的命令表项cmd_tbl_t 。
- cmd_call函数,执行命令表项cmd_tbl_t 中的命令,执行do_xxx函数
2.3.17 uboot启动linux内核(bootz命令)
在run_main_loop函数倒计时结束时没有输入,uboot执行bootcmd命令,启动内核
-
bootcmd 保存着 uboot 默认命令,这些命令一般都是用来启动 Linux 内核的,读取Linux镜像文件和设备树文件(从MMC、SD卡、网络读取等)到DARM(DDR)中,然后启动内核(bootz、bootm)。
-
bootm和bootz的不同地方
- bootm用于加载uImage和ramdisk
bootm ${kernel_load_address} ${ramdisk_load_address} ${devicetree_load_address}
- bootz用于加载zImage和ext4文件系统
bootz ${kernel_load_address} ${ramdisk_load_address} ${devicetree_load_address}
- bootm用于加载uImage和ramdisk
-
bootz的使用(tftp从网络读取)
tftp 80800000 zImage tftp 83000000 imx6ull-14x14-emmc-7-1024x600-c.dtb bootz 80800000 - 83000000
bootz流程
2.3.18 do_bootz函数
do_bootz是bootz的执行函数
do_bootz
- -> bootz_start
- -> do_bootm_states 阶段为BOOTM_STATE_START
- -> bootm_start 对images全局变量清零
- -> images->ep = 0X80800000
- ->bootz_setup 判断zImage是否正确
- -> bootm_find_images 查找镜像文件
- -> boot_get_fdt 找到设备树,然后将设备树起始地址和长度,写入到images的ft_addr和ft_len成员变量中。
- -> bootm_disable_interrupts 关闭中断相关
- -> images.os.os = IH_OS_LINUX; 表示要启动Linux系统
- -> do_bootm_states 状态BOOTM_STATE_OS_PREP 、BOOTM_STATE_OS_FAKE_GO 、
BOOTM_STATE_OS_GO,- -> bootm_os_get_boot_func 查找Linux内核启动函数。找到Linux内核启动函数
do_bootm_linux,赋值给boot_fn。 - -> boot_fn(BOOTM_STATE_OS_PREP, argc, argv, images); 就是do_bootm_linux。
- -> boot_prep_linux 启动之前的一些工作,对于使用设备树来说,他会将Bootargs传递给Linux内核,通过设备树完成。也就是向Linux内核传参。
- -> boot_jump_linux
- -> machid= gd->bd->bi_arch_number;
- -> kernel_entry = (void (*)(int, int, uint))images->ep; 0X80800000。
- -> announce_and_cleanup 输出Starting kernel……
- -> kernel_entry(0, machid, r2); 启动Linux内核。Uboot的最终使命,启动Linux内核。
- -> bootm_os_get_boot_func 查找Linux内核启动函数。找到Linux内核启动函数
- -> do_bootm_states 阶段为BOOTM_STATE_START
2.3.19 kernel_entry函数
kernel_entry函数是内核的入口函数。内核镜像第一行代码
kernel_entry(0, machid, r2);
此函数有三个参数:zero,arch,params,
- 第一个参数 zero 同样为 0;
- 第二个参数为机器 ID;
- 第三个参数启动参数标记列表(ATAGS)或者设备树(DTB)首地址,ATAGS 是传统的方法,用于传递一些命令行信息啥的,如果使用设备树的话就要传递设备树(DTB)。
- 是汇编函数,因为内核开始是汇编代码。
- images->ep 保存着 Linux内核镜像的起始地址,也就是kernel_entry的地址
kernel_entry函数的传参
- 向汇编函数传递参数要使用 r0、r1 和 r2寄存器
- r0 = 0
- r1 = 机器类型ID(machid)
- Linux 内核会在自己的机器 ID 列表里面查找是否存在与 uboot 传递进来的 machid 匹配的项目,如果存在就说明 Linux 内核支持这个机器,那么 Linux 就会启动!
- 如果使用设备树的话这个 machid 就无效了,设备树存有一个“兼容性”这个属性,Linux 内核会比较“兼容性”属性的值(字符串)来查看是否支持这个机器。
- r2 =
- 如果使用设备树的话,r2 应该是设备树的起始地址
- 如果不使用设备树的话,r2 应该是 uboot 传递给 Linux 的参数起始地址,也就是环境变量 bootargs 的值
如何从uboot跳转到内核
直接修改PC寄存器的值为Linux内核所在的地址,CPU从内核所在的地址去取指令,从而执行内核代码
为什么要传参数给内核
在此之前, uboot已经完成了硬件的初始化,可以说已经噎适应了“这块开发板。然而,内核并不是对于所有的开发板都能完美适配的(如果适配了,可想而知这个内核有多庞大,又或者有新技术发明了,可以完美的适配各种开发板),此时,对于开发板的环境一无所知。所以,要想启动 Linux内核, uboot必须要给内核传递一些必要的信息来告诉内核当前所处的环境。