从前面的硬件实例可以知道,系统上电以后,需要通过一段程序来进行初始化:关闭WATCHDOG、改变系统时钟、初始化存储控制器、将更多的代码复制到内存中等。如果他能将操作系统内核复制到内存中运行,无论是本地(比如Flash)还是远端(比如网络),就称这段程序为Bootloader
简单的说,Bootloader就是这么一小段程序,它在系统上电时开始执行,初始化硬件设备、准备好软件环境、最后调用操作系统内核。
可以增强Bootloader的功能,比如增加网络功能,从PC上通过串口或网络下载文件、烧写文件、将Flash上压缩的文件解压后再运行等,这就是一个功能强大的Bootloader,也称monitor。实际上,在最终产品中用户并不需要这些功能,它们只是为了方便开发。
Bootloader的实现非常依赖具体的硬件,在嵌入式系统中 硬件配置千差万别,即使是相同的CPU,它的外设(比如Flash)也可能不同,所以不可能有一个Bootloader支持所有的CPU,所有的电路板。即使是支持CPU架构比较多的U-Boot,也不是一拿来就可以使用的(除非里面的配置刚好与你的板子相同 ),需要进行一些移植
CPU上电后,会从某个地址开始执行。比如MIPS结构的CPU会从0xBFC00000取得第一条指令,而ARM结构的CPU则从地址0x00000000开始。嵌入式开发板中,需要把存储器件ROM或Flash等映射到这个地址,Bootloader就存放在这个地址开始处,这样一上电就可以执行。
在开发时,通常需要使用各种命令操作Bootloader,一般通过串口来连接PC和开发板,可以在串口上输入各种命令,观察运行结果等。这也只是对开发人员才有意义,用户使用产品时是不用接串口来控制Bootloader的。从这个观点来看,Bootloader可以分为以下两种操作模式
启动加载(Boot loading)模式
上电后,Bootloader从板子上的某个固定存储设备上将操作系统加载到RAM中运行,整个过程并没有用户的介入。产品发布时,Bootloader工作在这种模式下。
下载(Downloading)模式
在这种模式下,开发人员可以使用各种命令,通过串口连接或网络连接等通信手段从主机(host)下载文件,(比如内核映象,文件系统映象),将它们直接放在内存运行或烧入Flash类固态存储设备中
板子与主机间传输文件时,可以使用串口的xmodem/ymodem/zmodem协议,它们使用简单,只是传输速度比较慢;还可以使用网络通过tftp,nfs协议来传输,这时,主机上要开启tftp、nfs服务;还有其他方法,比如USB等。
像Blob或U-Boot等这样功能强大的Bootloader通常同时支持两种工作模式,而且允许用户在这两种工作模式之间进行切换。比如,U-Boot在启动时处于正常的启动加载模式,但是他会延时若干秒(这里可以设置),等待终端用户按下任意键,而将U-Boot切换到下载模式。如果在指定时间内没有用户按键,则U-Boot继续启动Linux内核。
总的来所,U-Boot就是一个Bootloader,可以实现在系统上电后,进行初始化系统(关闭WATCHDOG,改变系统时钟,初始化存储控制器等),复制操作系统到内存中功能的这样一段小程序。
Bootloader1的启动方式,一般有两种;启动加载模式,产品发布时,Bootloader的工作模式,不需要用户操作。Bootlaoder会从某个固态存储设备上将操作系统加载到RAM下进行。
下载模式:开发人员可以通过串口,网络等方式,通过发送命令的方式从主机中将操作系统直接放在内存运行或烧入Flash类固态存储设备。
在移植Bootloader之前先了解Bootloader的一些通用概念,对理解它的代码会有所帮助。
嵌入式Linux系统从软件的角度通常可以分为以下4个层次。
固件(Firmware) 就是写入EROM(可擦写只读存储器)或EEPROM(电可擦可编程只读存储器)中的程序。
固件是指设备内部保存的设备“驱动程序”,通过固件,操作系统才能按照标准的设备驱动实现特定机器的运行动作,比如光驱、刻录机等都有内部固件。
固件是担任着一个系统最基础最底层工作的软件。而在硬件设备中,固件就是硬件设备的灵魂,因为一些硬件设备除了固件以外没有其它软件组成,因此固件也就决定着硬件设备的功能及性能。
固件是为控制硬件设备而设计的。在计算机中,我们称之为BIOS(基本输入/输出系统)或UEFI(统一可扩展固件接口)。BIOS是计算机上电后第一个启动的。它可以与硬件交互,并检查它是否有任何错误。BIOS调用另一个名为bootloader的程序,它负责唤醒沉睡在硬盘驱动器内的操作系统,并将其放入随机存取存储器中。
2. Linux内核 特定于嵌入式板子的定制内核以及内核的启动参数。内核的启动参数可以是内核默认的,或是由Bootloader传递给她的
3. 文件系统 包括根文件系统和建立在Flash内存设备之上的文件系统。里面包含了Linux系统能够运行所必须的应用程序,库等,比如可以给用户提供操作Linux的控制界面的shell程序,动态连接的程序运行时需要的glibc或uClibc库等。
4. 用户应用程序
特定于用户的应用程序,它们也存储在文件系统中。有时在用户应用程序和内核层之间可能还会包括一个嵌入式图形用户界面。常用的嵌入式GUI有:Qtopia和MiniGUI等。
显然,在嵌入系统的固态存储设备上有相应的分区来存储它们,如下图所示,为一个典型的分区结构。
Boot parameters 分区中存放一些可设置的参数,比如IP地址,串口波特率、要传递给内核的命令行参数等。正常启动过程中,Bootloader首先运行,然后它将内核复制到内存中(也有些内核可以在固态存储设备上直接运行),并且在内存某个固定的地址设置好要传递给内核的参数,最后运行内核。内核启动之后,它会挂载(mount)根文件系统(“Root Filesystem”),启动文件系统中的应用程序。
Bootlaoder的启动过程可以分为单阶段(Single Stage)、多阶段(Multi-Stage)两种、通常多阶段的Bootloader能够提供更为复杂的功能以及更好的可移植性。从固态存储设备上启动的Bootloader大多都是两阶段的启动过程。
第一阶段使用汇编来实现,它完成一些依赖于CPU体系结构的初始化,并调用第二阶段的代码;
第二阶段则通常使用C语言来实现,这样可以实现更加复杂的功能,而且代码会有更好的可读性和可移植性。
一般而言,这两个阶段完成的功能可以如下分类。
甚至,将第二阶段的代码复制到RAM空间中也不是必须的,对于NOR Flash等存储设备,完全可以在上面直接执行代码,只不过相比在RAM中执行效率大为降低。
为了方便开发,至少要初始化一个串口以便程序员与Bootloader进行交互。
所谓检测内存映射,就是确定板上使用了多少内存、它们的地址空间是什么。由于嵌入式开发中Bootloader多是针对某类板子进行编写,所以可以根据板子的情况直接设置,不需要考虑可以适用于各类情况的复杂算法。
Flash上的内核映象有可能是经过压缩的,在读到RAM之后,还需要进行压缩。当然,对于有自解压功能的内核,不需要Bootloader来解压。
将根文件系统映象复制到RAM中,这不是必须的。这取决于是什么类型的根文件系统,以及内核访问它的方法。
将内核存放在适当的位置后,直接跳到它的入口点即可调用内核。调用内核之前,下列条件满足。
void (*theKernel)(int zero ,int arch,u32 params_addr)=(void (*)(int ,int
,u32))KERNEL_RAM_BASE;
theKernel(0 ,ARCH_NUMBER,(u32) kernel_params_start);
Bootloader与内核的交互是单向,Bootloader将各类参数传给内核。由于它们不能同时运行,传递办法只有一个:Bootloader将参数放在某个约定的地方之后,再启动内核,内核启动后从这个地方获得参数。
除了约定好参数存放地址外,还要规定参数的结构。Linux2.4X以后的内核都期望以标记列表(tagged list)的形式来传递启动参数。标记,就是一种数据结构;标记列表,就是挨着存放在的多个标记。标记列表以标记ATAG_CORE开始,以标记ATAG_NONE结束。
标记的数据结构为tag,它由一个tag_header结构和一个联合(union)组成。tag_header 结构表示标记的类型及长度,比如是表示内存还是命令行参数等。对于不同类型的标记使用不同的联合(union),比如表示内存时使用tag_mem32,表示命令行时使用tag_cmdline。数据结构tag和tag_header定义在Linux内核源码的 include/asm/setup.头文件中,如下图所示
下面以设置内存标记、命令行标记为例说明参数的传递
params=(struct tag *)0x300000100;
params->hdr.tag=ATAG_CORE;
params->hdr.size=tag_size(tag_core);
params->u.core.flags=0;
params->u.core.pagesize=0;
params->u.core.rootdev=0;
params=tag_next(params);
其中,tag_next定义如下,它指向当前标记的末尾:
#define tag_next(t) ((struct tag *)((u32 *)(t)+(t)->hdr.size))
2.设置内存标记
假设开发板使用的内存起始地址为0x300000100,大小为0x40000000,则内存标记可以如下设置:
params->hdr.tag=ATAG_MEM;
params->hdr.size=tag_size(tag_mem32);
params->u.mem.start=0x300000000;
params->u.mem.size=0x4000000;
params=tag_next(params);
3.设置命令行标记
命令行就是一个字符串,它被用来控制内核的一些行为。比如“root=/dev/mtdblock 2 init=/linuxrc console=ttySAC0”表示根文件系统在MTD2分区上,系统启动后执行的第一个程序为/linuxrc,控制台为ttySAC0(即第一个串口)
命令行可以在Bootloader中通过命令设置好,然后按如下构造标记传给内核
char *p="root=/dev/mtdblock 2 init =/linuxrc console=ttySAC0";
params->har.tag=ATAG_CMDLINE;
params->hdr.size=(sizeof(struct tag_header)+strlen(p)+1+4>>2);
strcpy(params->u.cmdline.cmdline,p);
params=tag_next(params);
params->hdr.tag=ATAG_NONE;
params->hdr.size=0;
现在Bootloader种类繁多。比如x86上有LILO,GRUB等。对于ARM架构的CPU,有U-Boot,Vivi等。它们各有特点,下面列出Linux开放源代码的Bootloader及其支持的体系架构,如下
Vivi是Mizi公司针对SAMSUNG的ARM架构CPU专门设计的,基本可以直接使用。命令简单方便。不过其初始版本只支持串口下载,速度较慢。在网上出现了各种改进版本;支持网络功能,USB功能,烧写YAFFS文件系统映象等。
U-Boot则支持大多数CPU,可以烧写EXT2,JFFS2文件系统映象,支持串口下载,网络下载,并提供了大量的命令。相对于Vivi,它的使用更复杂,但是可以用来更方便的调试程序。
相信通过这一篇博客的学习,您对Bootloader有了清晰但是初步的认识,下一篇博客将会介绍U-Boot的分析和移植。