客制化嵌入式操作系统

本文作者:admin       点击: 2011-07-15 00:00
前言:
前言

嵌入式系统顾名思义,是指完全嵌入于受控装置中,提供单一或少数特定功能的系统。通常系统的反应时间也有限制,必须在接受命令之后快速做出反应。
Linux 是开放原始码的操作系统 kernel,而 Embedded Linux 则是指运行于嵌入式系统中的Linux。Embedded Linux 搭配特定的应用程序,组成了十分热门的嵌入式操作系统。

本文将介绍如何在极短的时间内,搭建出一套基于 ARM 平台的嵌入式操作系统。操作系统建置完成后,便能着手测试硬件效能,或是快速搭建产品的原型。

开机流程

我们搭建的嵌入式操作系统,其开机步骤如下:
1. 启动 bootloader
2. Bootloader 载入 kernel
3. Kernel 挂载文件系统
4. 执行系统初始化程序
5. 操作系统启动完成,开始等待使用者的命令

接下来我们将详细说明,如何快速搭建一个嵌入式操作系统。

Bootloader 

相信时常接触嵌入式系统的读者对于 bootloader 这个名词已不陌生,若要在一套硬件平台建立完整的软件系统,bootloader 往往是第一个必须要搞定的工具。有些读者可能认为 bootloader 其实就是 x86 上的 BIOS,这种说法其实不是完全正确的,相同的点为他们都是在开机时执行的第一支程序,等于是硬件的开拓者,但与 BIOS 不同的地方为当 bootloader 将 kernel 加载到内存后便将执行权移交给 OS,之后便完全的离开,而在 x86 上 OS 对于硬件的存取仍然需要 BIOS 的协助,因此在角色的定位上,两者仍然是有些许的差异。在本节中我们将对 bootloader 的任务、运作原理及使用方法作简单的介绍。

Bootloader的任务与运作原理

各位可以想象一下,当使用者按下电源钮后,Power通过给每个硬件部件时,整个世界是多么的混乱吗?CPU 这时就像刚出生的婴儿一般懵懂无知,若没有一个工具去帮它对周边的环境作一个初步的整理,我想刚诞生的世界瞬间就步入灭亡了吧!Bootloader 就是负责这件工作的利器,当 CPU 一开始运作时周边所有的硬件部件都是要经过初始化才能使用的,当我们要在完美的环境写一个 C 程序印出一行信息我想大部分的读者应该10 秒内就可以写完,但如果没有内存、没有堆栈时这就不是两三行程序代码可以解决的,因此 bootloader 在开机时便成为开拓者的角色,它在设计时要考虑的非常多,简单来说就是要在有限的资源中完成所有预期的工作。通常 CPU 执行的第一条指令地址都是固定的,或者可以由板子上的跳线(Jumper)作设定,而这个指令便是bootloader的进入点,而此指令通常会是一个跳跃,前往其程序主体,接者对clock及power作设定后再初始化一小块内存使用,在此之前的程序都是由汇编语言(assembly)撰写,当中的流程及所有缓存器存取都必须按照该芯片data sheet中的规范,同时程序大小也必须精准计算,否则系统很容易死当。当内存初始化有堆栈后便可使用C语言来撰写接下来的工作,而完成所有硬件初始化时,bootloader便可将我们所要执行的程序映像文件(kernel image)由指定的地址加载内存中,之后便可功成身退了。

Bootloader除了硬件初始化及加载外,还有一个重要的角色便是从host端传输kernel image或者是文件系统(root filesystem)到target板子上。现在的bootloader加入了越来越多支持,其中最重要的是网络,许多bootloader能够透过DHCP、BOOTP让网络相关的设定可以自动化,同时支持tftp让使用者可以快速的将刚编译好的kernel image从host端加载,进行实时的开启并除错。Bootloader通常会提供一组命令接口让用户操控,让嵌入式系统相较于以前更容易入门,使其成为开发者的好伙伴。

而世界上的bootloader百百种,在嵌入式系统中能见度最高的便是鼎鼎大名的U-Boot,而它成功的原因在于移植性佳,相较于其它bootloader,U-Boot是较容易移植的,而且有非常多的高手努力的将U-Boot移植到各式各样的平台,虽说容易,但仍然比一般程序复杂,在移植前必须对软硬件知识皆有一定程度的认知,同时必须熟读该平台所提供的data sheet。厂商体认到了这个难度,因此买一块开发板时所附的BSP中,便会提供厂商针对该硬件修改好的bootloader,通常只要直接拿来使用便可,但若真的要自己移植,也可在U-Boot中找最相近平台的配置文件与程序代码作修改,而修改的多寡则是要看硬件的差异程度,在过程中遇到困难也可以上U-Boot的mailing list与网络上的高手作讨论。

使用方法

接下来我们以U-Boot为例子简单的介绍一下bootloader的使用方式,通常开发板在出货时就会烧写一份bootloader在其中,若没有的话则是在BSP中,再透过开发板文件中的说明方式烧入板子。假设我们今天要从host端加载一个kernel image再烧入到板子中,以下为简单的操作流程:
1. 设定好host端tftp server的环境。
2. 利用RS232与板子连接,baud rate设定必须跟板子上一致。
3. 开启板子,此时可能有一个倒数,若我们按下任意键便可进入命令接口的模式。
4. 键入printenv指令便可列出目前所有环境变量的设定值,我们可利用setenv指令加上环境变量的名称去设定其值。
5. 首先对网络作设定:
setenv ipaddr xxx.xxx.xxx.xxx 此为target端的ip
setenv serverip xxx.xxx.xxx.xx 此为server端的ip
相同方式我们可设定netmask及gateway等信息
6. 接下来使用tftp将kernel加载target端的内存中
tftp virtual_address img_filename
接下来我们可以选择直接开启或者是写入flash中
直接开启,则键入:bootm virtual_address
若是要写入NAND flash中则键入类似以下指令
nand erase block_address size
nand write virtual_address block_address size
7.使用同样的方式,我们也可以将root filesystem写入NAND flash中。
8.若想知道此板本的U-boot有支持那些指令及使用方式,也可在命令接口键入help,屏幕上便会列出所有信息。

其实除了利用tftp加载kernel外,有些板子也可支持将kernel image 和root filesystem放在SD中由bootloader加载执行,其使用方式读者也可参考开发板的说明文件。最后设定bootarg及bootcmd等环境变量后便可顺利将板子成功启动,当看到「uncompressing linux.................. done, booting the kernel……」就表示各位读者已距离成功不远了。

系统核心:Linux

核心的选择、组态、编译、安装
当我们决定要让自己的开发版上面运行Linux OS,我们需要去客制化属于自己的核心(customize),否则若你的核心不支持目标版的某个硬件部件,核心运作时,你的硬件部件是毫无用处的。

所以在为了让自己的板子能够正确的动作之前,我们需要重新编译核心,我们常说这个动作是交叉编译(cross-compile),原因在于我们的目标板几乎不可能跟我们的host是同一个型态的CPU。

核心选择

要取得核心,当然最直接的方式就是从 http://www.kernel.org/下载我们想要的核心版本。但是没有经过特殊的组态选择以及相关目标板的patching,通常这是无法正常开机的。除非你使用的是相当常见的公板(Samsung series、OMAP series etc…),但是那还是存在着无法让你的核心easy to boot的风险。最简单的方式是从BSP(Board Support Package)中取得你想要的核心与相关的patch file。提到了BSP,在这边我们简略说明一下。BSP 是硬件开发厂商,为了让应用开发人员,快速研发产品的一个必备套件。目的当然是 time to market。一个好的硬件商,BSP大概会有
● 可执行文件:bootloader、pre-built kernel
● 原始码:kernel source code以及相关核心patch files、bootloader source code、应用程序demo code
● 预先编译(Pre-built)好的 root filesystem
● 甚至会提供相关烧写flash的应用程序。

现在我们选好了我们要的核心,可以开始做编译了。

交叉编译的设定

有些厂商的BSP会提供自己优化过的gcc编译程序,请使用他们的编译程序,否则你将会遭遇到一些特殊的bugs。如果厂商没有提供的话,请到网站去下载常用的cross-compile:http://www.codesourcery.com/。在这边我下载常用的版本为Sourcery G++ Lite 2007q3-51 for ARM GNU/Linux,并安装在自己的host OS:
tarjxfarm-2007q3-51-arm-none-linux-gnueabi-i686-pc-linux-gnu.tar.bz2
默认我的解压缩目录夹是:/home/iii/cross。此时你的cross-tool将会解压缩在/home/iii/cross/arm-2007q3。
接下来设定环境变量,这是在交叉编译时最重要的一步。您可以写在~/.bashrc 里面,这样OS 启动时,会自动加载这些环境变量,或是写成一个script 去记录:
#!/bin/sh
export PATH="$PATH":/home/iii/cross/arm-2007q3/bin
export CROSS_COMPILE=arm-none-linux-gnueabi- ARCH=arm
source ~/.basrc or your script

环境变量已经设置好了,接下来是核心的组态。

核心组态

解压缩你的kernel source tree,在最上层输入 make menuconfig,进去主选单。值得一提的是,若你在之前的步骤尚未设定环境变量,或是你想享受打字的快感。这时候你可以输入:
make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabi- menuconfig
这也可以达到之前的目的。接下来你会看到有许多功能需要勾选,如果你对此硬件不熟悉,你也不知道要选择什么组态,这将会是一个耗时费工的地方(因为你必须try and error)。通常我们会借助BSP的帮助,default config帮你搞定大多的选择。
make Target-default-config
这些config档,我们可以在SOURCETREE_DIR/arch/arm/configs底下找到跟我们的目标板名称类似的。我们也可以直接覆盖掉.config来完成我们的目的:cp /arch/arm/configs/Target-default-config .config

核心编译

接下来编译zImage、bzImage(big size zImage)、uImage(header append)以及驱动程序modules,并安装到我们想要的目录夹。
make clean;make zImage;make modules

核心安装

笔者习惯先让bootloader(U-boot为例)下载server上的kernel来开机,主要是因为尚未得知kernel是否有其他bugs。
所以先安装server,以tftp server为例:
sudo apt-get install tftp tftpd openbsd-inetd
mkdir -m 755 /tftpboot(建立共享文件夹)
放入编译好的核心到文件夹
cp /arch/arm/boot/zImage /tftpboot
安装模块到指定的root filesystem底下
make INSTALL_MOD_PATH=/home/iii/ rootfs modules_install
修改service设定,加入tftp service:sudo vim /etc/inetd.conf
内容像是这样:
tftp dgram udp wait nobody /usr/sbin/tcpd /usr/sbin/in.tftpd /tftpboot
重启tftp server
sudo /etc/init.d/openbsd-inetd restart
实机测试
设定bootloader从tftp server下载核心开机:
tftp ${kerneladdr} zImage(下载到预定的memory address,请参考BSP的文件)
bootm ${kerneladdr}(从内存启动核心)

接下来就会进入开机程序,当然如果你没有在bootloader设置rootfilesystem的位置,最后一定会是kernel panic。所以请先设置root filesystem(可使用BSP的pre-built rootfilesystem做测试)。
设置方式如下,这里不再多加解释:
set bootargs root=/dev/mtdblock2 console=ttyS0,115200 rootfstype=cramfs mem=32mb(参数也请参考BSP的文件)
若是已经正确指定rootfilesystem 路径,仍然发生kernel panic时,这就需要去debug kernel的错误了,这是一个耗时的工作,成效取决于你对板子的熟悉度与对核心的知识的了解度。看懂error message可能有帮助,使用kgdb等相关工具也有帮助。但是仔细地看懂BSP文件,或许才能真正避免掉大部分不必要的困扰。

Root Filesystem

BSP所提供的rootfilesystem(以下简称rootfs)并不一定符合我们的需求,它也许过于阳春或冗余。若想快速建立一个轻量化、符合FHS(FilesystemHierarchyStandard)、具有套件管理系统的rootfs,Debootstrap是十分推荐的工具。

Debootstrap可以快速建立一套Debian或Ubuntu的rootfs,其执行步骤如下:
1. 从套件库下载所需的套件。
2. 将套件解压缩至指定的目录。
3. 执行chroot进入指定的目录。
4. 执行每个套件的安装与设定scripts。
通常步骤三与步骤四是在目的端(targetside)执行,但我们可以透过Qemu,在x86的本机端建立ARM的rootfs,再将其复制到已刻录bootloader和kernel的SD卡,基本的嵌入式系统就完成了。下个章节将详细介绍上述的步骤。

从套件库下载套件并解压缩

执行Debootstrap时,我们必须指定目标平台、套件库、系统版本及额外所需套件等信息,范例如下:
#mkdir~/my_emdebian;cd~/my_emdebian;
#debootstrap --arch=armel --foreign
--include=vim,openssh-server squeeze rootfs/
http://www.emdebian.org/grip/
上述的范例所建立的rootfs,是Debian的Squeeze(stable)版本,若想采用Ubuntu,可自行更改为lucid(10.04)等版本名。参数foreign是通知Debootstrap只要解压缩套件,而不执行每个套件的安装与设定scripts。
Qemu和chroot
将套件下载并解压缩至指定文件夹后,我们就准备要chroot进入此文件夹。在此之前,我们必须将QemuforARM复制到此文件夹中:
#cp/usr/bin/qemu-arm-static~/my_emdebian/rootfs/usr/bin
接着chroot进入我们建立好的rootfs,并执行每个套件的安装与设定scripts:
#chroot~/my_emdebian/rootfs/bin/sh
#/debootstrap/debootstrap--second-stage

执行至此,rootfs已经大致完成。当我们在此rootfs中执行任何动作,就如同在ARM的环境中执行一样。但我们发现,shell的提示符号为“Ihavenoname!”,而非我们所熟悉的命名。这也说明了,我们还需要对rootfs做点最后的微调。

系统调校

一般来说,需要调整的基本项目如下:
● hostname(/etc/hosthame)
● 网络接口(/etc/network/interfaces)
● DNS(/etc/resolv.conf)
● 套件库列表(/etc/apt/sources.list)
开发者亦可依需求调整环境变量、装置节点等细项。

结语

透过U-Boot、Linuxkernel和Debootstrap,开发者可以在极短的时间内,快速搭建一套嵌入式操作系统,以便进行硬件测试或软件开发。在瞬息万变的信息领域,快速搭建原型以验证构想是不可或缺的能力,希望读者透过本文介绍能对您有所裨益。

(本文作者任职于台湾资策会网络多媒体研究所)