网络知识 娱乐 Bootloader详解,理解Bootloader看这篇就够了

Bootloader详解,理解Bootloader看这篇就够了

Bootloader作用及实现步骤

  • 一、Bootloader作用(目的)
  • 二、完成Boot最终目的的前提条件
  • 三、对前提条件的详细说明
    • 3.1、对前提条件(1)的说明:
    • 3.2、对前提条件(2)的说明:
    • 3.3、对前提条件(3)的说明:
    • 3.4、对前提条件(4)的说明:
    • 3.5、对前提条件(5)的说明:
    • 3.6、Boot阶段基本流程简单总结:
  • 四、loader是干啥的

一、Bootloader作用(目的)

Bootloader=Boot + loader

Boot的目的:
最终目的:跳到C语言中;为了C语言运行程序会进行一系列的初始化,系统一上电后如何通过一系列的设置让软件程序员进入C语言/更高级语言环境的开发,这个过程就是boot的主要目的。

Loader的目的:
主要目的是开始执行应用逻辑,比如点灯:需要灯的接口开发;串口输入输出:需要串口编程;加载linux的内核:flash的编程、网卡的编程、内核启动前的初始化部分。根据不同的应用会有不同的变化。

一个开发板要想执行loader,要先看boot做了什么事。

二、完成Boot最终目的的前提条件

围绕boot的最终目的,对执行最终目的的前提条件进行说明:
前提条件:
(1)让SP指向可读可写的设备空间中

(2)满足递减栈的规则—SP想办法放在内存的高段地址

(3)配置SDRAM的控制器。首选空间为SDRAM,SDRAM不是系统一上电就好,所以在做(1)、(2)之前要配置SDRAM的控制器,使SDRAM可以正常工作。

(4)配置系统工作时钟,通过代码对相应寄存器进行相关配置即可实现。

(5)关闭看门狗、中断、MMU、CACHE,通过汇编语言对相应的寄存器进行置位即可实现。

开发时前提条件的执行顺序:(5)、(4)、(3)、(1)、(2)

三、对前提条件的详细说明

3.1、对前提条件(1)的说明:

先看代码:

int main()
{
	abc();//main函数中调用abc函数
}

int abc()
{
	int  a = 10;//假设用r0寄存器保存了立即数10,接下来跳到fun函数中
	fun();//abc函数中调用fun函数
	if(a xx)//函数fun返回后再判断a的情况,发现a不再是10
}

int fun()
{
	int  b = 20;/*假设同样用寄存器r0保存20,不用内存保存是因为寄存器的访问速度比内存访问速度快,
				函数abc中的变量a在整个代码执行区中不需要再往回传给main函数了,a是一个临时的值,
				寄存器r0只有一个,也只能保存一个,所以a只是出现一次,然后就不用了。当abc函数调用fun函数后,
				寄存器r0中的值10会被20替换*/
}

在上述代码运行过程中,一旦函数abc又调用了函数fun,寄存器r0中的值会被替换,从10变成了20,函数fun在返回后就会出现问题:这时用if语句判断a的情况,会发现a已经不再等于10了。

如果按照上述逻辑去设计汇编的话问题会有很多。于此问题,C语言给我们提供了栈的概念—stack

栈的逻辑:先进后出、后进先出,压栈出栈是通过代码对指向栈区指针的值进行更新、赋值,进而对指针指向的内存空间进行读和写。由此有了栈指针概念—该指针必须指向一个具备可以进行读写的硬件空间。在Arm体系架构中,栈指针用寄存器SP来描述。何为可读可写——与应用程序思路不同,硬件上的可读可写如代码:

int  a = 10;//这句话执行完后让以a为首地址的那段空间/设备把10赋进去
*(&a)==10;//进行解引用后将值取出来和10是相等的相当于将10写进去了

如果a的地址有问题,比如它指向的是一个不存在的空间。何为不存在的空间——0x2000 0000是内存机地址,但硬件公司只买了一个16M的内存,现在软件工程师想把a放在地址0x2100 0000,这个地址发出去后显然没有落到16M的范围内,那么对a的赋值就无法进行了。这时C语言无法帮我们进行压栈入栈的操作。

小结:栈指针一定要指向一个合法的设备——可读可写的地址空间。

3.2、对前提条件(2)的说明:

系统刚上电时SP指向的是0地址,即ROM,显然写不进去。所以在跳到C语言之前一定要让SP指向一个可读可写的设备区间中,SP首选指向地址是SDRAM中。如果用汇编写Bootloader:
代码为:

MOV  SP, #0x2000  0000//跳之前将sp赋值
BL  MAIN//跳到MAIN中

这样会出现问题,我们在学Arm基础时知道栈指针在C语言中有一个规范,称之为ATPCS,它规定C语言默认将SP作为栈指针,且SP指向的是一个向下压栈的过程,所以把栈指针指向内存的低地址不合适,SP指针一定要往上指,所以要满足递减栈的规则。
代码应为:

MOV  SP, #0x2100  0000//跳之前将sp赋值
BL  MAIN//跳到MAIN中

问题没完。既然指向地址0x2100 0000,而这个地址是内存,内存是一上电就能干活?不行,内存涉及到一个动态的充电过程。内存的访问:发出地址——直接把数据发给它——将数据写过去。
假设内存大小是16M,地址总线数计算:
在这里插入图片描述
存储总量=存储单元个数×存储字长,
然后:
存 储 单 元 个 数 = 2 地 址 总 线 位 数 存储单元个数 = 2^{地址总线位数} =2线

另:
1 M B = 1024 K B = 1024 B ∗ 1024 = 1048576 B = = 2 20 B 1MB=1024KB=1024B*1024=1048576B== 2^{20}B 1MB=1024KB=1024B1024=1048576B==220B

存储字长是什么:
存储字长:存储单元中的二进制代码(存储字)位数,存储字长可以是8位、16位、32位等。
早期计算机的存储字长一般和机器的指令字长与数据字长相等,故访问一次主存便可取一条指令或一个数据。随着计算机的应用范围的不断扩大,解题精度的不断提高,往往要求指令字长是可变的,数据字长也要求可变。为了适应指令和数据字长的可变性,其长度不由存储字长来确定,而由字节的个数来表示。1个字节(Byte)被定义为由8位(Bit)二进制代码组成。
存储字是指存放在一个存储单元中的二进制代码组合。一个存储字可代表一个二进制数,也可代表一串字符,如存储字为0011011001111101,既可表示为由十六进制字符组成的367DH(ASCII码),又可代表16位的二进制数,此值对应十进制数为13 949,还可代表两个ASCII码:“6”和“}”。一个存储字还可代表一条指令。 字长就是在同一时间中处理二进制数的位数叫字长。通常称处理字长为8位数据的CPU叫8位CPU,32位CPU就是在同一时间内处理字长为32位的二进制数据。
存储字长是在内存中存储字的长度,可以是16bit或8bit

故:
16 M = 2 地 址 总 线 位 数 × 存 储 字 长 16M=2^{地址总线位数}×存储字长 16M=2线×

即:
( 2 4 ∗ 2 20 ) B = 2 地 址 总 线 位 数 × 存 储 字 长 (2^4*2^{20})B= 2^{地址总线位数}×存储字长 (24220)B=2线×

存储字长为1,所以地址总线数为24。

所以这个内存有24根地址总线,用这24根地址总线就可以对16兆进行逐一编址。现在CPU只要发出对应的地址,就会进行数据的读写。

如果一个芯片的内存比较大,为了减小芯片的体积,需要减少地址总线的数目。如何用较少的地址线来将较大的内存全访问到?利用C语言中一维数组、二维数组的概念。
如代码:

int  a[16];//在这里定义了16个字节(2^4),用4根线就能找到它(用4bit可以表示)

即用这4根线就能表示这16种状态。
现在想办法减少这4根线的数目,对于同样的空间我还可以怎样访问?如代码:

int  b[4][4];//将16个字节分为4行4列

用这种方式访问时需要知道行数和列数,范围都是0~3,4个字节大小,现在用2bit就能表示(2根线访问)。先发出行坐标,再发出列坐标。
通过改成2维数组使得外部接口上的位数变少了。数组维数越大,在表示相同大小的内存时,需要的位数越小。

芯片内存的内部结构可以理解为一个多维数组,它由多个面构成,要想访问其中的一块空间,要先知道它在哪一个面上,然后知道它在这个面上的行、列坐标(地址)。内部结构大致如下图所示:
在这里插入图片描述
引出问题:现在发出地址0x2100 0000,要将这个地址转换为在这个芯片中的哪一个面上、哪一行上、那一列上。这个转换过程可以由软件程序员来自行分解,也可以由硬件做。软件做成本高、意义也不大,故而让硬件来帮我们确定。要让硬件做会引出另外一个概念:在芯片的datasheet中的DRAM CONTROLLER中规定了这一过程:见对前提条件(3)的说明中的内容。

细节补充说明:SP指针要往上指,但是不是随便指。在ARM体系中ARM工作分了7个工作空间。第一个:SVC模式,在系统复位后默认会用的一个模式,工作在Linux的kernel空间;第二个:IRQ模式,中断一旦发生后的运行模式;第三个:FIQ模式;第四个:USR模式,它对应的实际上就是Linux下的app,就是应用程序空间。
…等七个工作空间。
其中USR模式和SVC模式工作的空间都有自己的sp指针,这样用户空间操作的代码不会影响内核。这样会出现一个问题,内存假如只有一块,两个sp指针在遵循指向高地址的原则时可能会重复指向一个地址,所以在对sp赋值时要考虑用哪一个模式时要将sp分开,如何分开:对应每一个工作模式划分相应的可供使用的内存空间,如下图所示,但这时可能会出现另一个问题,如果USR的内存空间没有设计好,一旦发生死循环递归,栈会慢慢的往下延伸,会发生栈的溢出,溢出后会将SVC的空间内容修改了,那么在执行SVC时结果就错了,程序就跑得乱七八糟。
在这里插入图片描述
所以在执行前提条件(1)(2)的时候要考虑几个问题,首先要考虑用哪些空间/模式,就要初始化哪些模式下的sp,如何初始化:有一套专门用于模式切换的指令。其次在对sp赋值时有原则:每个模式的值不能重复、不能覆盖其他模式。

3.3、对前提条件(3)的说明:

在这里插入图片描述
CPU直接发出地址后现在要访问SDRAM,让CPU把想做的事情告诉给控制器,控制器根据我的配置(行地址数、列地址数、多少块、周期性充电问题、计算多久让它充一次电)

关于计算多久让它充一次电:CPU是一个脉冲波形,这里要引入时序的概念。
在这里插入图片描述
时序是芯片的心脏,每一个上升沿触发CPU干一件事,多久充一次电?
控制器凭什么知道经过多久可以充?所以要先配置控制器,在配置中又有一个问题,CPU和控制器都有自己的工作频率,它们共用同一个频率吗?即它们共用同一个时钟吗?因为外围控制器的成本/芯片工艺过程决定了控制器处理数据的能力较弱,导致不能同步接收到发送给它的数据,所以总时钟总线上的频率不会直接给控制器用,而是通过一个分频器,将频率降低后再给它用。所以还要先配置系统工作时钟。只有配置好了工作时钟才能计算出多久充一次电。

3.4、对前提条件(4)的说明:

在嵌入式开发过程中这一步是最耗时的,因为后期Linux内核最终还是运行在RAM,包括很多程序都是在RAM上运行,而RAM中,如果我们给它的频率不符合这款芯片本身的物理特性,那么很容易出现程序跑飞,在PC中就可能出现程序在运行时突然蓝屏,蓝屏的原因有很多,但是大部分都是由于内存的问题,有些时候买的内存的芯片的物理结构/质量不好,响应时间会很慢。举例:现在CPU要发出序列1101,现在芯片要想办法以一定的速度将这4bit发到数据总线上,或者说给到这片SDRAM上,假设CPU工作在1GHZ(这频率很快了),基本上1GHZ分之一再乘以4,这个时间就可以把4个比特发完了。SDRAM价格便宜,根本就不能同步响应1GHZ的CPU给的信息,那么内存工作就有问题,出现蓝屏。

配置SDRAM的难度就在于不仅要看懂SDRAM的芯片手册,同时还要懂得一定的硬件原理,包括一些调试经验,然后去动态的算出针对某一块开发板的SDRAM的具体工作参数。

3.5、对前提条件(5)的说明:

配置系统时钟之前还有一个问题,不是配了就马上工作正常,还要经过一段时间,在这期间还要引入看门狗。看门狗就是一个定时器,在一段时间之内,如果不对定时器进行重新的喂狗,一旦减到0就会触发一个中断/复位,CPU就相当于重启了。所以要保证有足够的长的时间来执行前提条件(1)、(2)、(3)、(4),所以在配置时钟之前为了安全起见还要把看门狗给关了。同时中断也要关了,因为我们还没进入到C或者说还没进到系统处理,即使中断来了,我们还不知道该怎么处理,因为我们的函数还没注册进去,这时最好把中断也关了,在调用main函数时再将中断打开。除此之外还可以关闭MMU、CACHE,MMU一般指的是有操作系统情况下才用,裸机程序上一般情况下为了简单一点可以不要管它,对于CACHE,指令CACHE无所谓可开可关,但是数据CACHE一般情况下在这时最好关了,虽然说速度可以提高,但是由于涉及到数据完整性的问题,有可能CACHE打开后数据的完整性就会有影响,这样给后期的调试带来很大麻烦,所以先关了它。

3.6、Boot阶段基本流程简单总结:

Boot最终目的是跳到C语言,在跳到C语言之前要考虑栈指针的初始化,因为C语言编译器要自动的利用SP指针来作为我们的栈空间,这样我们就必须要把SP指向一个可读可写的设备,具体赋什么值要根据规则来决定,然后再放到SDRAM上,紧接着SDRAM要想工作,在擦之前还要配置SDRAM,这个工作量比较大,包括:行地址数、列地址数、多少块、周期性充电问题,在配置SDRAM之前还要计算多久充一次电,所以还要配置系统时钟,而系统时钟工作还要一些前导,即关闭看门狗、中断、MMU、CACHE。

所以具体的来说,不管什么系统,要想设计一款Bootloader中的Boot阶段基本是要完成上述前提工作。最后根据每个系统的条件不同,还涉及到一个概念就是代码搬移。

为什么要代码搬移?一个芯片,系统启动时它会规定只能处理多少量的代码,如果代码量超过了规定的可处理的量,多余的代码就处理不了了,这时就要程序员自己把代码搬到一个更大的空间上去运行,要程序员自己对控制器进行控制,所以在main函数执行前也还要对相应存储器的控制器进行初始化,在代码搬移前还要初始化对应控制器,这样做软件工程师的工作量大一些,因为首先要看硬件公司用哪一个存储器启动,比如SD卡就要写SD卡的驱动,如果是NAND启动,就要写NAND的驱动,然后考虑怎么把代码快读搬到内存上,跳到内存上执行,一般来说代码搬移到RAM上,所以都要在SDRAM工作正常的基础上。其次是执行速度问题,如果代码本身之前工作在flash上,flash的执行速度比RAM慢得多,所以要把程序从存储器(nor-flash)搬移到快速的内存上运行。这就是我们说的代码搬移。

以上就是完成Bootloader中的Boot工作所要进行的一些事。

四、loader是干啥的

当MAIN函数跳过来之后(进入到C语言后)进行Loader的目的,即开始执行应用逻辑,这个要根据具体的工程代码来决定。