一、U-boot启动流程
boot的启动大致可分为两个阶段:
stage1:使用汇编完成CPU的初始化
stage2:通过C语言分board_init_f 和 board_init_r 两个阶段完成外设的初始化
U-Boot在启动过程中,需要将自身代码、环境变量、内核等数据从外部的RAM加载到速度更快,性能更好的DDR内部中。首先,将U-Boot的二进制代码从Flash存储器中读取出来,加载到DDR的一个预设地址(通常是RAM的低地址区域)。然后,找到链接脚本(u-boot.lds)中制定的入口点,并开始执行u-boot的初始化代码。在执行初始化代码的过程中,会对U-boot代码中的变量、函数、环境变量等进行重定位(重定位就是u-boot将自身拷贝到DDR的另一个地方去继续运行(高地址处)),将它们的地址修正成正确的地址,以便在程序执行时能够正确地访问这些变量及函数,避免与操作系统或其他程序运行时所使用的内存区域发生冲突(重定位的原因)。主要过程如下:
- reset函数将处理器设置为SVC模式,并且关闭 FIQ 和 IRQ(在u-boot启动阶段不需要中断触发什么事件,避免干扰代码,使用CPSR禁用),设置中断向量表可重定位,并设置中断向量偏移,初始化CP15;
- lowlevel_init函数设置 SP 指针(任何C程序运行都要设置堆栈指针用于存储函数参数、局部变量和返回地址)、R9 寄存器(存储全局数据指针),调用 s_init (空函数);
- _main函数主要调用 board_init_f、relocate_code、relocate_vectors 和 board_init_r 这 4 个函数;
- board_init_f 函数初始化一系列外设,比如DDR、串口、定时器,或者打印一些消息等,初始化 gd(gd是全局数据(global_data)结构的一个指针,它包含了整个引导加载器运行过程中所需的各种信息。初始化gd的各个成员变量是U-Boot启动过程的关键部分) 的各个成员变量,用于重定位 u-boot。u-boot 会将自己重定位到 DRAM 最后面的地址区域(u-boot 原先运行在 DDR 的 0x87800000 地址处),也就是将自己拷贝到 DRAM 最后面的内存区域中。 这么做的目的是给 Linux 腾出空间,防止 Linux kernel 覆盖掉 u-boot,将 DRAM 前面的区域完整的空出来。在拷贝之前肯定要给 u-boot 各部分分配好内存位置和大小,比如 gd 应该存放到哪个位置,malloc 内存池应该存放到哪个位置等等。这些信息都保存在 gd 的成员变量中,因此要对 gd 的这些成员变量做初始化。最终形成一个完整的内存“分配图”,在后面重定位 u-boot 的时候就会用到这个内存“分配图”;
relocaddr是U-Boot在内存中的重定位地址,即U-Boot代码和数据从其初始加载地址复制到的目标地址;start_addr_sp是U-Boot栈的起始地址。在计算机程序中,栈是用于存储局部变量、函数参数、返回地址等临时数据的内存区域;
- relocate_code 为代码重定位函数,此函数负责将 uboot 拷贝到新的地方去,此函数定义在文件 arch/arm/lib/relocate.S 中;
- 函数 relocate_vectors 对中断向量表做重定位,此函数定义在文件 arch/arm/lib/relocate.S 中;
- board_init_f 函数初始化board_init_f 函数没有初始化的外设,函数定义在文件common/board_r.c中;
- run_main_loop 函数主要是调用parse_stream_outer函数,接收命令行输入,解析并执行相应的命令,最终真正执行命令解析和执行相应操作的函数;完成uboot 启动以后会进入 3 秒倒计的功能;
二、bootz 启动 Linux 内核过程
当U-Boot初始化完成后,会将内核等其他操作系统或者应用程序从Flash存储器中读取出来,并根据内核的要求进行重定位,从而实现内核的正常启动工作。主要过程如下:
- do_bootz(启动流程的开始):这个阶段由函数do_bootz负责,它是启动流程的入口点。该函数主要调用bootz_start和do_bootm_states以启动内核;
- bootz_start(初始化系统镜像):bootz_start函数用于初始化一个名为images的结构体,这个结构体包含了系统镜像的相关信息。 在bootz_start内部,它首先通过调用do_bootm_states执行BOOTM_STATE_START阶段。 接下来,bootz_setup函数用于判断当前的系统镜像文件是否为Linux镜像。 最后,bootm_find_images函数负责查找ramdisk(如果使用)和设备树。如果不使用ramdisk,这个函数仅用于查找和初始化设备树相关的 成员变量。
do_bootm_states函数处理三种主要的启动状态:
BOOTM_STATE_START:bootm_start函数执行初始化操作,包括重置images结构体,为系统镜像的加载做好准备。随后,bootm_os_get_boot_func函数被调用以确定特定操作系统的启动函数,对于Linux系统而言,这个函数是do_bootm_linux,并被赋予boot_fn函数指针。此时,boot_fn(现指向do_bootm_linux)负责处理接下来的两个状态
BOOTM_STATE_OS_PREP:boot_prep_linux处理环境变量bootargs,它们携带了启动Linux内核所需的参数。最终,在BOOTM_STATE_OS_GO状态,
BOOTM_STATE_OS_GO:boot_jump_linux函数执行Linux的kernel_entry函数,标志着内核的正式启动。这一系列精细协调的步骤确保了Linux内核在嵌入式系统中的平稳启动和运行。
补充:在某些情况下,U-Boot在启动时可能不需要重定位:
- 固定内存映射:如果系统的内存映射是固定的,并且U-Boot已经被编译为直接在其最终运行地址处执行,则不需要重定位。这通常在硬件设计时就已经决定了。
- 足够的启动内存地址空间:在某些系统中,如果启动时分配的内存地址空间足够大,足以容纳U-Boot的全部功能,且不会与操作系统或其他应用程序发生冲突,则U-Boot可以直接在其初始加载地址运行,无需重定位。
- 特殊的硬件设计:在某些特殊的硬件设计中,比如使用了特定的存储器或具有特殊的内存管理机制,U-Boot可能被设计为无需重定位就能运行。
- 简化版本的U-Boot:在某些应用中,可能使用了精简版的U-Boot,这个版本的U-Boot已经针对特定的硬件和使用场景进行了优化,从而无需进行重定位。
在这些情况下,U-Boot可以避免重定位过程,直接在其加载地址上运行。这样做可以简化启动过程,减少启动时间,但这通常需要特定的硬件设计和软件配置来支持。