一、Bootloader的引入
从前面的硬件实验可以知道,系统上电之后,需要一段程序来进行初始化:关闭
WATCHDOG、改变系统时钟、初始化存储控制器、将更多的代码复制到内存中等等。如果它能将操作系统内核(无论从本地,比如Flash;还是从远端,
比如通过网络)复制到内存中运行,就称这段程序为Bootloader。
简单地说,Bootloader就是这么一小段程序,它在系统上电时开始执行,初始化硬件设备、准备好软件环境,最后调用操作系统内核。
可以增强Bootloader的功能,比如增加网络功能、从PC上通过串口或网络下载文件、
烧写文件、将Flash上压缩的文件解压后再运行等──这就是一个功能更为强大的Bootloader,也称为Monitor。实际上,在最终产品中用户
并不需要这些功能,它们只是为了方便开发。
Bootloader的实现严重依赖于具体硬件,在嵌入式系统中硬件配置千差万别,即使是相
同的CPU,它的外设(比如Flash)也可能不同,所以不可能有一个Bootloader支持所有的CPU、所有的电路板。即使是支持CPU架构比较多
的U-Boot,也不是一拿来就可以使用的(除非里面的配置刚好与你的板子相同),需要进行一些移植。
二、 Bootloader的启动方式
CPU上电后,会从某个地址开始执行。比如MIPS结构的CPU会从0xBFC00000取
第一条指令,而ARM结构的CPU则从地址0x0000000开始。嵌入式单板中,需要把存储器件ROM或Flash等映射到这个地
址,Bootloader就存放在这个地址开始处,这样一上电就可以执行。
在开发时,通常需要使用各种命令操作Bootloader,一般通过串口来连接PC和开发
板,可以在串口上输入各种命令、观察运行结果等。这也只是对开发人员才有意义,用户使用产品时是不用接串口来控制Bootloader的。从这个观点来
看,Bootloader可以分为两种操作模式(Operation Mode):
(1)启动加载(Boot loading)模式。
上电后,Bootloader从板子上的某个固态存储设备上将操作系统加载到RAM中运行,整个过程并没有用户的介入。产品发布时,Bootloader工作在这种模式下。
(2)下载(Downloading)模式。
在这种模式下,开发人员可以使用各种命令,通过串口连接或网络连接等通信手段从主机(Host)下载文件(比如内核映像、文件系统映像),将它们直接放在内存运行或是烧入Flash类固态存储设备中。
板子与主机间传输文件时,可以使用串口的xmodem/ymodem/zmodem协议,它们使用简单,只是速度比较慢;还可以使用网络通过tftp、nfs协议来传输,这时,主机上要开启tftp、nfs服务;还有其他方法,比如USB等。
像Blob或U-Boot等这样功能强大的Bootloader通常同时支持这两种工作模
式,而且允许用户在这两种工作模式之间进行切换。比如,U-Boot在启动时处于正常的启动加载模式,但是它会延时若干秒(这可以设置)等待终端用户按下
任意键而将U-Boot切换到下载模式。如果在指定时间内没有用户按键,则U-Boot继续启动Linux内核。 编辑] 15.1.2 Bootloader的结构和启动过程
- 1. 概述
在移植之前先了解Bootloader的一些通用概念,对理解它的代码会有所帮助。
在一个嵌入式Linux系统中,从软件的角度通常可以分为4个层次:
(1)引导加载程序,包括固化在固件(firmware)中的 boot 代码(可选)和Bootloader两大部分。
有些CPU在运行Bootloader之前先运行一段固化的程序(固件,firmware),比如x86结构的CPU就是先运行BIOS中的固件,然后才运行硬盘第一个分区(MBR)中的Bootloader。
在大多嵌入式系统中并没有固件,Bootloader是上电后执行的第一个程序。
(2)Linux内核。
特定于嵌入式板子的定制内核以及内核的启动参数。内核的启动参数可以是内核默认的,或是由Bootloader传递给它的。
(3)文件系统。
包括根文件系统和建立于Flash内存设备之上的文件系统。里面包含了Linux系统能够运行所必需的应用程序、库等,比如可以给用户提供操作Linux的控制界面的shell程序,动态连接的程序运行时需要的glibc或uClibc库,等等。
(4)用户应用程序。
特定于用户的应用程序,它们也存储在文件系统中。有时在用户应用程序和内核层之间可能还会包括一个嵌入式图形用户界面。常用的嵌入式 GUI 有:Qtopia 和 MiniGUI 等。
显然,在嵌入系统的固态存储设备上有相应的分区来存储它们,图15.1是一个典型的分区结构。 [[Image:]]
图15.1 嵌入式Linux系统中的典型分区结构
“Boot
parameters”分区中存放一些可设置的参数,比如IP地址、串口波特率、要传递给内核的命令行参数等。正常启动过程中,Bootloader首先
运行,然后它将内核复制到内存中(也有些内核可以在固态存储设备上直接运行),并且在内存某个固定的地址设置好要传递给内核的参数,最后运行内核。内核启
动之后,它会挂接(mount)根文件系统(“Root filesystem”),启动文件系统中的应用程序。
- 2. Bootloader的两个阶段
Bootloader的启动过程启动过程可以分为单阶段(Single
Stage)、多阶段(Multi-Stage)两种。通常多阶段的Bootloader能提供更为复杂的功能,以及更好的可移植性。从固态存储设备上启
动的Bootloader大多都是 2 阶段的启动过程。这从前面的硬件实验可以很好地理解这点:第一阶段使用汇编来实现,它完成一些依赖于 CPU
体系结构的初始化,并调用第二阶段的代码。第二阶段则通常使用C语言来实现,这样可以实现更复杂的功能,而且代码会有更好的可读性和可移植性。
一般而言,这两个阶段完成的功能可以如下分类,但这不是绝对的:
(1)Bootloader第一阶段的功能。
- 硬件设备初始化。
- 为加载Bootloader的第二阶段代码准备RAM空间。
- 拷贝Bootloader的第二阶段代码到 RAM 空间中。
- 设置好栈。
- 跳转到第二阶段代码的C入口点。
在第一阶段进行的硬件初始化一般包括:关闭WATCHDOG、关中断、设置CPU的速度和时钟频率、RAM初始化等。这些并不都是必需的,比如S3C2410/S3C2440的开发板所使用的U-Boot中,就将CPU的速度和时钟频率的设置放在第二阶段。
甚至,将第二阶段的代码复制到RAM空间中也不是必需的,对于NOR Flash等存储设备,完全可以在上面直接执行代码,只不过这相比在RAM中执行效率大为降低。
(2)Bootloader第二阶段的功能。
- 初始化本阶段要使用到的硬件设备。
- 检测系统内存映射(memory map)。
- 将内核映像和根文件系统映像从Flash上读到RAM空间中。
- 为内核设置启动参数。
- 调用内核。
为了方便开发,至少要初始化一个串口以便程序员与Bootloader进行交互。
所谓检测内存映射,就是确定板上使用了多少内存,它们的地址空间是什么。由于嵌入式开发中,Bootloader多是针对某类板子进行编写,所以可以根据板子的情况直接设置,不需要考虑可以适用于各类情况的复杂算法。
Flash上的内核映像有可能是经过压缩的,在读到RAM之后,还需要进行解压。当然,对于有自解压功能的内核,不需要Bootloader来解压。
将根文件系统映像复制到RAM中,这不是必需的。这取决于是什么类型的根文件系统,以及内核访问它的方法。
为内核设置启动参数将在下一小节介绍。
将内核存放在适当的位置后,直接跳到到它的入口点即可调用内核。调用内核之前,下列条件要满足:
(1)CPU 寄存器的设置。
- R0=0
- R1=机器类型ID;对于ARM结构的CPU,其机器类型ID可以参见 linux/arch/arm/tools/mach-types。
- R2=启动参数标记列表在 RAM 中起始基地址
(2)CPU工作模式。
- 必须禁止中断(IRQs和FIQs)
- CPU 必须 SVC 模式
(3)Cache 和 MMU 的设置。
- MMU 必须关闭
- 指令 Cache 可以打开也可以关闭
- 数据 Cache 必须关闭
如果用C语言,可以像下列示例代码一样来调用内核:
void (*theKernel)(int zero, int arch, u32
params_addr) = (void (*)(int, int, u32))KERNEL_RAM_BASE; ……
theKernel(0, ARCH_NUMBER, (u32) kernel_params_start);
- 3. Bootloader与内核的交互
Bootloader与内核的交互是单向的,Bootloader将各类参数传给内核。由于它们不能同时运行,传递办法只有一个:Bootloader将参数放在某个约定的地方之后,再启动内核,内核启动后从这个地方获得参数。
除了约定好参数存放的地址外,还要规定参数的结构。Linux 2.4.x
以后的内核都期望以标记列表(tagged
list)的形式来传递启动参数。标记,就是一种数据结构;标记列表,就是挨着存放的多个标记。标记列表以标记ATAG_CORE
开始,以标记ATAG_NONE
结束。标记的数据结构为tag,它由一个tag_header结构和一个联合(union)组成。tag_header结构表示标记的类型及长度,比如是
表示内存还是表示命令行参数等。对于不同类型的标记使用不同的联合(union),比如表示内存时使用tag_mem32,表示命令行时使用
tag_cmdline。数据结构tag和tag_header定义在Linux内核源码的include/asm/setup.h头文件中:
struct tag_header { u32 size; u32 tag; };
struct tag { struct tag_header hdr; union { struct
tag_corecore; struct tag_mem32mem; struct tag_videotextvideotext;
struct tag_ramdiskramdisk; struct tag_initrdinitrd; struct
tag_serialnrserialnr; struct tag_revisionrevision; struct
tag_videolfbvideolfb; struct tag_cmdlinecmdline;
struct tag_acornacorn;
struct tag_memclkmemclk; } u; };
下面以设置内存标记、命令行标记为例说明参数的传递:
(1)设置标记 ATAG_CORE。
标记列表以标记 ATAG_CORE开始,假设Bootloader与内核约定的参数存放地址为0x30000100,则可以以如下代码设置标记 ATAG_CORE:
params = (struct tag *) 0x30000100;
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)设置内存标记。
假设开发板使用的内存起始地址为0x30000000,大小为0x4000000,则内存标记可以如下设置:
params->hdr.tag = ATAG_MEM;
params->hdr.size = tag_size (tag_mem32);
params->u.mem.start = 0x30000000;
params->u.mem.size = 0x4000000;
params = tag_next (params);
(3)设置命令行标记。
命令行就是一个字符串,它被用来控制内核的一些行为。比如"root=/dev
/mtdblock2 init=/linuxrc
console=ttySAC0"表示根文件系统在MTD2分区上,系统启动后执行的第一个程序为/linuxrc,控制台为ttySAC0(即第一个串
口)。
命令行可以在Bootloader中通过命令设置好,然后如下构造标记传给内核:
char *p = "root=/dev/mtdblock2 init=/linuxrc console=ttySAC0";
params->hdr.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);
(4)设置标记ATAG_NONE。
标记列表以标记ATAG_NONE结束,如下设置:
params->hdr.tag = ATAG_NONE;
params->hdr.size = 0;
常用Bootloader介绍
现在Bootloader种类繁多,比如x86上有LILO、GRUB等。对于ARM架构的CPU,有U-Boot、Vivi等。它们各有特点,下面列出Linux的开放源代码的Bootloader及其支持的体系架构,如表15.1所示。
开放源码的Linux引导程序
Bootloader
Monitor
描述
X86
ARM
PowerPC
LILO
否
Linux磁盘引导程序
是
否
否
GRUB
否
GNU的LILO替代程序
是
否
否
Loadlin
否
从DOS引导Linux
是
否
否
ROLO
否
从ROM引导Linux而不需要BIOS
是
否
否
Etherboot
否
通过以太网卡启动Linux系统的固件
是
否
否
LinuxBIOS
否
完全替代BUIS的Linux引导程序
是
否
否
BLOB
是
LART等硬件平台的引导程序
否
是
否
U-Boot
是
通用引导程序
是
是
是
RedBoot
是
基于eCos的引导程序
是
是
是
Vivi
是
Mizi公司针对SAMSUNG的ARM CPU设计的引导程序
否
是
否
对于本书使用的S3C2410/S3C2440开发板,U-Boot和Vivi是两个好选
择。Vivi是Mizi公司针对SAMSUNG的ARM架构CPU专门设计的,基本上可以直接使用,命令简单方便。不过其初始版本只支持串口下载,速度较
慢。在网上出现了各种改进版本:支持网络功能、USB功能、烧写YAFFS文件系统映像等。U-Boot则支持大多CPU,可以烧写EXT2、JFFS2
文件系统映像,支持串口下载、网络下载,并提供了大量的命令。相对于Vivi,它的使用更复杂,但是可以用来更方便地调试程序。
2 U-Boot分析与移植
2.1 U-Boot工程简介
U-Boot,全称为Universal Boot
Loader,即通用Bootloader,是遵循GPL条款的开放源代码项目。其前身是由德国DENX软件工程中心的Wolfgang
Denk基于8xxROM的源码创建的PPCBOOT工程。后来整理代码结构使得非常容易增加其他类型的开发板、其他架构的CPU(原来只支持
PowerPC);增加更多的功能,比如启动Linux、下载S-Record格式的文件、通过网络启动、通过PCMCIA/CompactFLash
/ATA disk/SCSI等方式启动。增加ARM架构CPU及其他更多CPU的支持后,改名为U-Boot。
它的名字“通用”有两层含义:可以引导多种操作系统、支持多种架构的CPU。它支持如下操作
系统:Linux、NetBSD、
VxWorks、QNX、RTEMS、ARTOS、LynxOS等,支持如下架构的CPU:PowerPC、MIPS、x86、ARM、NIOS、
XScale等。
U-Boot有如下特性:
- 开放源码;
- 支持多种嵌入式操作系统内核,如Linux、NetBSD、VxWorks、QNX、RTEMS、ARTOS、LynxOS;
- 支持多个处理器系列,如PowerPC、ARM、x86、MIPS、XScale;
- 较高的可靠性和稳定性;
- 高度灵活的功能设置,适合U-Boot调试、操作系统不同引导要求、产品发布等;
- 丰富的设备驱动源码,如串口、以太网、SDRAM、FLASH、LCD、NVRAM、EEPROM、RTC、键盘等;
- 较为丰富的开发调试文档与强大的网络技术支持;
- 支持NFS挂载、RAMDISK(压缩或非压缩)形式的根文件系统
- 支持NFS挂载、从FLASH中引导压缩或非压缩系统内核;
- 可灵活设置、传递多个关键参数给操作系统,适合系统在不同开发阶段的调试要求与产品发布,尤对Linux支持最为强劲;
- 支持目标板环境变量多种存储方式,如FLASH、NVRAM、EEPROM;
- CRC32校验,可校验FLASH中内核、RAMDISK镜像文件是否完好;
- 上电自检功能:SDRAM、FLASH大小自动检测;SDRAM故障检测;CPU型号;
- 特殊功能:XIP内核引导;
可以从http://sourceforge.net/projects/u-boot获得U-Boot的最新版本,如果使用过程中碰到问题或是发现Bug,可以通过邮件列表网站http://lists.sourceforge.net/lists/listinfo/u-boot-users/获得帮助。
最新的更新代码地址http://www.denx.de/wiki/U-Boot/WebHome