详谈嵌入式之Bootloader

电子说

1.3w人已加入

描述

 一、Bootloader 的引入

1.1 Bootloader 的引入

Linux 内核的启动是需要一定的必要条件的,但在 CPU 刚上电启动时,一般连内存控制 器都没有配置过,根本无法在内存中运行程序,更不可能处在 Linux 内核的启动环境中。为 了初始化 CPU 及其他外设,使得 Linux 内核可以在系统主存中跑起来,并让系统符合 Linux 内核启动的必备条件,必须要有一个先于内核运行的程序,他就是所谓的引导加载程序: Bootloader。

Bootloader并不是只有Linux才需要,是几乎所有的运行操作系统的设备都必须具备的。 PC电脑的BIOS就是bootloader的一部分,对于Linux PC来说:Bootloader = BIOS + GRUB/LILO。

1.2 嵌入式 Linux 系统软件结构与分布

一般情况下嵌入式 Linux 系统中软件主要由以下几个部分组成:

1.引导加载程序:其中包括内部ROM中的固化启动代码和bootloader两部分。固化ROM 是厂家在芯片生产时固化,用于引导 bootloader。

2.Linux Kernel 和Drivers。

3.文件系统:包括根文件系统和建立于Flash 内存设备之上的文件系统(ext4、UBI、 CRAMFS 等)。它是提供管理系统的各种配置文件以及系统执行用于应用程序的良好运行环 境的载体。

4.应用程序:用于自定义的应用程序,存放于文件系统之中。  在Flash 存储器中,上面四个部分的分布如图 1 所示:

Linux

图1:嵌入式Linux的软件分布

但是以上只是大部分情况下的分布,也有一些根文件系统可能是 initramfs,被一起压缩到了内核映像里,或者没有 bootloader 参数区等。

 二、Bootloader 介绍

2.1 Bootloader 的功能

Bootloader是在操作系统内核运行之前运行的一段小程序,通过它我们可以初始化硬件设备,从而将系统的软硬件环境带到一个合适的状态,以便为最终调用操作系统内核准备好 正确的环境,最后从别处(Flash、以太网、UART 等)载入内核映像并跳到入口地址运行。

简单的说,Bootloader 就是这么一小段程序,它在系统上电时开始执行,初始化硬件设备、准备好软件环境,最后调用操作系统内核。

可以增强 Bootloader 的功能,比如增加网络功能、从 PC 上通过串口或网络下载文件、 烧写文件、将 Flash 上压缩的文件解压后再运行等,这就是一个功能更为强大的 Bootloader, 也称为 Monitor。实际上,在最终产品中用户并不需要这些功能,他们只是为了方便开发。

2.2 Bootloader 的特点

由于 Bootloader 直接操作硬件,因此它严重依赖于硬件,且依据所引导的操作系统的不同而不同。在嵌入式世界中建立一个通用的 Bootloader 几乎是不可能的,而有可能的是 让一个 Bootloader 代码支持多种不同的架构和操作系统,并让他方便移植。Bootloader的启动过程通常是多阶段的,这样既能够提供复杂的功能,又具有更好的可移植性。

大多数 Bootloader 都包含两种不同的操作模式:本地加载模式和远程下载模式。远程下载模式只对开发人员才有意义。

Bootloader 都映射在 CPU复位后运行的第一条指令的地址处,以保证系统上电或复位 后首先执行 Bootloader。

2.3 Bootloader 的分类

首先区分一下“Bootloader”和“Monitor”的概念:严格的讲,“Bootloader”只是引 导设备并执行主程序的固件;而“Monitor”还提供更多的命令行接口,可进行调试、读写内存、烧写 Flash、配置环境变量等。“Monitor”在嵌入式系统开发过程中可以提供更好的调试功能,开发完成后,就完全设置成一个“Bootloader”了。所以习惯上将他们统称为 Bootloader。表 2.1 为常见的开放源码的 Linux 引导程序。

Linux

表2.1 常见的开放源码 Linux 引导程序

其中 U-Boot 支持大多 CPU,可以烧写 EXT2、JFFS2 文件系统映像,支持串口下载、网络 下载,并提供了大量的命令。

2.4 Bootloader 的启动模式

Bootloader 的主要功能是引导操作系统,但在开发时,通常需要使用各种命令操作 Bootloader,一般通过串口来连接 PC 和开发板,可以在串口上输入各种命令、观察运行结果等。这也只是对开发人员才有意义,用户使用产品时是不用接串口来控制 Bootloader 的。 从这个角度可以将 Bootloader 分为启动加载(Boot loading)模式和下载(Down loading)模 式。

2.4.1 启动加载(Bootloading)模式

这种模式也称为"自主"(Autonomous)模式。也即 Boot Loader 从目标机上的某个固态存储设备上将操作系统加载到 RAM 中运行,整个过程并没有用户的介入。这种模式是 Boot Loader 的正常工作模式,因此在嵌入式产品发布的时侯,Bootloader显然必须工作在这种模 式下。

2.4.2 下载(Downloading)模式

在这种模式下,目标机上的 Boot Loader 将通过串口连接或网络连接等通信手段从主机(Host)下载文件,比如:下载内核映像和根文件系统映像等。从主机下载的文件通常首先 被 Boot Loader 保存到目标机的 RAM 中,然后再被 Boot Loader 写到目标机上的 FLASH 类固 态存储设备中。Boot Loader 的这种模式通常在第一次安装内核与根文件系统时被使用;此 外,以后的系统更新也会使用Boot Loader的这种工作模式。工作于这种模式下的Boot Loader 通常都会向它的终端用户提供一个简单的命令行接口。

像 Blob 或 U-Boot 等这样功能强大的 Boot Loader 通常同时支持这两种工作模式,而且 允许用户在这两种工作模式之间进行切换。比如,Blob 在启动时处于正常的启动加载模式, 但是它会延时 10 秒等待终端用户按下任意键而将 blob 切换到下载模式。如果在 10 秒内没 有用户按键,则 blob 继续启动 Linux 内核。

2.4.3 下载模式之网络启动方式

这种方式开发板不需要配置较大的存储介质(跟无盘工作站有点类似),但是使用这种 启动方式之前,需要把 Bootloader 安装到板上的 EPPROM 或者 Flash 中。Bootloader 通过以 太网接口远程下载 Linux 内核映像或者文件系统到 RAM 中运行。这种方式对于嵌入式系统开发来说非常重要。

使用这种方式的前提条件,就是目标板有串口、以太网接口、USB 接口或者其他链接方式:串口一般作为控制台,同时可以用来下载内核映像和 RAMDISK 文件系统;用网络接口 来挂载 NFS 文件系统;也可以使用 USB 接口虚拟成以太网口来通讯。

使用网络启动方式,还需要在服务器上配置启动相关网络服务:使用 TFTP 服务为 Bootloader 客户端提供文件下载功能;DHCP 服务动态为 Bootloader 设置 IP 地址,配置网络 参数。网络启动方式如图 2.1 所示。

Linux

图2.1 网络方式启动系统

2.5 Bootloader 的启动流程

Bootloader 的启动过程可以分为单阶段(Single Stage)、多阶段(Multi-Stage)两种。通 常多阶段的 Bootloader 能提供更为复杂的功能以及更好的可移植性。从固态存储设备上启动 的Bootloader 大多都是两个阶段的启动过程。第一阶段使用汇编来实现,它完成一些依赖于 CPU 体系结构的初始化,并调用第二阶段的代码;第二阶段则通常使用 C 语言来实现,这样 可以实现更复杂的功能,而且代码会有更好的可读性和可移植性。

一般而言,这两个阶段完成的功能可以如下分类:

2.5.1 Bootloader 第一阶段的功能

Bootloader 在第一阶段主要完成以下功能:

硬件设备初始化;

为加载 Bootloader 的第二阶段代码准备 RAM 空间;

复制 Bootloader 的第二阶段代码到 RAM 空间中;

设置好栈;

跳转到第二阶段代码的 C 入口点;

在第一阶段进行的硬件初始化一般包括:关闭 WATCHDOG、关中断、设置 CPU 的速度 和时钟频率、RAM 初始化等。这些并不都是必须的,比如 S3C2410/S3C2440 的开发板所使 用的 U-Boot 中,就将 CPU 的速度和时钟频率放在第二阶段进行设置。

甚至,将第二阶段的代码复制到 RAM 空间也不是必须的,对于 Nor Flash 等支持 XIP 的 存储设备,完全可以在上面直接执行代码,只不过相比在 RAM 中执行效率大为降低。

2.5.2 Bootloader 第二阶段的功能

Bootloader 在第二阶段主要完成以下功能:

初始化本阶段要使用到的硬件设备;

检测系统内存映射(Memory map);

将内核映像和根文件系统映像从 Flash 上读到 RAM 空间中;

为内核设置启动参数;

调用内核;

为了方便开发,只要要初始化一个串口以便程序员与 Bootloader 进行交互。

所谓检测内存映射,就是确定板上使用了多少内存、他们的地址空间是什么。由于嵌入 式开发中的 Bootloader 多是针对某类板子进行编写,所以可以根据板子的情况直接设置,不 需要考虑可以适用于各类情况的复杂算法。

Flash 上的内核映像有可能是经过压缩的,在读到 RAM 之后,还需要进行解压。当然, 对于有自解压功能的内核,不需要Bootloader 来解压。

将根文件系统映像复制到 RAM 中并不是必须的,这取决于是什么类型的根文件系统,以及内核访问它的方法。

将内核放在适当的位置后,在跳入执行内核之前,需要根据内核启动的需求,配置相应 的启动参数。如 Linux 内核的启动要求如表 2.2 所示。

Linux

Linux

表2.2 Linux 内核启动条件

在 C 语言中,可以像下列示例代码一样来调用内核:

void (*theKernel)(int zero, int arch, u32params_addr) = (void (*)(int, int, u32))KERNEL_RAM_BASE;

theKernel(0,ARCH_NUMBER, (u32)kernel_params_start);

2.6 Bootloader 与内核的交互

U-Boot 与内核之间的交互是单向的,U-Boot 将各类参数传递给内核。由于他们(U-Boot 与内核)不能同时运行,传递的办法只有一个:U-Boot 将参数放在放在某个约定的地方之 后,再启动内核,内核启动后从这个地方获得参数。

除了约定好参数存放的地址外,还要规定参数的结构。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 头文件中(在 U-Boot 的 include/asm-arm/目录下的 setup.h 中也有定义),如下所示:

struct tag_header {

u32 size;

u32 tag;

};

struct tag {

struct tag_header hdr; 

union {  

struct tag_core  core;  

struct tag_mem32 mem;  

struct tag_videotext videotext;  

struct tag_ramdisk ramdisk; 

struct tag_initrd initrd;  

struct tag_serialnr serialnr;  

struct tag_revision revision;  

struct tag_videolfb videolfb;  

struct tag_cmdline cmdline;       

/*   

* Acorn specific   

*/  

struct tag_acorn acorn;       

/*

* DC21285 specific   

*/  

struct tag_memclk memclk; 

} u;

};

下面以设置内存标记、命令标记为例说明参数的传递。

2.6.1 设置标记ATAG_CORE

标记列表以标记 ATAG_CORE 开始,其结构体定义如下:

/* The list must start with an ATAG_COREnode */

#define ATAG_CORE 0x54410001     

struct tag_core { 

u32 flags;  /* bit 0 =read-only */ 

u32 pagesize; 

u32 rootdev;

}; 

假设 Bootloader与内核约定的参数存放地址为 0x30000100,则可以以如下代码设置标 记 ATAG_CORE:

#define tag_next(t) ((struct tag *)((u32*)(t) + (t)->hdr.size))

static struct tag *params;  //定义全局变量 params,为 struct tag 结构

static void setup_start_tag (bd_t *bd)

params = (struct tag *) bd->bi_boot_params; //bi_boot_params =0x30000100

params->hdr.tag = ATAG_CORE; // ATAG_CORE 在前面定义,指定标记类型 

params->hdr.size = tag_size (tag_core);  //计算标记大小    

// 设置 ATAG_CORE 标记的内容 

params->u.core.flags = 0; 

params->u.core.pagesize = 0; 

params->u.core.rootdev = 0;      

params = tag_next (params);

}

其中,tag_next定义为指向当前标记的末尾。

2.6.2 设置内存标记

内存标记 tag_mem32 的定义如下:

struct tag_mem32 { 

u32 size;  // 内存的大小 

u32 start; /* physical start address */

}; 

假设开发板使用的内存起始地址为 0x30000000,大小为 0x4000000,则内存标记可以如 下设置:

static void setup_memory_tags (bd_t *bd)

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);

}

2.6.3 设置命令行标记

命令行就是一个字符串,它被用来控制内核的一些行为。比如“root=/dev/mtdlock2 init=/linuxrc console=ttySAC0”表示根文件系统在 MTD2 分区上,系统启动后执行的第一个程序为/linuxrc,控制台为 ttySAC0。

命令行可以在 Bootloader 中通过命令设置好,然后按如下构造标记传给内核。

char *p = ”root=/dev/mtdlock 2init=/linuxrc console=ttySAC0”;    

params->hdr.tag = ATAG_CMDLINE; 

params->hdr.size = (sizeof (structtag_header) + strlen (p) + 1 + 4) >> 2; 

strcpy (params->u.cmdline.cmdline,p); 

params = tag_next (params);

2.6.4 设置标记ATAG_NONE

标记列表以标记 ATAG_NONE 结束,如下设置:

static void setup_end_tag (bd_t *bd)

params->hdr.tag = ATAG_NONE; 

params->hdr.size = 0;

}

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分