嵌入式设备上的 Linux 系统开发.docx
《嵌入式设备上的 Linux 系统开发.docx》由会员分享,可在线阅读,更多相关《嵌入式设备上的 Linux 系统开发.docx(21页珍藏版)》请在冰点文库上搜索。
嵌入式设备上的Linux系统开发
嵌入式设备上的Linux系统开发
2002年3月
如果您刚接触嵌入式开发,那么大量可用的引导装载程序(bootloader)、规模缩小的分发版(distribution)、文件系统和GUI看起来可能太多了。
但是这些丰富的选项实际上是一种恩赐,允许您调整开发或用户环境以完全符合您的需要。
对Linux嵌入式开发的概述将帮助您理解所有这些选项。
Linux正在嵌入式开发领域稳步发展。
因为Linux使用GPL(请参阅本文后面的参考资料),所以任何对将Linux定制于PDA、掌上机或者可佩带设备感兴趣的人都可以从因特网免费下载其内核和应用程序,并开始移植或开发。
许多Linux改良品种迎合了嵌入式/实时市场。
它们包括RTLinux(实时Linux)、uclinux(用于非MMU设备的Linux)、MontavistaLinux(用于ARM、MIPS、PPC的Linux分发版)、ARM-Linux(ARM上的Linux)和其它Linux系统(请参阅参考资料以链接到本文中提到的这些和其它术语及产品。
)
嵌入式Linux开发大致涉及三个层次:
引导装载程序、Linux内核和图形用户界面(或称GUI)。
在本文中,我们将集中讨论涉及这三层的一些基本概念;深入了解引导装载程序、内核和文件系统是如何交互的;并将研究可用于文件系统、GUI和引导装载程序的众多选项中的一部分。
引导装载程序
引导装载程序通常是在任何硬件上执行的第一段代码。
在象台式机这样的常规系统中,通常将引导装载程序装入主引导记录(MasterBootRecord,(MBR))中,或者装入Linux驻留的磁盘的第一个扇区中。
通常,在台式机或其它系统上,BIOS将控制移交给引导装载程序。
这就提出了一个有趣的问题:
谁将引导装载程序装入(在大多数情况中)没有BIOS的嵌入式设备上呢?
解决这个问题有两种常规技术:
专用软件和微小的引导代码(tinybootcode)。
专用软件可以直接与远程系统上的闪存设备进行交互并将引导装载程序安装在闪存的给定位置中。
闪存设备是与存储设备功能类似的特殊芯片,而且它们能持久存储信息—即,在重新引导时不会擦除其内容。
这个软件使用目标(在嵌入式开发中,嵌入式设备通常被称为目标)上的JTAG端口,它是用于执行外部输入(通常来自主机机器)的指令的接口。
JFlash-linux是一种用于直接写闪存的流行工具。
它支持为数众多的闪存芯片;它在主机机器(通常是i386机器—本文中我们把一台i386机器称为主机)上执行并通过JTAG接口使用并行端口访问目标的闪存芯片。
当然,这意味着目标需要有一个并行接口使它能与主机通信。
Jflash-linux在Linux和Windows版本中都可使用,可以在命令行中用以下命令启动它:
Jflash-linux
某些种类的嵌入式设备具有微小的引导代码—根据几个字节的指令—它将初始化一些DRAM设置并启用目标上的一个串行(或者USB,或者以太网)端口与主机程序通信。
然后,主机程序或装入程序可以使用这个连接将引导装载程序传送到目标上,并将它写入闪存。
在安装它并给予其控制后,这个引导装载程序执行下列各类功能:
∙初始化CPU速度
∙初始化内存,包括启用内存库、初始化内存配置寄存器等
∙初始化串行端口(如果在目标上有的话)
∙启用指令/数据高速缓存
∙设置堆栈指针
∙设置参数区域并构造参数结构和标记(这是重要的一步,因为内核在标识根设备、页面大小、内存大小以及更多内容时要使用引导参数)
∙执行POST(加电自检)来标识存在的设备并报告任何问题
∙为电源管理提供挂起/恢复支持
∙跳转到内核的开始
带有引导装载程序、参数结构、内核和文件系统的系统典型内存布局可能如下所示:
清单1.典型内存布局
/*TopOfMemory*/
Bootloader
ParameterArea
Kernel
Filesystem
/*EndOfMemory*/
嵌入式设备上一些流行的并可免费使用的Linux引导装载程序有Blob、Redboot和Bootldr(请参阅参考资料获得链接)。
所有这些引导装载程序都用于基于ARM设备上的Linux,并需要Jflash-linux工具用于安装。
一旦将引导装载程序安装到目标的闪存中,它就会执行我们上面提到的所有初始化工作。
然后,它准备接收来自主机的内核和文件系统。
一旦装入了内核,引导装载程序就将控制转给内核。
设置工具链
设置工具链在主机机器上创建一个用于编译将在目标上运行的内核和应用程序的构建环境—这是因为目标硬件可能没有与主机兼容的二进制执行级别。
工具链由一套用于编译、汇编和链接内核及应用程序的组件组成。
这些组件包括:
∙Binutils—用于操作二进制文件的实用程序集合。
它们包括诸如ar、as、objdump、objcopy这样的实用程序。
∙Gcc—GNUC编译器。
∙Glibc—所有用户应用程序都将链接到的C库。
避免使用任何C库函数的内核和其它应用程序可以在没有该库的情况下进行编译。
构建工具链建立了一个交叉编译器环境。
本地编译器编译与本机同类的处理器的指令。
交叉编译器运行在某一种处理器上,却可以编译另一种处理器的指令。
重头设置交叉编译器工具链可不是一项简单的任务:
它包括下载源代码、修补补丁、配置、编译、设置头文件、安装以及很多很多的操作。
另外,这样一个彻底的构建过程对内存和硬盘的需求是巨大的。
如果没有足够的内存和硬盘空间,那么在构建阶段由于相关性、配置或头文件设置等问题会突然冒出许多问题。
因此能够从因特网上获得已预编译的二进制文件是一件好事(但不太好的一点是,目前它们大多数只限于基于ARM的系统,但迟早会改变的)。
一些比较流行的已预编译的工具链包括那些来自Compaq(FamiliarLinux)、LART(LARTLinux)和Embedian(基于Debian但与它无关)的工具链—所有这些工具链都用于基于ARM的平台。
内核设置
Linux社区正积极地为新硬件添加功能部件和支持、在内核中修正错误并且及时地进行常规改进。
这导致大约每6个月(或6个月不到)就有一个稳定的Linux树的新发行版。
不同的维护者维护针对特定体系结构的不同内核树和补丁。
当为一个项目选择了一个内核时,您需要评估最新发行版的稳定性如何、它是否符合项目要求和硬件平台、从编程角度来看它的舒适程度以及其它难以确定的方面。
还有一点也非常重要:
找到需要应用于基本内核的所有补丁,以便为特定的体系结构调整内核。
内核布局
内核布局分为特定于体系结构的部分和与体系结构无关的部分。
内核中特定于体系结构的部分首先执行,设置硬件寄存器、配置内存映射、执行特定于体系结构的初始化,然后将控制转给内核中与体系结构无关的部分。
系统的其余部分在这第二个阶段期间进行初始化。
内核树下的目录arch/由不同的子目录组成,每个子目录用于一个不同的体系结构(MIPS、ARM、i386、SPARC、PPC等)。
每一个这样的子目录都包含kernel/和mm/子目录,它们包含特定于体系结构的代码来完成象初始化内存、设置IRQ、启用高速缓存、设置内核页面表等操作。
一旦装入内核并给予其控制,就首先调用这些函数,然后初始化系统的其余部分。
根据可用的系统资源和引导装载程序的功能,内核可以编译成vmlinux、Image或zImage。
vmlinux和zImage之间的主要区别在于vmlinux是实际的(未压缩的)可执行文件,而zImage是或多或少包含相同信息的自解压压缩文件—只是压缩它以处理(通常是Intel强制的)640KB引导时间的限制。
有关所有这些的权威性解释,请参阅LinuxMagazine的文章“KernelConfiguration:
dealingwiththeunexpected”(请参阅参考资料)。
内核链接和装入
一旦为目标系统编译了内核后,通过使用引导装载程序(它已经被装入到目标的闪存中),内核就被装入到目标系统的内存(在DRAM中或者在闪存中)。
通过使用串行、USB或以太网端口,引导装载程序与主机通信以将内核传送到目标的闪存或DRAM中。
在将内核完全装入目标后,引导装载程序将控制传递给装入内核的地址。
内核可执行文件由许多链接在一起的对象文件组成。
对象文件有许多节,如文本、数据、init数据、bass等等。
这些对象文件都是由一个称为链接器脚本的文件链接并装入的。
这个链接器脚本的功能是将输入对象文件的各节映射到输出文件中;换句话说,它将所有输入对象文件都链接到单一的可执行文件中,将该可执行文件的各节装入到指定地址处。
vmlinux.lds是存在于arch//目录中的内核链接器脚本,它负责链接内核的各个节并将它们装入内存中特定偏移量处。
典型的vmlinux.lds看起来象这样:
清单2.典型的vmlinux.lds文件
OUTPUT_ARCH()/*includesarchitecturetype*/
ENTRY(stext)/*stextisthekernelentrypoint*/
SECTIONS/*SECTIONScommanddescribesthelayout
oftheoutputfile*/
{
.=TEXTADDR;/*TEXTADDRisLMAforthekernel*/
.init:
{/*Initcodeanddata*/
_stext=.;/*Firstsectionisstextfollowed
by__initdatasection*/
__init_begin=.;
*(.text.init)
__init_end=.;
}
.text:
{/*Realtextsegmentfollows__init_datasection*/
_text=.;
*(.text)
_etext=.;/*Endoftextsection*/
}
.data:
{
_data=.;/*Datasectioncomesaftertextsection*/
*(.data)
_edata=.;
}/*Datasectionendshere*/
.bss:
{/*BSSsectionfollowssymboltablesection*/
__bss_start=.;
*(.bss)
_end=.;/*BSSsectionendshere*/
}
}
LMA是装入模块地址;它表示将要装入内核的目标虚拟内存中的地址。
TEXTADDR是内核的虚拟起始地址,并且在arch//下的Makefile中指定它的值。
这个地址必须与引导装载程序使用的地址相匹配。
一旦引导装载程序将内核复制到闪存或DRAM中,内核就被重新定位到TEXTADDR—它通常在DRAM中。
然后,引导装载程序将控制转给这个地址,以便内核能开始执行。
参数传递和内核引导
stext是内核入口点,这意味着在内核引导时将首先执行这一节下的代码。
它通常用汇编语言编写,并且通常它在arch//内核目录下。
这个代码设置内核页面目录、创建身份内核映射、标识体系结构和处理器以及执行分支start_kernel(初始化系统的主例程)。
start_kernel调用setup_arch作为执行的第一步,在其中完成特定于体系结构的设置。
这包括初始化硬件寄存器、标识根设备和系统中可用的DRAM和闪存的数量、指定系统中可用页面的数目、文件系统大小等等。
所有这些信息都以参数形式从引导装载程序传递到内核。
将参数从引导装载程序传递到内核有两种方法:
parameter_structure和标记列表。
在这两种方法中,不赞成使用参数结构,因为它强加了限制:
指定在内存中,每个参数必须位于param_struct中的特定偏移量处。
最新的内核期望参数作为标记列表的格式来传递,并将参数转化为已标记格式。
param_struct定义在include/asm/setup.h中。
它的一些重要字段是:
清单3.样本参数结构
structparam_struct{
unsignedlongpage_size;/*0:
Sizeofthepage*/
unsignedlongnr_pages;/*4:
Numberofpagesinthesystem*/
unsignedlongramdisk/*8:
ramdisksize*/
unsignedlongrootdev;/*16:
Numberrepresentingtherootdevice*/
unsignedlonginitrd_start;/*64:
startingaddressofinitialramdisk*/
/*Thiscanbeeitherinflash/dram*/
unsignedlonginitrd_size;/*68:
sizeofinitialramdisk*/
}
请注意:
这些数表示定义字段的参数结构中的偏移量。
这意味着如果引导装载程序将参数结构放置在地址0xc0000100,那么rootdev参数将放置在0xc0000100+16,initrd_start将放置在0xc0000100+64等等—否则,内核将在解释正确的参数时遇到困难。
正如上面提到的,因为从引导装载程序到内核的参数传递会有一些约束条件,所以大多数2.4.x系列内核期望参数以已标记的列表格式传递。
在已标记的列表中,每个标记由标识被传递参数的tag_header以及其后的参数值组成。
标记列表中标记的常规格式可以如下所示:
清单4.样本标记格式。
内核通过头来标识每个标记。
#define
struct{
u32;
u32;
};
/*Exampletagforpassingmemoryinformation*/
#defineATAG_MEM0x54410002/*Magicnumber*/
structtag_mem32{
u32size;/*sizeofmemory*/
u32start;/*physicalstartaddressofmemory*/
};
setup_arch还需要对闪存存储库、系统寄存器和其它特定设备执行内存映射。
一旦完成了特定于体系结构的设置,控制就返回到初始化系统其余部分的start_kernel函数。
这些附加的初始化任务包含:
∙设置陷阱
∙初始化中断
∙初始化计时器
∙初始化控制台
∙调用mem_init,它计算各种区域、高内存区等内的页面数量
∙初始化slab分配器并为VFS、缓冲区高速缓存等创建slab高速缓存
∙建立各种文件系统,如proc、ext2和JFFS2
∙创建kernel_thread,它执行文件系统中的init命令并显示lign提示符。
如果在/bin、/sbin或/etc中没有init程序,那么内核将执行文件系统的/bin中的shell。
设备驱动程序
嵌入式系统通常有许多设备用于与用户交互,象触摸屏、小键盘、滚动轮、传感器、RA232接口、LCD等等。
除了这些设备外,还有许多其它专用设备,包括闪存、USB、GSM等。
内核通过所有这些设备各自的设备驱动程序来控制它们,包括GUI用户应用程序也通过访问这些驱动程序来访问设备。
本节着重讨论通常几乎在每个嵌入式环境中都会使用的一些重要设备的设备驱动程序。
帧缓冲区驱动程序
这是最重要的驱动程序之一,因为通过这个驱动程序才能使系统屏幕显示内容。
帧缓冲区驱动程序通常有三层。
最底层是基本控制台驱动程序drivers/char/console.c,它提供了文本控制台常规接口的一部分。
通过使用控制台驱动程序函数,我们能将文本打印到屏幕上—但图形或动画还不能(这样做需要使用视频模式功能,通常出现在中间层,也就是drivers/video/fbcon.c中)。
这个第二层驱动程序提供了视频模式中绘图的常规接口。
帧缓冲区是显卡上的内存,需要将它内存映射到用户空间以便可以将图形和文本能写到这个内存段上:
然后这个信息将反映到屏幕上。
帧缓冲区支持提高了绘图的速度和整体性能。
这也是顶层驱动程序引人注意之处:
顶层是非常特定于硬件的驱动程序,它需要支持显卡不同的硬件方面—象启用/禁用显卡控制器、深度和模式的支持以及调色板等。
所有这三层都相互依赖以实现正确的视频功能。
与帧缓冲区有关的设备是/dev/fb0(主设备号29,次设备号0)。
输入设备驱动程序
可触摸板是用于嵌入式设备的最基本的用户交互设备之一—小键盘、传感器和滚动轮也包含在许多不同设备中以用于不同的用途。
触摸板设备的主要功能是随时报告用户的触摸,并标识触摸的坐标。
这通常在每次发生触摸时,通过生成一个中断来实现。
然后,这个设备驱动程序的角色是每当出现中断时就查询触摸屏控制器,并请求控制器发送触摸的坐标。
一旦驱动程序接收到坐标,它就将有关触摸和任何可用数据的信号发送给用户应用程序,并将数据发送给应用程序(如果可能的话)。
然后用户应用程序根据它的需要处理数据。
几乎所有输入设备—包括小键盘—都以类似原理工作。
闪存MTD驱动程序
MTD设备是象闪存芯片、小型闪存卡、记忆棒等之类的设备,它们在嵌入式设备中的使用正在不断增长。
MTD驱动程序是在Linux下专门为嵌入式环境开发的新的一类驱动程序。
相对于常规块设备驱动程序,使用MTD驱动程序的主要优点在于MTD驱动程序是专门为基于闪存的设备所设计的,所以它们通常有更好的支持、更好的管理和基于扇区的擦除和读写操作的更好的接口。
Linux下的MTD驱动程序接口被划分为两类模块:
用户模块和硬件模块。
用户模块
这些模块提供从用户空间直接使用的接口:
原始字符访问、原始块访问、FTL(闪存转换层,FlashTransitionLayer—用在闪存上的一种文件系统)和JFS(即日志文件系统,JournaledFileSystem—在闪存上直接提供文件系统而不是模拟块设备)。
用于闪存的JFS的当前版本是JFFS2(稍后将在本文中描述)。
硬件模块
这些模块提供对内存设备的物理访问,但并不直接使用它们。
通过上述的用户模块来访问它们。
这些模块提供了在闪存上读、擦除和写操作的实际例程。
MTD驱动程序设置
为了访问特定的闪存设备并将文件系统置于其上,需要将MTD子系统编译到内核中。
这包括选择适当的MTD硬件和用户模块。
当前,MTD子系统支持为数众多的闪存设备—并且有越来越多的驱动程序正被添加进来以用于不同的闪存芯片。
有两个流行的用户模块可启用对闪存的访问:
MTD_CHAR和MTD_BLOCK。
MTD_CHAR提供对闪存的原始字符访问,而MTD_BLOCK将闪存设计为可以在上面创建文件系统的常规块设备(象IDE磁盘)。
与MTD_CHAR关联的设备是/dev/mtd0、mtd1、mtd2(等等),而与MTD_BLOCK关联的设备是/dev/mtdblock0、mtdblock1(等等)。
由于MTD_BLOCK设备提供象块设备那样的模拟,通常更可取的是在这个模拟基础上创建象FTL和JFFS2那样的文件系统。
为了进行这个操作,可能需要创建分区表将闪存设备分拆到引导装载程序节、内核节和文件系统节中。
样本分区表可能包含以下信息:
清单5.MTD的简单闪存设备分区
structmtd_partitionsample_partition={
{
/*Firstpartition*/
name:
bootloader,/*Bootloadersection*/
size:
0x00010000,/*Size*/
offset:
0,/*Offsetfromstartofflash-location0x0*/
mask_flags:
MTD_WRITEABLE/*Thispartitionisnotwritable*/
},
{/*Secondpartition*/
name:
Kernel,/*Kernelsection*/
size:
0x00100000,/*Size*/
offset:
MTDPART_OFS_APPEND,/*Appendafterbootloadersection*/
mask_flags:
MTD_WRITEABLE/*Thispartitionisnotwritable*/
},
{/*Thirdpartition*/
name:
JFFS2,/*JFFS2filesystem*/
size:
MTDPART_SIZ_FULL,/*Occupyrestofflash*/
offset:
MTDPART_OFS_APPEND/*Appendafterkernelsection*/
}
}
上面的分区表使用了MTD_BLOCK接口对闪存设备进行分区。
这些分区的设备节点是:
简单闪存分区的设备节点
UserdevicenodeMajornumberMinornumber
Bootloader/dev/mtdblock0310
Kernel/dev/mtdblock1311
Filesystem/dev/mtdblock2312
在本例中,引导装载程序必须将有关root设备节点(/dev/mtdblock2)和可以在闪存中找到文件系统的地址(本例中是FLASH_BASE_ADDRESS+0x04000000)的正确参数传递到内核。
一旦完成分区,闪存设备就准备装入或挂装文件系统。
Linux中MTD子系统的主要目标是在系统的硬件驱动程序和上层,或用户模块之间提供通用接口。
硬件驱动程