Linux による埋め込みアプリケーションについて


埋め込み(embedded)アプリケーションはそれ自体非常に多様なのですが、そこで利用される OS も実にさまざまなものがあります. 多くの場合 RTOS (Real Time Operating System) と呼ばれるタイプの OS が使われますが、その機能にしろサイズにしろ非常に多様でもちろんどんなレベルの互換性もないといっても差し支えないほどです. このようなアプリケーションの中にはハイエンドのパソコンの計算リソース全てを占有してやっと間に合うようなものもでてくる始末で、その OS への要求も大きくなっています. 中には通常のパソコン用 OS と同様のグラフィックスやファイル、ネットワーク操作を歌うものもあり、そのような OS のサイズは実質何百 KB にもなります.

このようなクラスのアプリケーションなら Linux を埋め込み化すれば面白いだろうと思えます. (ひょとしてすごく面白くない人もいるかもしれませんが... :-)) そこでこのような場合にどうしたら Linux のような OS が適用できるかを主な狙いにしていろいろ考えてみることにしました.

埋め込みの応用の場合そのプラットフォームは必ずしも既存のハード、例えば PC/AT に似たものとは限りません. Linux を新しいプラットフォームに移植することは難しいでしょうか? 答えは、もしそれが可能ならそれほど難しくはない、といったあたりでしょう. 最初に新しいハードウェアへの Linux の移植について考えてみます.

埋め込みの応用の場合メカニカルに動く部分、つまりハードディスクやフロッピーディスクを嫌う場合があります. またネットワーク上で boot したり、NFS が使えるというわけにもいかない応用があります. こういうときにはカーネルや必要なファイルが ROM (Read Only Memory) の中にあったらなあという話が2番目の話題です.

最後に、多くの埋め込み応用で必要になる実時間性の問題を扱います. 普通の Linux は 10ms 以下の精度でいろいろなことを制御することはできません. 考えてみると例えば x86 系にしても 33MHz の 486 の時代から考えると 10-20 倍は速くなっているのだから、 ここもなんとか工夫しだいと思えてきます.

というわけで、この文章は以下のような構成になってます. 内容はもちろんかなり(すごーく)独断と偏見に満ち満ちているので、ご承知おきの程を.

参考文献およびURL

次のものは NDA に基づいて公開できません :-)

1. 新しいハードウェアへの Linux kernel の移植.

新しいハードウェアへの Linux kernel の移植のコストは対象となるハードウェアのアーキテクチャでずいぶん異なりますが、 そこで使われる CPU がすでに Linux がサポートしているものならそれほど難しいとは言えません. 現在 2.1 系 kernel は Alpha, ARM, x86, M68k, MIPS, PPC, Sparc 及び 64bit Sparc の各 CPU をサポートしています.

それ以外の CPU を使ったハードウェアへの移植はずっと困難になりますが、基本的には 仮想記憶が使える CPU ならば Linux の移植は可能だと考えられます. gcc がサポートしてない CPU だったりするとまず gcc のポーティングからということになりますが.

ここでは、Linux はすでに target の CPU をサポートしている場合に新規ハードウェアへの Linux の移植についての具体的な手順と注意を述べます.

説明の都合上、以後 CPU は MIPS の Rシリーズ で新規ハードは FOOBAR ボードと呼ばれていると仮定します.

1.1. 必要な既存ソースファイルの修正

(1) まず arch/mips/config.in ファイルに FOOBAR の定義行を追加します. diff スタイルで修正部分を出力すると以下のようになります.

*** linux-original/arch/mips/config.in  Wed Dec 31 05:36:34 1997
--- linux/arch/mips/config.in   Mon Nov  2 20:12:28 1998
***************
*** 18,23 ****
--- 18,24 ----
    bool 'Support for Deskstation RPC44' CONFIG_DESKSTATION_RPC44
    bool 'Support for Deskstation Tyne' CONFIG_DESKSTATION_TYNE
    bool 'Support for Mips Magnum 3000' CONFIG_MIPS_MAGNUM_3000
+   bool 'Support for FOOBAR board' CONFIG_MIPS_FOOBAR
 fi
 bool 'Support for Cobalt Server' CONFIG_COBALT_MICRO_SERVER
 bool 'Support for Mips Magnum 4000' CONFIG_MIPS_MAGNUM_4000

(2) arch/mips/Makefile に以下のような部分を追加します.

 ifdef CONFIG_MIPS_FOOBAR
 ARCHIVES      += arch/mips/foobar/foobar.o
 SUBDIRS       += arch/mips/foobar
 LOADADDR      += 0x80100000
 endif

ここで 0x80100000 は linux kernel のローディングアドレスで、target や kernel のローディングのやり方によって書換えてやる必要があるでしょう. また、platform 依存のマクロ定義などは、さきほどの config.in ファイルに追加しておきます:

***************
*** 68,73 ****
--- 69,79 ----
  if [ "$CONFIG_SNI_RM200_PCI" = "y" ]; then
        define_bool CONFIG_PCI y
  fi

+ if [ "$CONFIG_MIPS_FOOBAR" = "y" ]; then
+	define_bool CONFIG_NO_SWAPPER y
+	define_bool CONFIG_FOOBAR_SERIAL y
+	define_bool CONFIG_BLK_DEV_CORE y
+ fi
  endmenu

必要があれば arch/mips/kernel/Makefile も修正します. 例えば、FOOBAR ボードのタイマーチップが MIPS への移植の場合に標準となっている MC146818 やその互換チップでなければ標準のタイマールーティン arch/mips/kernel/time.c を使わないように arch/mips/kernel/Makefile を次のように修正してやります:

*** linux-original/arch/mips/kernel/Makefile    Sat Feb 28 09:53:22 1998
--- linux/arch/mips/kernel/Makefile     Fri Oct 30 21:24:21 1998
***************
*** 23,29 ****
--- 23,33 ----
  # SGI's have very different interrupt/timer hardware.
  #
  ifndef CONFIG_SGI
+ ifndef CONFIG_MIPS_FOOBAR
  O_OBJS += irq.o time.o
+ else
+ O_OBJS += irq.o
+ endif
  endif
  
  #

platform 特有の初期化ルーティンは architecture の setup routine (この場合は arch/mips/kernel/setup.c の中にあります)から call されます.

*** linux-original/arch/mips/kernel/setup.c     Wed Dec 31 05:36:36 1997
--- linux/arch/mips/kernel/setup.c      Sun Nov  8 17:28:13 1998
***************
*** 152,161 ****
  	void jazz_setup(void);
  	void sni_rm200_pci_setup(void);
  	void sgi_setup(void);
  
  	/* Perhaps a lot of tags are not getting 'snarfed' - */
  	/* please help yourself */
--- 157,166 ----
  	void jazz_setup(void);
  	void sni_rm200_pci_setup(void);
  	void sgi_setup(void);
+ 	void foobar_setup (void);
  
  	/* Perhaps a lot of tags are not getting 'snarfed' - */
  	/* please help yourself */
***************
*** 170,175 ****
--- 175,185 ----
  
  	switch(mips_machgroup)
  	{
+ #ifdef CONFIG_MIPS_FOOBAR
+ 	case MACH_GROUP_FOOBAR:
+ 		foobar_setup();
+ 		break;
+ #endif
  #ifdef CONFIG_COBALT_MICRO_SERVER
  	case MACH_GROUP_COBALT:
  		cobalt_setup();

MACH_GRP_XXX というタイプのマクロは include/asm-mips/bootinfo.h で定義されます. MIPS architecture の場合は、次の 3 つのマクロ定義をこのファイルに追加します:

 #define MACH_GROUP_FOOBAR      8 /* foobar board  */
 #define MACH_FOOBAR            0 /* foobar hardware */
 #define GROUP_FOOBAR_NAMES { "FOOBAR" }

1.2. 必要となる新しいファイル.

FOOBAR ボードのために arch/mips/foobar という directory を用意しほとんどの platform 依存ファイルをその中にいれることにします. 例えば、 それらが mach_dep.c, time.c そして int-handler.S とするとき、 arch/mips/foobar/Makefile を次のように作成します.

 #
 # Makefile for the FOOBAR board specific parts of the kernel
 #
 # Note! Dependencies are done automagically by 'make dep', which also
 # removes any old dependencies. DON'T put your own dependencies here
 # unless it's something special (ie not a .c file).
 #
 
 .S.s:
 	$(CPP) -D__LANGUAGE_ASSEMBLY__ $(CFLAGS) $< -o $*.s
 .S.o:
 	$(CC) -D__LANGUAGE_ASSEMBLY__ $(CFLAGS) -c $< -o $*.o
 
 all: foobar.o
 O_TARGET := foobar.o
 O_OBJS	 := mach_dep.o time.o int-handler.o
 
 clean:
 
 include $(TOPDIR)/Rules.make

これで platform 依存の object をまとめた foobar.o という object が make 時に作られます. もし mach_dep.c, time.c あるいは int-handler.S が何か特に platform 依存であるような追加的なファイル - 例えばデバイスのレジスタマップを含むインクルードファイルなど - を必要とするなら、それらもこの arch/mips/foobar directory にいれておけば良いでしょう.

Linux kernel の移植に必要となる platform 依存のルーティンはその場合場合で大きく異なるでしょう. しかし一般的には最低限必要なものは割り込みハンドラと platform の初期化ルーティンです. もちろん platform 特有のデバイスがあればそのドライバが必要になるでしょう.

ほら、新しいハードウェアへの移植って簡単でしょう? :-)

1.3. カーネルの configuration と make

以上の準備の下でカーネルの configuration をソースの top directory で行います. これは

make config

とし configuration を行うプログラムの質問に答えるだけです. これによって include/linux/autoconf.h というファイルが作られます.

make dep; make

によって kernel が作られることになります. これらの操作はどんな kernel を作るときでも似たようなものです.

2. 埋め込み向きのルートデバイス.

多くの埋め込みアプリケーションで、ハードディスクもフロッピーもネットワークもないか、または起動時には使いたくないという要求があります. このような場合には、 Linux kernel やその他必要なファイルやアプリを ROM や flush メモリや FRAM のような NVRAM にいれたくなります. 一般にオンボードの ROM はそれぼど大きくありませんが、埋め込み用途といっても RAM のサイズは非常に大きくなっています. そこでこれら必要なものをデータ圧縮して ROM に格納し、それを RAM エリア上に伸長してやることが考えられます. もちろん、カーネルを ROM 上に置き、それを直接実行することもできますが、 通常 ROM はせいぜい 1-2 バイトの幅しかなく、そのアクセスもかなり遅いことから Linux を埋め込み用途で使おうなどという場合にはあまり勧められない方法です.

Linux は起動時にルートファイルシステムを置くルートデバイスを必要とします. 上に述べたような場合に、最も単純な解決策はファイルシステムのイメージ(例えば、 ext2 ファイルシステムでのルートファイルシステムを持つフロッピーディスクのイメージ)をたった 1 つのデータセクションを持つオブジェクトファイルにしてしまいカーネルにリンクしてしまうことでしょう. これはもちろんかなり大きなサイズのカーネルをつくり出すことになりますが、 カーネルの圧縮の際、ルートファイルシステムのイメージ部分が圧縮されるため、 必要となる ROM 中でのサイズはずっと小さくなります.

この方法の欠点の一つはルートファイルシステムに新しいファイルを追加するには kernel の再構築が必要なことです. もし対象となるハードウェアで交換可能な flush メモリや他の NVRAM が PCMCIA 経由などで使えるなら、これらのメモリの中にユーザのアプリケーションを含むファイルシステムを作りルートファイルシステムの directory にマウントすることが考えられます. この場合はアプリケーションの追加や交換のためにカーネルを作り直すことは不必要になります.

大きな ROM を持っている場合に ROM だけから Linux を boot した話がずいぶん前ですが Linux Jurnal (1997 年 1月号 "Booting Linux from EPROM") にのっています.

2.1. ファイルシステムのイメージを含む ELF オブジェクトファイルの作り方.

ELF オブジェクトファイル は Linux でも標準のオブジェクトファイル形式だといえます. データセクションだけを含むような ELF オブジェクトファイルはごく簡単な c のソースファイルから作る事ができます. 例えば coreimage.c. を

 char core_disk_image[1440 * 1024] =
 { 'I', 'M', 'A', 'G', 'E', };
 int core_disk_size = sizeof (core_disk_image);
 int core_disk_start = (int) &core_disk_image[0];

とすれば、これから目的のオブジェクトファイル coreimage.o を得ることができます. ここで配列 core_disk_image の初期化データを指定したのはこの配列が .bss セクションに割り付けられるのを避けるためだけの目的です.

一方, 次のようにすることで 1440k byte のサイズの ext2 ファイルシステムを作成できます:

        /sbin/mkfs -t ext2 /dev/ram 1440
        mount -t ext2 /dev/ram /mnt

そして /mnt 中に必要な directories やファイルを作ってから

	umount /mnt
	dd bs=1k count=1440 of=test.img if=/dev/ram

とすることで 1440k byte サイズのファイルシステムのイメージが test.img というファイルに得られます.

さて coreimage.o を objdump -x で調べてみると

coreimage.o:
file format elf32-bigmips
coreimage.o
architecture: mips:3000, flags 0x00000011:
HAS_RELOC, HAS_SYMS
start address 0x0000000000000000

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         00000000  0000000000000000  0000000000000000  00000034  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .data         00168008  0000000000000000  0000000000000000  00000034  2**2
		  CONTENTS, ALLOC, LOAD, RELOC, DATA
  2 .bss          00000000  0000000000000000  0000000000000000  0016803c  2**0
                  ALLOC
...

のような出力が得られ、.data セクションがファイルのオフセット 0x34 から始まることがわかります. そこで coreimatge.o のその部分を test.img で置き換えればファイルシステムイメージを持つオブジェクトファイルが作れます. これをマニュアルでやるとすれば

	dd if=coreimage.o of=x0 count=52 bs=1
	dd if=coreimage.o of=x2 skip=1474612 bs=1
	cat x0 test.img x2 >emcore.o

で充分です. ここでは結果は emcore.o に得ています.

補 1 にこのタイプのメモリデバイスのごく簡単なデバイスドライバのソースを挙げておきました。

3. Linux kernel のリアルタイム化 (工事中).

3.1. RT-Linux.

Real-Time Linux (http://rtlinux.cs.nmt.edu/~rtlinux/) は Linux kernel のリアルタイム化を目指したプロジェクトです. 詳しい情報が上の URL から得られます.

3.2. Linux kernel を μITRON のタスクとして動かす (in preparation).

RT-Linux は正攻法ですが、単にハードウェアをもっと高い時間精度で制御したいだけなら他の方法も考えられます. ここではその一つとして既存の RTOS 上のタスクあるいはプロセスとして Linux を動かしてしまう手法を考えてみましょう. このようにすれば Linux 側からは RTOS 側の機能 - 例えば高い時間精度による制御 - が可能なはずです. ただし RTOS 側から Linux 側の機能を呼び出すにはさまざまな問題があってそう簡単にはいかないでしょう. [続く]

補遺.

補.1. 簡単な core デバイスドライバ.

The following is a sample device driver drivers/block/core.c.

/*
 * core.c - Core disk driver
 *
 * Core disk is designed to have filesystems in data section of kernel
 * itself. 
 */

#include <linux/config.h>
#include <linux/sched.h>
#include <linux/minix_fs.h>
#include <linux/ext2_fs.h>
#include <linux/fs.h>
#include <linux/kernel.h>
#include <linux/string.h>
#include <linux/mm.h>
#include <linux/mman.h>
#include <linux/malloc.h>
#include <linux/ioctl.h>
#include <linux/fd.h>
#include <linux/module.h>

#include <asm/system.h>
#include <asm/segment.h>

extern int core_disk_start;
extern int core_disk_size;

/*
 * 64 has been locally registered as the EMCORE major number in
 * include/linux/major.h.
 */
#define MAJOR_NR  CORE_MAJOR
#include <linux/blk.h>

static int core_length;
static int core_blocksizes[1] = {0};

static int core_read (struct inode *inode,struct file *file,char *buf,
		     int count)
{
  int left;

  left = core_disk_size - file->f_pos;
  if (count > left)
    count = left;
  if (count <= 0)
    return 0;
  memcpy_tofs (buf, (char *) core_disk_start + file->f_pos, count);
  file->f_pos += count;
  return count;
}

static struct file_operations ed_fops =
{
  NULL,            /* lseek - default */
  core_read,	   /* read */
  NULL,            /* write */
  NULL,            /* readdir */
  NULL,            /* select */
  NULL,            /* ioctl */
  NULL,            /* mmap */
  NULL,            /* no special open code */
  NULL,            /* no special release code */
  NULL             /* fsync */
};

static int get_image (unsigned char *buf, int ofs, int len)
{
  memcpy (buf, (char *) core_disk_start + ofs, len);
  return 0;
}

static int get_edisk (unsigned char *buf, int sect, int num_sect)
{
  int s;    /* Sector offset */

  for (s = 0; s < num_sect; s++)
    get_image (buf + s * 512, (sect + s) * 512, 512);

  return 0;
}

static void core_request (void)
{
  int len, ofs;

repeat:
  INIT_REQUEST;

  ofs = CURRENT->sector << 9;
  len = CURRENT->current_nr_sectors << 9;

  if (!(MINOR(CURRENT->rq_dev) == 0) || (ofs + len > core_length))
    {
      printk ("COREDISK: minor=%d ofs=%d len=%d core_length=0x%x\n",
	      MINOR(CURRENT->rq_dev), ofs, len, core_length);
      end_request (0);
      goto repeat;
    }
  if (CURRENT->cmd == READ)
    {
      get_edisk (CURRENT->buffer, CURRENT->sector,
		 CURRENT->current_nr_sectors);
    }
  else
    {
      panic ("COREDISK: unknown RAM disk command !\n");
    }
  end_request (1);
  goto repeat;
}

/*
 * Returns amount of memory which needs to be reserved.
 */

long core_init (long mem_start, int mem_end)
{

  if (register_blkdev (CORE_MAJOR,"emcore",&ed_fops))
    {
      printk ("COREDISK: Unable to get major %d.\n", CORE_MAJOR);
      return 0;
    }
  blk_dev[CORE_MAJOR].request_fn = DEVICE_REQUEST;

  core_blocksizes[0] = 512;
  blksize_size[MAJOR_NR] = core_blocksizes;

  core_length = core_disk_size;
  printk ("COREDISK: %d bytes\n", core_length);
  return 0;
}
Index に戻る