当你按下电源开关的那一瞬间,第一行代码,是如何在芯片上运行起来的呢?
我们都知道嵌入式软件代码,是需要通过一定的方式,烧录在硬件芯片中才能运行,而我们所熟知的烧录方式,除了物理刻蚀以外,无论是通讯端口的传输,还是调试端口的烧录,多少都是需要驱动程序作支持。所以说是程序烧录程序,软件启动了软件。这听起来就像是我们提着自己的鞋带,把自己给拎了起来,这就是BootStrapLoader最初的命名来源,我们也常简称它为Boot Loader。所以也有中文翻译为自举。
那么最初的软件是怎么烧录进去,运行起来的呢?bootloader是芯片中最初运行的代码吗?其实几乎每一块刚出厂的控制芯片,都在其内部非遗失存储器ROM中,烧录了属于他最基础的软件,cpu搬运并运行第一条代码的默认位置,就在ROM的地址空间,所以一切的起始其实是硬件。以x86架构的鼻祖8086为例,按下开关的一瞬间,芯片的reset引脚,接受到了电平跳变,在一连串电路的作用下,代码段寄存器和指令指针寄存器,分别恢复成0xFFFF和0x0000,他们组合而成的20位长度地址。
正好对应于ROM存放第一条代码的位置,
之后取出这里的指令,再次跳转至别处去。
而ARM架构的芯片也是类似的过程,对于32位的ARM芯片,上电后,PC指针寄存器直接复位至零地址
随后从中断向量表表头的reset向量处,获取下一步跳转的地址。
这时候的代码是以二进制进行存储,处理器直接把它搬运到自身的缓存中运行。有了这第一部分运行起来的代码,就能够跳转至存放有更多复杂代码的地址,执行一些硬件自检、基本初始化、提供基础的输入输出支持,之后还能把更多的程序以及操作系统从外部存储空间加载到内部:
代码就这样,以一种接力方式流转起来,所以我们把出厂就写在ROM里,负责启动后续用户软件的软件称为BootRom或者是RomCode。虽然现在已经不会用严格意义上的只读存储器来存放这部分程序,但至少ROM是一块掉电不易失的存储设备,现在都是使用EEPROM或者是NorFlash,且往往我们也没有权限去修改它,不然改错变砖,芯片就真的没药可治了。不过BootRom对于我们而言,也不是完全的黑盒,大部分芯片都会有外部启动配置引脚,通常是以拨码开关的形式,复位后的一段时间内,BootRom会把引脚采集到的高低电平组合存储起来,作为后续动作和启动设备的选择依据。而对于pc而言,BootRom对应的其实就是我们常说的Bios,他同样留有启动配置途径,而且提供了交互界面,用于配置部分功能,和选择后续的引导设备。那么除了芯片自带的BootRom,我们可能还需要再给自己实际的应用程序,写一个二次引导代码,或者更多层的引导代码,用作操作系统、文件系统加载以及后序程序的更新。也就是常说的在应用编程-IAP。当我们在说bootloader时,我们指的其实也大多数是这样的二次引导代码。
上面的事情大部分芯片的BootRom也能做,还需要bootloader做什么?
其实原因在于,BootRom实现的功能和配置方法不够灵活,且不一定有你想要实现的功能,而用作二次引导的bootloader,是开发人员可以完全控制的引导代码,你可能希望在上电启动后,跳转OS之前,或者是在检查代码更新请求的阶段,已经有一部分基础软件是可用的,那么就需要自己编写bootloader来实现。
举个例子:以汽车上的智能控制器而言,为他设计的bootloader就需要提供符合USB统一诊断服务标准,CAN诊断服务和刷新服务,这就要求我们必须要在另外的bootloader把这些工作给做掉,而不能指望芯片自身的BootRom。
在设计bootloader时,MCU的引导步骤,便开始和嵌入式Linux或PC有所不同,这一定程度上也与不同芯片架构所采用的存储方案有关。先来说MCU,与SOC相比,MCU的主要特征是单核和或多核同构的微处理器。主频一般不超过1G赫兹,般没有MMU内存管理单元。通常最多只能运行RTOS。
常见的MCU:多基于ARM的cortexM和cortexR系列内核、英飞凌自有的tricore内核、TI自有的C28x系列内核等等。
MCU下程序运行的主要介质:
基本都是NOR,因为他和RAM一样,有分离的地址线和数据线,并且可以以字节长度精确寻址,所以NOR中的程序,是不需要拷贝到RAM中运行的。
以英飞凌家的TC27x系列MCU为例,
上电默认读取位置是0x8FFF 8000,这就是他的BootRom。在NorFlash中的地址,并且这块BootRom分为SSW(也就是厂商提供的系统bootloader,SSW是每次上电必须要运行的),
他会根据写在 program flash,PF0地址的前32byte中的配置字来决定SSW执行完的跳转地址,我们可以选择一个合适的跳转地址
比如0x800000020,放上自己写的bootloader,
也可以选择不跳转,运行厂家提供的系统bootloader
来实现对外部代码的接收,不管你用不用它,还是那句老话,我们没有权限,对这片flash地址进行修改。
不同mcu芯片的启动形式又不尽相同:
比如采用Cortex-M内核的 stm32系列,如前面所说,启动时会固定跳转到0地址处,他可以选择把SystemBoot,也就是ARM中的BootRom的地址0x1FFF 0000映射到零地址处。
也可以选择把存放代码的internal Flash或ram的地址映射到这里:
这些选择都是通过两个外部引脚的电平配比来实现的,而你自己写的bootloader就应该放在internal flash的起始处并且把它映射到零地址。
MCU下的bootloader 需要完成的事情有:
那么运行嵌入式 Linux的SOC和PC的这一过程有何不同呢?
先看存储方案:运行嵌入式Linux的SOC,一般将他的操作系统、文件系统和他的应用程序放在nandflash中。处理器运行代码前需要先将代码从NAND搬运到SDRAM中,相比MCU多了一道步骤
PC的存储方案与之类似,NAND一般换做机械硬盘,或者同样属于NAND的固态硬盘一类,所以对于SOC的BootROM和PC的BIOS他们结束运行前的最终任务:是将某些代码从nandflash搬运到内部SRAM中。临shi前转移的重要内容就是bootloader,而一般SOC的bootloader,又分为SPL(Secondary Program Loader)和UBOOT两个阶段。SPL的secondary就是相对于BootROM而言,他就像是接力赛中的第二棒选手,那么首先被搬运到内部SRAM中的SPL,会初始化空间更大的,外部的DRAM。再负责把uboot搬运至外部RAM中去运行,这就完成了他第二棒的交接。而uboot作为第三棒选手,他开始运行他的初始化程序,之后再根据系统环境变量,将OS内核搬运到外部RAM中去运行,完成他这一棒的交接。
同样的,OS再去完成根文件系统的加载。
总结来看,虽然对不同的芯片架构,在不同的芯片手册中,可能会对这几个阶段的引导代码,有着不同的称法和应用方式,但芯片的启动,基本都逃不过这三层的引导结构。
第一层就是芯片出厂自带的BootRom,由于硬件自检和部分初始化,加载Bootloader、提供外部配置引脚
第二层就是我们自己写的Bootloader层,可以通过他访问外部RAM、NANDFlash,更多的存储设备,初始化时钟、通讯等,接收、存储以及跳转代码,
第三层就是我们常见的app层,它包含操作系统、文件系统,用户应用程序等,可以方便被更新的代码。