U-Boot(Universal Boot Loader)是一个广泛使用的开源引导加载程序,常用于嵌入式系统中。它负责在设备上电或复位后初始化硬件,并引导操作系统或应用程序的启动。U-Boot的启动流程是一个逐步的过程,包含了硬件初始化、环境变量配置、加载操作系统映像(如Linux内核),以及启动内核的多个步骤。
U-Boot 启动流程
U-Boot的启动过程可以分为以下几个主要步骤:
1. 硬件初始化
启动引导加载程序(ROM Bootloader):当嵌入式系统上电时,CPU首先会从一个预定义的存储位置(通常是片上ROM)加载初始的启动代码。这些启动代码通常非常简短,作用是加载uboot到内存中、初始化内存控制器,配置时钟等。
2.uboot初始化
和STM32一样,uboot也有链接脚本文件和启动文件,链接脚本为 arch/arm/cpu/u-boot.lds,部分内容如下:
1 OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")#指定输出可执行文件是 32 位 ARM 指令、小端模式的 ELF 格式。
2 OUTPUT_ARCH(arm) #指定输出可执行文件的平台为 ARM
3 ENTRY(_start) #代码入口点:_start,_start 在文件 arch/arm/lib/vectors.S 中有定义
4 SECTIONS
5 {
6 . = 0x00000000; #指明目标代码的起始地址从 0x0 位置开始,"."代表当前位置。
7 . = ALIGN(4); #表示此处 4 字节对齐
8 .text :
9 {
10 *(.__image_copy_start)
11 *(.vectors) #vectors 段保存中断向量表。从u-boot.map(uboot 的映射文件,可以从该文件看到某个文件或者函数链接到了哪个地址)可以看出,vectors 段的起始地址也是 0x4000000,说明整个uboot的起始地址就是 0x4000000。
根据入口_start找到文件 arch/arm/lib/vectors.S,部分内容如下
22 #ifdef CONFIG_ARCH_K3
23 ldr pc, _reset
24 #else
25 b reset
使用 grep -Nr "CONFIG_ARCH_K3"命令查找 CONFIG_ARCH_K3 变量在 uboot 中没有定义,所以当 uboot 启动后程序会跳转到第 25 行的复位异常向量的指令"b reset"处,紧接着执行 reset 代码段。标号 reset 在 arch/arm/cpu/armv7/start.S 中定义
38 reset:
39 /* Allow the board to save important registers */
40 b save_boot_params
从标号 reset 跳转到了 save_boot_params 函数,而 save_boot_params 函数同样定义在 start.S 中
119 ENTRY(save_boot_params)
120 b save_boot_params_ret @ back to my caller
save_boot_params 函数也是只有一句跳转语句,跳转到该文件中的 save_boot_params_ret 函数
75 #ifdef CONFIG_HAS_VBAR
76 /* Set vector address in CP15 VBAR register */
77 ldr r0, =_start
78 mcr p15, 0, r0, c12, c0, 0 @Set VBAR
79 #endif
82 /* the mask ROM code should have PLL and others stable */
83 #ifndef CONFIG_SKIP_LOWLEVEL_INIT
84 #ifdef CONFIG_CPU_V7A
85 bl cpu_init_cp15
86 #endif
87 #ifndef CONFIG_SKIP_LOWLEVEL_INIT_ONLY
88 bl cpu_init_crit
89 #endif
90 #endif
91
92 bl _main
这部分和STM32中的复位中断的处理函数作用类似
第 78 行设置 r0 寄存器的值为_start,_start 就是整个 uboot 的入口地址,其值为 0x4000000
第81行函数 cpu_init_cp15 用来设置 CP15(ARM 体系结构中的一个协处理器(Coprocessor),它用于控制和配置系统的一些低级操作和特性相关的内容),比如关闭 MMU
第88行函数 cpu_init_crit 也在是定义在 start.S 文件中,一般是为了允许执行函数 board_init_f 所做的基本初始化
第92行要执行的是 save_boot_params_ret 中的_main 函数,接下来分析_main函数
_main 函数定义在文件 arch/arm/lib/crt0.S 中,主要需要知道uboot 中定义了一个指向 gd_t 的指针 gd(保存了uboot的相关配置信息),gd 存放在寄存器 r9 里面的
在 _main 函 数 里 面 主 要 调 用 了 board_init_f 、 relocate_code、relocate_vectors 和 board_init_r 这 4 个函数,接下来依次来看下这 4 个函数的作 用。
board_init_f 函数定义在文件 common/board_f.c 中,此函数主要有两个工作:
1、初始化一系列外设,比如串口、定时器,或者打印一些消息等。
2、初始化 gd 的各个成员变量。
relocate_code 函数是用于代码拷贝的,此函数定义在文件 arch/arm/lib/relocate.S 中。
函数 relocate_vectors用于重定位向量表,此函数定义在文件 arch/arm/lib/relocate.S中,比如r0=gd->relocaddr,也就是重定位后 uboot 的首地址 ,向量表从这个地址开始存放。
board_init_f 函数,在此函数里面会调用一系列的函数来初始化一些外设和 gd的成员变量。但是 board_init_f并没有初始化所有的外设,还需要做一些后续工作,这 些后续工作就是由函数 board_init_r 来完成的,board_init_r 函数定义在文件 common/board_r.c 中,最后进入main_loop();main_loop 函数定义在文件 common/main.c 里面
3.uboot是否启动Linux
main_loop 函数代码如下:
41 void main_loop(void)
42 {
43 const char *s;
45 bootstage_mark_name(BOOTSTAGE_ID_MAIN_LOOP, "main_loop"); #打印出启动进度
47 if (IS_ENABLED(CONFIG_VERSION_VARIABLE))
48 env_set("ver", version_string); /* set version variable */
50 cli_init();
52 if (IS_ENABLED(CONFIG_USE_PREBOOT))
53 run_preboot_environment_command();
55 if (IS_ENABLED(CONFIG_UPDATE_TFTP))
56 update_tftp(0UL, NULL, NULL);
58 s = bootdelay_process();
59 if (cli_process_fdt(&s)) #函数cli_process_fdt读取设备树中的 bootcmd
60 cli_secure_boot_cmd(s);
62 autoboot_command(s); #autoboot_command 函数,此函数就是检查倒计时是否结束,结束就启动Linux打断则启动uboot
64 cli_loop(); #uboot 的命令行处理函数
65 panic("No CLI available");
66 }
autoboot_command 函数定义在文件 common/autoboot.c 中。
cli_loop 函数是 uboot 的命令行处理函数,我们在 uboot 中输入各种命令,进行 各种操作就是由cli_loop 来处理的,此函数定义在文件 common/cli.c 中。
4.uboot启动Linux
假设倒计时自动结束,接下来就是uboot启动Linux,目的是使处理器开始处理Linux代码部分,接着分析uboot如何启动Linux的
上述的autoboot_command 函数会调用do_boot()。这个函数负责启动内核或操作系统镜像,
我们这里是do_bootz,在文件 cmd/bootz.c 中
int do_bootz(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[])
62 {
63 int ret;
65 /* Consume 'bootz' */
66 argc--; argv++;
68 if (bootz_start(cmdtp, flag, argc, argv, &images))
69 return 1;
75 bootm_disable_interrupts();
77 images.os.os = IH_OS_LINUX;
78 ret = do_bootm_states(cmdtp, flag, argc, argv,
79 #ifdef CONFIG_SYS_BOOT_RAMDISK_HIGH
80 BOOTM_STATE_RAMDISK |
81 #endif
82 BOOTM_STATE_OS_PREP | BOOTM_STATE_OS_FAKE_GO |
83 BOOTM_STATE_OS_GO,
84 &images, 1);
86 return ret;
87 }
注:images是Linux相关配置信息的一个结构体和上述uboot的gd类似
可以看到do_bootz函数调用了三个函数,逐一分析,
第 68 行,调用 bootz_start 函数,该函数用于支持 zImage 镜像的启动,定义在文件 cmd/bootz.c 中,设置 images 的 ep 成员变量,也就是系统镜像的入口点,使用 bootz 命令启动系 统的时候就会设置系统在 DRAM 中的存储位置,调用 bootz_setup 函数,此函数会判断当前的系统镜像文件是否为 Linux 的镜像 文件,并且会打印出镜像相关信息bootz_start 主要用于初始化 images 的相关成员变量。
第 75 行,调用函数 bootm_disable_interrupts 关闭中断。
第78行函数 do_bootm_states会 根据不同的 BOOT 状态执行不同的代码段
int do_bootm_states(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[],int states, bootm_headers_t *images, int boot_progress){
616 if (states & BOOTM_STATE_START)
617 ret = bootm_start(cmdtp, flag, argc, argv);
......
683 if (!ret && (states & BOOTM_STATE_OS_PREP)) {
688 ret = boot_fn(BOOTM_STATE_OS_PREP, argc, argv, images);
689 }
690
691 #ifdef CONFIG_TRACE
692 /* Pretend to run the OS, then run a user command */
693 if (!ret && (states & BOOTM_STATE_OS_FAKE_GO)) {
694 char *cmd_list = env_get("fakegocmd");
695
696 ret = boot_selected_os(argc, argv, BOOTM_STATE_OS_FAKE_GO,images, boot_fn);
698 if (!ret && cmd_list)
699 ret = run_command_list(cmd_list, -1, flag);
700 }
701 #endif
709 /* Now run the OS! We hope this doesn't return */
710 if (!ret && (states & BOOTM_STATE_OS_GO))
711 ret = boot_selected_os(argc, argv, BOOTM_STATE_OS_GO,images, boot_fn);
......
724 return ret;
725 }
do_bootm_states 是一个状态机函数,它通过循环处理不同的启动状态。每个状态执行对应的操作,并在完成后转移到下一个状态。四个状态如下:
**BOOTM_STATE_START:**这是启动流程的开始。此时会进行一些初始化工作。然后将状态更新为 BOOTM_STATE_OS_PREP,准备操作系统。
**BOOTM_STATE_OS_PREP:**该状态负责准备操作系统的相关资源,例如加载内核映像。如果没有有效的内核映像,处理函数将会失败并退出。
**BOOTM_STATE_OS_FAKE_GO:**这个阶段是一个模拟启动阶段,通常用于调试或仿真。它将状态更新为 BOOTM_STATE_OS_GO,以继续进行实际的操作系统启动。我们这里没有定义故不需要。
**BOOTM_STATE_OS_GO:**这是实际的操作系统启动阶段,U-Boot 将会跳转到操作系统的入口点,启动内核。如果内核映像不可用或启动失败,函数会返回错误代码。
可以看到代码中最后一个执行函数为boot_selected_os,函数定义在文件common/bootm_os.c 中,该函数内调用了 do_bootm_linux函数,此函数 定义在文件 arch/arm/lib/bootm.c
418 int do_bootm_linux(int flag, int argc, char * const argv[],
419 bootm_headers_t *images)
420 {
422 if (flag & BOOTM_STATE_OS_BD_T || flag & BOOTM_STATE_OS_CMDLINE)
423 return -1;
424
425 if (flag & BOOTM_STATE_OS_PREP) {
426 boot_prep_linux(images);
427 return 0;
428 }
429
430 if (flag & (BOOTM_STATE_OS_GO | BOOTM_STATE_OS_FAKE_GO)) {
431 boot_jump_linux(images, flag);
432 return 0;
433 }
434
435 boot_prep_linux(images);
436 boot_jump_linux(images, flag);
437 return 0;
438 }
由于boot_selected_os 函数在调用 do_bootm_linux 的时候会将flag设置为 BOOTM_STATE_OS_GO。执行 boot_jump_linux 函数,该函数最后调用函数 kernel_entry,看名字"内核_进入",说明此函数是进入 Linux 内核的 。此函数有三个参数:zero,arch,params,
第一个参数 zero 为 0;
第二个参数为机器 ID;
第三个参数 ATAGS 或者设备树(DTB)首地址;