次のページ 前のページ 目次へ

9. 移植の実際

この章では,私流の実際の移植作業について順を追って説明していきます. 最近は,評価ボードで動作する Linux カーネルのサンプルが付いてくること が多いので,一から移植することも少なくなってしまいました.

9.1 情報の入手

まずは,該当する CPU アーキテクチャの Linux についての情報を調べましょう.

x86 以外の Linux の場合,大抵は,そのアーキテクチャについてのサブプロジェクトが存在します. 例えば MIPS と ARM の例

もちろん,

あたりも必要です.

9.2 クロス開発環境の入手

前述のサブプロジェクトの web などで触れられているはずです. 具体的には binutils と gcc ですが,GNU の正式リリース版にパッチが当たっている場合がありますので,こちらを使用した方がよいでしょう. 運がいい場合は rpm や deb パッケージなんかもあるので,コンパイルの手間が省けます.

debian の場合は,toolchain-source パッケージが利用できます. このパッケージからクロスコンパイラを生成する手順は Build Cross Development Environments などを参照しましょう.

で,とりあえずカーネルのコンパイルだけならは glibc は不要です. glibc もコンパイルしようとすると…鶏卵問題にぶつかり,結構大変です :-)

9.3 ブートローダとのインターフェース

を考える必要があります. ここでいうブートローダとは,メモリ上に Linux カーネルを配置してジャンプするプログラムです. x86 の PC では BIOS と LILO がそれに当たります.

カーネルのエントリアドレスにジャンプする際のレジスタインターフェースに関しては,アーキテクチャ依存になっています. MIPS の場合ARM の場合 については後述.

どこに配置するか・どこにジャンプするか

カーネルのイメージをどこに配置するかは,カーネルのリンク時に使用した ld.script に依ります. このスクリプトの場所は arch/アーキテクチャ名/ld.script です.

つまり,このスクリプトを修正することにより,カーネル自身を別の場所に配置することも可能です.

あと,単に「エントリアドレスを知りたい」という場合は,コンパイル後にで きる System.map を見てみましょう. head.S にあるエントリのシンボルのアドレスを探せばよいわけです.

MIPS の場合

ジャンプ時のレジスタインターフェースですが.a0, a1, a2 レジスタに int argc, char **argv, char **envp 相当の値を代入します. これがカーネルコマンドラインになります.

詳しくは arch/mips/kernel/head.S を参照のこと. エントリアドレスは先頭ではなく,kernel_entry です.

ARM の場合

ジャンプ時のレジスタインターフェースですが.r0 = 0, r1 = アーキテクチャ番号,となります. これも詳しくは arch/arm/kernel/head-armv.S を参照しましょう.

9.4 まずは LED

ターゲットボードに制御可能な LED がある場合は,まずはこれを点滅させてみます. 大抵は GPIO 経由で制御するようになっているので,アセンブラでも数命令で制御することができると思います. 不幸にして,そのような LED の無いボードの場合は… ボードの設計者を呪って下さい :-)

LED の点滅により,少なくとも「ここまではプログラムが動いている」ということが確認できます.

9.5 シリアル出力関数

次は,シリアルポートに文字列を出力する関数を作ります. 例えば,void debug_puts ( const char * ) のような感じ. とりあえずは,割り込みは使わずにビジーループで出力待ちする関数で十分です.

そして,これを printk() 関数の中に仕込んでやります. これにより,とりあえず printk() 関数による,デバッグに有用な情報の出力が可能となります.

通常,printk() (というか,コンソールドライバ)は,コンソールデバイスの初期化が終了するまでは,メッセージをバッファに貯めておきます. このメッセージは,コンソールの初期化が終了した時点でまとめて出力されます. つまり,コンソールの初期化に到達する前に何らかの理由により,処理が止まってしまった場合,そこまでのメッセージが出力されないことになります.

ここまで来れば,適所に printk() を入れることにより,アドホックにデバッグを行なうことができるでしょう.

9.6 割り込みハンドラ

次に作成するのは,割り込みハンドラあたりになるでしょう.

9.7 インターバルタイマ

このあたりでインターバルタイマの実装も必要になるでしょう.

リアルタイムクロックとインターバルタイマ

x86 ではこの 2 つは別物ですが,SoC の場合は実装依存です.

リアルタイムクロックについては

といった感じです.

インターバルタイマ

割り込み間隔は 1/100 秒ごと,というのは(ほぼ)必須です. が,インターバルタイマのカウンタの分解能は,これ以上の,できるだけ分解能が高くなるように設定した方がいいでしょう.

というのは,gettimeofday() システムコールでは(事実上)マイクロ秒単位 の時刻が要求されているからです. もちろん,1/100 秒の分解能でもカーネルの動作には問題はありませんが,

という悲しい現象が生じます.

インターバルタイマに関しては,「マスタークロックの周波数」も確認が必要です. カーネルのサンプルをそのまま実行し,しばらく経過すると時刻が進む・遅れる,などの不具合が生じるボードをしばしば見掛けます. 公称 66 MHz であっても,実は 66.6666666... MHz であるかもしれません. 回路図や部品表から,元となるクロック周波数を調べ,そこからタイマのカウント周波数を求めることが必要です.

タイマ割り込み処理ハンドラ

通常,割り込みを直接受けるハンドラはアセンブラで記述されます. で,このアセンブラの関数から C の割り込み解釈ルーチンが呼び出されます.

が,タイマに関しては割り込み回数が多いためか,アセンブラから直接タイマ のイベントハンドラが呼び出されている実装も多いようです.

というわけで,do_IRQ() あたりで網を張っていてもタイマ割り込みが引っか からない場合があります. また,/proc/interrupts あたりにタイマ割り込みが表示されないこともあり ます.

注意しましょう.

9.8 ルートファイルシステム

この当たりまで来れば,「ルートファイルシステムが見つからへんで」というメッセージを拝むことができるでしょう. というわけで,当座のルートファイルシステムを手配しましょう.

組み込みシステムで使えそうなブロックデバイスとしては

があります.

9.9 各種ドライバ

ここまでくれば,必要なドライバを適宜移植,ということになります.

シリアルドライバ

ここで言うシリアル,というのは,いわゆる UART,というものです.

ターゲットのシリアルが 16550 互換ならばあまり問題はないのですが,16550 と互換性のないシリアルインターフェースの場合,少し考えることがあります. というのは,「どのデバイスファイルに割り当てるか」というあたりです.

x86 linux では,通常,/dev/ttyS* というデバイスファイルに振られています. ターゲットボードで 16550 非互換のデバイスをこのデバイスファイルに振った場合,PCMCIA の移植で困ったことが起きます. PCMCIA のシリアルデバイスは 16550 互換ですが,/dev/ttyS* は,16550 非互換のドライバが担当しているので,これでは都合が悪いことになります.

linux/Documentation/devices.txt を見ると,このようなデバイスは /dev/ttyFW* に割り当てた方が良いように見えます.

なお,drivers/char/serial.c のシリアルドライバ自体のソースは,x86 以外のアーキテクチャにも対応しており,

というあたりも,ドライバ自体に手を加えること無く,include/asm/serial.h ヘッダファイルでの変更のみで対応することができます.

強いて問題点を挙げるとすると,「初期化と終了のコールバックのエントリが欲しいな」というあたりです. 組み込み用途の CPU では,省電力のために各デバイスへの電源・クロック供給を制御できるものが多く,そのほとんどは,電源立ち上げ時はシリアルインターフェースへの電源は切れています. 省電力にそれほどこだわらないのでなければ,アーキテクチャ依存の初期化関数にシリアルインターフェースへの電源・クロック供給処理を入れておけば良いでしょう.

USB ホストドライバ

組み込み系の場合,おそらく OHCI/EHCI 互換のホストコントローラでしょう. が,この移植に際し,

の問題があります.

キャッシュコヒーレンシ問題については,最近のドライバでは解決されているようです.
# が,以前はこれで苦しんだのですよ (^^;;

CPU コア内部のパイプラインによって問題が生じることもあります. 例えば

writel(data1, addr1);
data2 = readl(addr2);
というコードの場合,一般的には,readl() は,writel() 実行後に実行されることが期待されます.

ところが,アーキテクチャによっては,CPU のパイプラインにより,writel() の,バスへのライトサイクルが発行される前に readl() のリードサイクルが 発行されてしまうことがあります.

このような場合は,MIPS では,

という回避方法があります.

sync 命令の場合,CPU によっては(例えば NEC の VR4100 シリーズなど)では,単に nop と解釈され,期待した動作をしないので,注意が必要です. この場合,素直に複数の nop でパイプラインを埋めてやりましょう.

9.10 デバッガ

MIPS CPU の場合,gdb を使用してデバッグすることができます. この場合,デバッグ対象となるのはターゲットボード上で動いている Linux カーネルです. いわゆる「リモートデバッグ」です.

gdb (のコマンド)自体は,ホストマシンで起動します. この場合の gdb は,ターゲットマシンのアーキテクチャ用にコンパイルされたものを使用します.

接続イメージを示します.

+--------+ ttyS0 +---------+
| target |-------| host    |
|        |       |         |
| board  |-------| machine |
+--------+ ttyS1 +---------+
ttyS0 にシリアルコンソールを設定し,ttyS1 に gdb のデバッグ情報が流れます.

ターゲットカーネル側の準備

まずは,make *config で,「Remote GDB kernel debugging」を有効にします.

あと,上図の ttyS1 に gdb デバッグ情報を流すための関数を用意します. たとえば,Au1000 では arch/mips/au1000/common/dbg_io.c に定義されています. このファイルにあるように

という関数を用意します. 関数の役割は自明だとは思います (^^;;

あと,カーネルのバージョンによっては前述の「printk バッファリング回避」が必要になるかもしれません. この理由については後述.

gdb の準備

host machine で動作する gdb を準備します. これは,素直に

  1. gdb のソースを入手
  2. (MIPS ならば) configure で --target=mipsel-linux と指定し
  3. make
で,ホストマシン上で動作する,ターゲットアーキテクチャ用の gdb が出来 上がります.

それでは起動

コンパイルしたカーネルをターゲットボードに送り込み,Linux を起動します. すると,コンソールに「リモートの gdb を起動せい」というメッセージが出る…はずなのですが,カーネルのバージョンによっては printk バッファリングのため,このメッセージが出ないことがあります.

というわけで,最初は,printk 内にプリミティブな出力ルーチンを組み込むという,前述の方法を使用した方が良いでしょう.

話を元に戻して,メッセージが出力されたら,言われた通り,リモートマシンで gdb を起動しましょう.

$ gdb vmlinux
(gdb) set baud 115200(ボーレートの設定)
(gdb) target remote /dev/ttyS1(ターゲットボードに接続)

あとは,普通のプログラムのデバッグと同様

などを行なうことができます.

限界

タイミングにシビアなところのデバッグは,printk デバッグ以上に困難です. 実行時の速度自体は問題ないのですが,実行を中断したりステップ実行が入ると,タイミング自体が狂ってしまいます.

このような場合は,gdb によるデバッグは素直に諦め,別の手段を考えましょう.


次のページ 前のページ 目次へ