主机和目标机之间一般通过串口建立连接,Bootloader软件在执行时通常会通过串口来进行I/O,如输出打印信息到串口,从串口读取用户控制字符等。Bootloader的启动过程有单阶段(SingleStage)和多阶段(Multi-Stage)两种形式。通常多阶段的Bootloader能提供更复杂的功能,以及更好的可移植性。从固态存储设备上启动的Bootloader大多都是两阶段的启动过程,即启动过程可以分为stage1和stage2两部分。stage1主要完成依赖于CPU体系结构的代码的初始化,如设备初始化代码等,而且这些代码通常都是用汇编语言的形式来实现的。stage2通常用C语言来实现,用以实现较复杂的功能,并且使代码具有更好的可读性和可移植性。
1.stage1通常包括以下步骤(按执行的先后顺序)
(1)硬件设备初始化
这是Bootloader一开始就执行的操作,其目的是为stage2的执行以及随后的Kernel的执行准备好一些基本的硬件环境。它通常包括以下步骤(按执行的先后顺序):
●屏蔽所有的中断。为中断提供服务通常是OS设备驱动程序的责任。因此,在Bootloader的执行全过程中可以不必响应任何中断。中断屏蔽可以通过写CPU的中断屏蔽寄存器或状态寄存器(如ARM的CPSR寄存器等)来完成。
●设置CPU的速度和时钟频率。
●RAM初始化。包括正确地设置系统的内存控制器的功能寄存器以及各内存库控制寄存器等。
●初始化LED。典型地,通过GPIO来驱动LED,其目的是表明系统的状态是OK还是Error。如果板子上没有LED,那么也可以通过初始化UART向串口打印Bootloader的Logo字符信息来完成这一点。
●关闭CPU内部指令/数据Cache。
(2)为加载Bootloader的stage2准备RAM空间
为了获得更快的执行速度,通常把stage2加载到RAM空间中来执行。因此,必须为加载Bootloader的stage2准备好一段可用的RAM空间范围。
由于stage2通常是C语言执行代码,因此在考虑空间大小时,除了stage2可执行映像的大小外,还必须把堆栈空间也考虑进来。此外,空间大小最好是memorypage大小(通常是4KB)的倍数。一般而言,1MB的RAM空间已经足够了。具体的地址范围可以任意安排,如blob就将它的stage2可执行映像安排到从系统RAM起始地址0xc0200000开始的1MB空间内执行。但是,将stage2安排到整个RAM空间的最顶1MB(即RamEnd-1MB~RamEnd)是一种值得推荐的方法。
为了后面的叙述方便,这里把所安排的RAM空间范围的大小记为stage2_size(字节),把起始地址和终止地址分别记为stage2_start和stage2_end(这两个地址均以4B边界对齐)。因此
另外,还必须确保所安排的地址范围的确是可读写的RAM空间,因此,必须对所安排的地址范围进行测试。可以采用以下的检测算法:
1)先保存memorypage一开始两个字的内容。
2)向这两个字中写入任意的数字。如向第一个字写入0x55,向第2个字写入0xaa。
3)然后,立即将这两个字的内容读回。显然,读到的内容应该分别是0x55和0xaa。如果不是,则说明这个memory page所占据的地址范围不是一段有效的RAM空间。
4)再向这两个字中写入任意的数字。如向第一个字写入0xaa,向第二个字写入0x55。
5)然后,立即将这两个字的内容读回。显然,读到的内容应该分别是0xaa和0x55。如果不是,则说明memorypage所占据的地址范围不是一段有效的RAM空间。
6)恢复这两个字的原始内容。测试完毕。
为了得到一段干净的RAM空间范围,也可以对所安排的RAM空间范围进行清零操作。
(3)复制Bootloader的stage2到RAM空间中
复制时要确定stage2的可执行映像在固态存储设备的存放起始地址和终止地址以及RAM空间的起始地址。
(4)设置好堆栈
堆栈指针的设置是为执行C语言代码做好准备。此外,在设置堆栈指针SP之前,也可以关闭LED,以提示用户准备跳转到stage2。经过上述这些执行步骤后,系统的物理内存布局应该如图3-2所示。
图3-2 Bootloader的stage2可执行映像刚被复制到RAM空间时的系统内存布局
(5)跳转到stage2的C入口点
在上述一切都就绪后,就可以跳转到Bootloader的stage2去执行了。比如,在ARM系统中,可以通过修改PC寄存器为合适的地址来实现。
2.stage2通常包括以下步骤(按执行的先后顺序)
(1)初始化本阶段要使用到的硬件设备
初使化本阶段要使用到的硬件设备通常包括初始化至少一个串口,以便和终端用户进行I/O输出信息;初始化计时器等。在初始化这些设备之前,也可以重新把LED点亮,以表明已经进入main()函数执行。设备初始化完成后,可以输出一些打印信息,程序名字字符串、版本号等。
(2)检测系统内存映射
内存映射(Memory Map)是指在整个4GB物理地址空间中有哪些地址范围被分配用来寻址系统的RAM单元。虽然CPU通常预留出一大段足够的地址空间给系统RAM,但是在搭建具体的嵌入式系统时却不一定会实现CPU预留的全部RAM地址空间。也就是说,具体的嵌入式系统往往只把CPU预留的全部RAM地址空间中的一部分映射到RAM单元上,而让剩下的那部分预留RAM地址空间处于未使用状态。由于上述这个事实,因此Bootloader的stage2必须在它想干点什么(如将存储在Flash上的内核映像读到RAM空间中)之前检测整个系统的内存映射情况,即它必须知道CPU预留的全部RAM地址空间中的哪些被真正映射到RAM地址单元,哪些是处于“unused”状态的。
可以用如下数据结构来描述RAM地址空间中的一段连续(Continuous)的地址范围:
这段RAM地址空间中的连续地址范围可以处于两种状态之一:
●used=1,说明这段连续的地址范围已被实现,即真正地被映射到RAM单元上。
●used=0,说明这段连续的地址范围并未被系统所实现,而是处于未使用状态。
基于上述memory_area_t数据结构,整个CPU预留的RAM地址空间可以用一个memory_area_t类型的数组来表示,如下所示:
下面给出一个可用来检测整个RAM地址空间内存映射情况的简单而有效的算法:(www.xing528.com)
在用上述算法检测完系统的内存映射情况后,Bootloader也可以将内存映射的详细信息打印到串口。
(3)将Kernel映像和根文件系统映像从Flash上读到RAM空间中
首先规划内存占用的布局,这里包括两个方面:内核映像所占用的内存范围;根文件系统所占用的内存范围。在规划内存占用的布局时,主要考虑基地址和映像的大小两个方面。
对于内核映像,一般将其复制到从基地址开始的大约1MB大小的内存范围内。
由于像ARM这样的嵌入式CPU通常都是在统一的内存地址空间中寻址Flash等固态存储设备的,因此从Flash上读取数据与从RAM单元中读取数据并没什么区别。用一个简单的循环就可以完成从Flash设备上复制映像的工作。
(4)为内核设置启动参数
在将内核映像和根文件系统映像复制到RAM空间后,就可以准备启动Linux内核了。但是在调用内核之前,应该做一些准备工作,即设置Linux内核的启动参数。
Linux2.4.x以后的内核都期望以标记列表(taggedlist)的形式来传递启动参数。启动参数标记列表以标记ATAG_CORE开始,以标记ATAG_NONE结束。每个标记由标识被传递参数的tag_header结构以及随后的参数值数据结构组成。数据结构tag和tag_header定义在Linux内核源码的include/asm/setup.h头文件中。
在嵌入式Linux系统中,通常需要由Bootloader设置的常见启动参数有ATAG_CORE、ATAG_MEM、ATAG_CMDLINE、ATAG_RAMDISK、ATAG_INITRD等。例如,设置ATAG_CORE的代码如下:
其中,BOOT_PARAMS表示内核启动参数在内存中的起始基地址;指针params是一个structtag类型的指针。宏tag_next()将以指向当前标记的指针为参数,计算紧临当前标记的下一个标记的起始地址。注意,内核的根文件系统所在的设备ID就是在这里设置的。
下面是设置内存映射情况的示例代码:
可以看出,在memory_map数组中,每一个有效的内存段都对应一个ATAG_MEM参数标记。
Linux内核在启动时可以以命令行参数的形式来接收信息,利用这点可以向内核提供内核不能自己检测的硬件参数信息,或者重载内核自己检测到的信息。下面是一段设置调用内核命令行参数字符串的示例代码:
下面是设置ATAG_INITRD的示例代码,它告诉内核在RAM中的什么地方可以找到initrd映像(压缩格式)以及它的大小。
下面是设置ATAG_RAMDISK的示例代码,它告诉内核解压后的Ramdisk有多大(单位是KB)。
最后,设置ATAG_NONE标记,结束整个启动参数列表。
(5)调用内核
Bootloader调用Linux内核的方法是直接跳转到内核的第一条指令处,即直接跳转到MEM_START+0x8000地址处。在跳转时,要满足下列条件:
●CPU寄存器的设置:R0=0;R1=机器类型ID;关于MachineTypeNumber,可参见linux/arch/arm/tools/mach-types;R2=启动参数标记列表在RAM中的起始基地址。
●CPU模式:必须禁止中断(IRQ和FIQ);CPU必须为SVC模式。
●Cache和MMU的设置:MMU必须关闭;指令Cache可以打开,也可以关闭;数据Cache必须关闭。
如果用C语言,可以像下列示例代码这样来调用内核:
theKernel()函数调用应该是永远不返回的。如果这个调用返回,则说明出错。
Bootloader的系统启动方案流程如图3-3所示。
图3-3 Bootloader系统启动方案流程
大多数的Bootloader都包含两种不同的操作模式(OperationMode):启动加载模式和下载模式。这种区别仅对于开发人员才有意义。但从最终用户的角度看,Bootloader的作用就是用来加载操作系统,而并不存在所谓的启动加载模式与下载工作模式的区别。
●启动加载(Bootloading)模式:这种模式也称为“自主”(Autonomous)模式,即Bootloader从目标机上的某个固态存储设备上将操作系统加载到RAM中运行,整个过程并没有用户的介入。这种模式是Bootloader的正常工作模式,因此在嵌入式产品发布时,Bootloader必须工作在这种模式下。
●下载(Downloading)模式:在这种模式下,目标机上的Bootloader将通过串口连接或网络连接等通信手段从主机(Host)下载文件,如下载内核映像和根文件系统映像等。从主机下载的文件通常首先被Bootloader保存到目标机的RAM中,然后再被Bootloader写到目标机上的Flash类固态存储设备中。Bootloader的这种模式通常在第一次安装内核与根文件系统时被使用;此外,以后的系统更新也会使用Bootloader的这种工作模式。工作于这种模式下的Bootloader通常都会向它的终端用户提供一个简单的命令行接口。
免责声明:以上内容源自网络,版权归原作者所有,如有侵犯您的原创版权请告知,我们将尽快删除相关内容。