BootLoader(引导加载器)是启动嵌入式系统时执行的第一个程序,位于固件中。它主要负责初始化系统硬件、加载操作系统,并将控制权转移到操作系统的启动过程。
关于Linux
引导加载程序的引入
嵌入式Linux系统从软件角度可以分为四个主要部分:引导加载程序(Bootloader)、Linux内核、文件系统和应用程序。这些部分共同构成了完整的嵌入式系统。
引导加载程序(Bootloader):引导加载程序是系统启动的第一个执行代码,负责系统的引导和初始化。它可以包括固化在固件中的boot代码和Bootloader程序。引导加载程序的主要任务是初始化硬件、加载Linux内核、配置系统参数,并将控制权传递给内核。
Linux内核:Linux内核是嵌入式系统的核心,负责管理系统的硬件资源、提供各种设备驱动程序和执行系统的核心功能。它提供了内存管理、进程调度、设备管理等基本功能,并提供了丰富的系统调用接口供应用程序使用。
文件系统:嵌入式系统通常会有一个或多个文件系统。根文件系统是其中最重要的,它包含了操作系统所需的基本文件和目录结构。此外,嵌入式系统还可以建立在闪存或其他存储设备上的文件系统,用于存储应用程序、配置文件和数据等。
应用程序:应用程序是嵌入式系统中运行在用户空间的程序,利用Linux内核提供的服务和资源完成特定的功能需求。应用程序可以包括各种应用、服务等用户程序,用于实现各种功能,如通信、控制、数据处理等。
启动阶段
通过这两个阶段的启动过程,引导加载程序能够在启动过程中对硬件进行初始化和配置,并加载操作系统,以使嵌入式系统能够正常运行。这种分阶段的启动过程为系统提供了灵活性和可扩展性,使Bootloader能够在不同的硬件平台和应用场景中进行自定义配置和操作。
预引导阶段(Pre-boot Stage):
第一阶段:该阶段也被称为硬件初始化阶段。在此阶段,Bootloader负责进行硬件初始化和基本系统设置。这包括检测和初始化处理器、内存、时钟、总线和其他外设的操作。
第二阶段:在此阶段,Bootloader负责加载第二阶段的Bootloader代码。此代码位于存储介质上(例如闪存、硬盘等),并负责执行更高级的系统配置和初始化,包括加载文件系统驱动程序等。这一阶段还可以提供用户界面、bootloader配置和固件升级等功能。
操作系统加载阶段(Operating System Load Stage):
该阶段是引导加载程序加载并启动操作系统(通常是Linux内核)的阶段。Bootloader会加载操作系统内核的映像文件,并执行一系列操作,例如设置内核参数、初始化设备树等。然后,它将控制权转移到操作系统的入口点,使操作系统(内核)接管系统的控制和管理。
如图为嵌入式Linux硬件上电到进入用户界面的全过程
在BootLoader的第二阶段结束后,控制权被交给Linux内核,接下来的步骤包括:
内核初始化:
内存管理初始化:设置物理内存和虚拟内存管理。
硬件设备初始化:初始化CPU、设备总线等。
中断处理初始化:设置中断向量表和中断处理程序。
启动核心子系统:
内核子系统初始化:启动调度器、IPC等核心功能。
驱动程序初始化:加载并初始化设备驱动程序。
挂载根文件系统:
内核挂载根文件系统,以访问系统的其余部分。
启动用户空间进程:
启动init进程(PID 1),负责启动用户空间服务和进程。
这些步骤完成后,系统进入正常运行状态,准备管理和处理用户任务。
下图为Linux(X86架构)硬件上电到进入用户界面的全过程
BootLoader的意义
最重要的是:Linux内核是由引导加载程序(Bootloader)装载到内存中的。在系统启动过程中,引导加载程序负责加载Linux内核(大的、独立的程序),将其从存储介质(如磁盘或闪存)读取到内存中的指定位置。
步骤如下:
(1)引导加载程序首先初始化硬件环境,例如处理器、内存和外设等。
(2)引导加载程序根据特定规则(如配置文件或参数)确定Linux内核的位置,通常是指定内核映像文件在存储介质上的位置。
(3)引导加载程序从存储介质中加载Linux内核的映像文件到内存中的指定位置。这个过程涉及到读取映像文件的内容,并将其复制到内存中的指定位置。引导加载程序还可能对内核进行一些预处理或修正,如设定内核启动参数、修改映像文件的头部信息等。
(4)加载完成后,引导加载程序将控制权转移到Linux内核的入口点,以开始内核的执行,随后完成创建/挂载文件系统、设置用户环境、内存和进程管理等各种工作。
(Linux不同于RTOS,它属于分时操作系统)
为什么需要BootLoader?
从其意义出发,总结如下:
(1)硬件初始化 (2)多引导选择 (3)系统配置和参数设置 (4)维护和固件升级 (5)加载操作系统
BootLoader的作用
BootLoader 作用 | 说明 |
硬件初始化 | 检测和初始化计算机或嵌入式系统的硬件设备,如处理器、内存、外部设备等。 |
加载操作系统 | 从存储设备中读取操作系统的引导程序,并将其加载到内存中。 |
启动操作系统 | 将控制权转移到操作系统的引导程序,以启动操作系统的执行。 |
提供启动选项 | 提供用户选择不同操作系统或不同启动模式的选项,支持多系统启动等。 |
提供固件升级 | 支持固件的更新和升级,以提供新功能、修复漏洞或提高系统性能。 |
处理错误检测 | 在启动过程中检测和处理硬件或软件错误,提供错误消息和故障排除功能。 |
支持设备引导 | 支持从不同的存储设备(如硬盘、闪存、网络等)引导操作系统。 |
自定义配置 | 允许用户自定义启动配置、参数和引导顺序,以满足特定需求。 |
下图为window与嵌入式Linux的启动区别
常用的Bootloader(ARM架构)
BootLoader | 特点 |
U-Boot | 开源,通用的Bootloader 支持多种处理器架构和嵌入式平台 功能丰富、灵活性高 用于启动操作系统、加载内核映像和文件系统等 |
Das U-Boot | 在U-Boot基础上进行定制和改进 提供更多功能和特定优化 快速启动、多协议支持和硬件平台兼容性等 |
barebox | 轻量级、模块化的Bootloader 小巧、可靠、高度可定制 适用于资源受限的嵌入式平台 |
TF-A (Trusted Firmware-A) | 开源的ARM架构下的Trusted Boot Firmware 提供安全启动和可信计算功能 用于ARM TrustZone技术的系统 确保系统启动过程的可信度和安全性 |
U-Boot(Universal BootLoader)是一个开源的通用BootLoader,究其开源和自由,功能丰富,平台支持广泛,可定制性强,开发社区活跃,是嵌入式系统中最广泛常用的BootLoader之一。
BootLoader的两种模式
(1)启动加载模式
启动加载模式又叫自主模式,指Bootloader从目标及某个固件存储设备上将操作系统加载到RAM运行,整个过程没有用户介入,是Bootloader的正常工作模式
(2)下载模式
目标机上的Bootloader将通过串口或网络或USB等其它通信手段从主机下载文件,如内核镜像,根文件系统镜像等,从主机下载的文件通常首先被Bootloader保存到目标机的RAM中,然后被Bootloader写到目标机的Flash内的固态存储设备中。这种模式通常在第一次安装内核和根文件系统时使用。系统更新也会使用这种工作模式。
BootLoader启动方式
方式 | 过程 |
引导加载程序(Bootloader) | 内部ROM启动:在芯片的内部ROM存储器中预装的引导加载程序,处理器上电或复位时直接执行。 外部存储器启动:引导加载程序存储在外部存储器(如Flash、SD卡等)中,处理器上电或复位时从外部存储器加载并执行。 刷写引导方式:通过工具或通信接口将引导加载程序刷写到目标设备的存储器中。 |
网络启动方式 | 网络引导启动:通过以太网接口远程下载内核映像或文件系统,实现通过网络启动设备。 PXE(Preboot Execution Environment)引导:通过网络引导服务器(如DHCP、TFTP、FTP等)提供的服务远程引导设备。 |
磁盘启动方式 | BIOS引导:使用电脑主板上的BIOS(基本输入/输出系统)进行引导,从硬盘、软盘或光盘中的引导扇区加载引导加载程序。 UEFI引导:使用统一的EFI(可扩展固件接口)进行引导,支持更大容量的硬盘、安全引导、图形界面等功能。 |
USB启动方式 | USB引导:通过连接到计算机的USB接口上的USB设备(如USB闪存驱动器)引导。 |
Flash启动方式 | 直接从Flash启动:引导加载程序存储在Flash存储器中,处理器上电或复位时直接执行Flash存储器中的引导加载程序。 从Flash复制并解压到RAM启动:将压缩的内存映像文件从Flash存储器复制到RAM中,再从RAM启动。 |
其他定制化启动方式 | SD卡启动:通过SD卡存储器中的引导加载程序和操作系统启动设备。 NAND启动:通过NAND闪存存储器中的引导加载程序和操作系统启动设备。 JTAG启动:通过JTAG接口进行调试和下载引导加载程序到目标设备中。 |
BootLoader依赖性
(1)硬件依赖性:每个硬件平台都有不同的架构、外设和寄存器配置。因此,Bootloader的实现必须与特定硬件平台的细节相匹配。这包括处理器类型、时钟频率、外设控制寄存器地址、中断和异常处理等。Bootloader需要正确地配置和操作硬件以实现正常的启动加载和初始化过程。
(2)设备配置依赖性:即使两个嵌入式板卡采用相同的处理器,它们的外设配置、内存映射和引导设备等可能仍然存在差异。因此,为了让Bootloader能够在不同的嵌入式板卡上运行,可能需要进行适当的参数配置和源代码的调整。这包括处理内存布局、引导设备的选择、时钟设置以及其他特定的硬件配置等。
(3)通信接口依赖性:根据具体的应用场景,Bootloader可能需要与主机或其他外部设备进行通信,如通过串口、以太网或USB接口。为了使Bootloader能够与这些接口正确交互,可能需要针对所用接口的规范和协议进行配置或开发相应的驱动程序。
(4)引导设备和文件系统依赖性:Bootloader通常从引导设备(如Flash、硬盘或SD卡)中加载映像文件(如操作系统内核或应用程序)。为了正确读取和加载这些文件,Bootloader需要了解并支持特定的引导设备类型和文件系统格式。
关于Baremetal
(1)硬件设备初始化:BootLoader的第一阶段负责初始化嵌入式系统的硬件设备,包括处理器、内存控制器、外部设备等。
(2)为加载Stage 2准备RAM空间:在Stage 1中,BootLoader会为加载Stage 2而准备好内存空间(RAM),通常通过栈(stack)的方式进行分配。
(3)加载Stage 2:BootLoader的Stage 1会从预定义的存储设备中读取Stage 2的引导程序,并将其加载到预先分配的内存空间(RAM)中。
(4)设置堆栈:BootLoader在加载Stage 2之前会设置好堆栈指针,以确保在后续的执行过程中能正确地进行函数调用和返回操作。
(5)跳转到Stage 2的C入口点:在加载完Stage 2之后,BootLoader会将控制权转移到Stage 2的C入口点,即Stage 2中的C语言代码执行的起始位置。(至此MCU的BootLoader基本完成,多阶段引导主要是涉及带操作系统的硬件)
这里的C入口点要注意:
在BootLoader的第二阶段,提到的C入口点通常是BootLoader自己的代码,而不是内核的一部分。这些实现因BootLoader的不同而异,比如GRUB的实现可能在其自身的C代码中,LILO是其他方式的实现。
另外一个C入口点:
内核初始化的早期阶段首先在汇编中执行(位于arch/arm64/boot/目录下的代码)。在完成基本的硬件初始化后,控制权会被转移到内核的C语言入口点。这个入口点通常是start_kernel函数,位于init/main.c文件中。
即 main.S并不是C入口点BootLoader的代码。它通常是内核中的汇编文件,负责早期的硬件初始化(用于低级别的初始化工作)。在BootLoader阶段,控制权从BootLoader的代码交给内核后,内核会执行类似main.S中的代码来进行初步设置,然后再进入start_kernel这样的C语言入口点来继续初始化。
这部分MCU没有的:
Stage 2(引导加载器的第二阶段):
(1)初始化硬件设备:BootLoader的Stage 2进一步初始化硬件设备,例如外部设备(键盘、鼠标、显示器等)和各种总线(如USB、PCI等)。
(2)检测内存映像:Stage 2会对系统的内存进行检测,确定可用的内存容量和位置,并为操作系统的加载做好准备。
(3)从存储设备读取内核映像和根文件系统映像:BootLoader的Stage 2从预定义的存储设备中(通常是硬盘或闪存)读取操作系统内核映像和根文件系统映像到事先准备好的内存区域。
(4)设置启动参数:Stage 2有责任为内核设置启动参数,这些参数包括内核命令行参数、映像加载地址等,以便操作系统能够正确地进行初始化和配置。
(5)调用内核:最后,BootLoader的Stage 2会通过跳转或者函数调用的方式将控制权转交给操作系统内核的入口点,以启动操作系统的执行。
关键点:
BootLoader的C入口点和内核的C入口点是两个不同的概念。总结中提到的C入口点是在BootLoader的第二阶段,而真正的内核C入口点是start_kernel。
在BootLoader完成它的任务后,最后一步是将控制权交给内核。之后,内核在完成汇编级别的初始化后,才会进入内核的C入口点。
总结中描述的是BootLoader的阶段,而不是内核的启动阶段。内核的C入口点是在BootLoader完成其工作之后才被调用的。
MCU启动流程分析
上电之后,系统会取执行ROM或者Flash里面的Bootloader启动代码,启动代码是用来在初始化电路以及用来为高级语言编写的软件做好运行前准备的一小段汇编语言。商业实时操作系统中,启动代码部分一般叫板级支持包(BSP)。Bootloader启动具体流程如下:
1.第一步设置中断和异常向量
2.完成处理器芯片一些寄存器的系统启动的最初配置
3.设置看门狗
4.配置系统存储器,包括Flash、SRAM、和DRAM等,并为它们分配地址空间
5.为处理器的每个工作模式设置栈指针,ARM处理器有多种工作模式,每种工作模式都需要设置单独的占空间
6.变量初始化,软件中已经赋值的全局变量,启动过程把这部分变量从只读区域复制到读写区域,已经赋值的静态全局变量直接固化在只读Flash或EEPROM中
7.数据区准备,软件所有未赋值的全局变量,启动过程中需要把这部分变量所在区域清零
8.调用高级语言入口,main函数
区别于Linux的启动
在Linux的启动过程中,大致可以分为以下几个阶段:
BootLoader阶段:
BootLoader负责引导系统启动,它会加载内核到内存中,并将控制权交给内核。常见的BootLoader包括GRUB、LILO等。
内核启动阶段:
在x86架构上,内核一开始是在汇编代码中执行的,主要负责硬件初始化和设置保护模式。
跳转到C入口点并在之后的start_kernel函数中,会进行更高级别的系统初始化,包括初始化内存管理、设备驱动、文件系统等,最终启动用户空间的init进程。
总结:
BootLoader的启动方式基本如下:
stage1程序(汇编)在内存之外的存储介质中对内存空间进行初始化后,再将stage2程序(C)加载到内存中执行。
stage2加载内核(操作系统),由操作系统完成对整个操作系统的初始化或者无操作系统,则无此阶段,加载阶段一后进入C入口点调用main函数。
与Linux下BootLoader启动区别
相同点
(1)硬件初始化:都需要进行基本的硬件初始化,如时钟、外设等。
(2)加载程序:BootLoader负责加载程序代码到内存中。
区别点
(1)复杂性:
STM32的BootLoader通常较简单,专注于基本初始化和启动应用程序,没有操作系统的复杂性。
(2)内存管理:无操作系统的环境中,没有复杂的内存管理,仅需简单的地址设置。
(3)多阶段引导:STM32通常没有多阶段引导,只有一个简单的启动过程。
C入口点
在STM32中,启动代码通常是一个汇编文件,负责初始设置(如堆栈指针)。
C语言入口点通常是Reset_Handler,它最终调用main函数。
跳到main函数的过程
(1)启动文件:在启动文件中,Reset_Handler进行基本初始化。
(2)初始化段:初始化数据段和BSS段。
(3)调用main:完成初始化后,Reset_Handler调用main函数开始执行用户代码。
可能有多种模式(涉及IAP升级)
例如STM32启动配置如下,支持不同的方案: