この文書には,私がいくつかのMIPSマシンへの移植や他のLinux関連の作業において学んだことが反映されています.初心者が何から始め,どのように経験を積んでいくかという手助けになれば幸いです.
この文書はMIPSマシンへのLinux移植における主要な工程をすべて含んでいます.中でも「MIPSマシン抽象化層」(すなわちマシン固有コードとほとんどのMIPS共通コード間のインタフェース)と呼ばれる部分に注目することになるでしょう.他にも「Linuxハードウェア抽象化層」(すなわちLinux共通コードとアーキテクチャ固有コード間のインタフェース)について注目した有用な文書には,C Hanish Menon (www.hanishkvc.com)によって書かれたこの文書が挙げられます.
この文書では次のような注釈を使います.
共通のMIPSツリーはlinux-mips.orgにあるCVSツリーです.Linux/MIPS HOWTOの"Anonymous CVS servers"の章にある解説を見てください.2004年1月26日現在のカレントカーネルは2.6.1です."linux_2_4"もしくは"linux_2_2"ブランチタグを利用することで,以前の安定版リビジョンを常にチェックアウトできます.
さまざまな理由から,適切な修正がチェックインされずに,ずいぶんと長い間カーネルツリーにバグが残されたままになっているかもしれません.さまざまな場所からパッチを取得できます.よく知られたものを次に挙げます.
Jun Sun patches
Linux/MIPS FTP archive
Maciej W. Rozyki patches
Brad LaRonde's patches
おそらくあなたのMIPS boxではまだLinuxが動いてないことでしょう(そうでなれけばなぜ悩むのでしょうか?).そこでカーネルイメージを作るために別のマシンが必要になります.イメージを作ったならばすぐに,このイメージをMIPS boxにダウンロードし,MIPSカーネルを動かします.これはクロス開発と呼ばれています.MIPS boxはたいていターゲットマシンと呼ばれ,カーネルイメージを作るために使われるマシンはホストマシンと呼ばれます.
通常,組込み機器はネイティブ開発するための十分な性能や周辺機器を持っていないので,その開発においてはクロス開発が一般的です.
もっとも一般的なホストマシンはおそらくi386/PCで動作するLinuxでしょう.
移植を始める前に,ホストマシンにクロス開発ツールをセットアップする必要があります.クロスコンパイルツールを作るために,Linux/MIPS HOWTOのような解説を見つけることができるでしょうが,おそらく既存のツールを取得するのが確実な方法でしょう.
MontaVistaはフル機能のツールチェインを含んだ無償のJorneymanエディションを提供していましたが,残念なことにもはや提供されていません.その代わり,「スリム」バージョンのツールチェインを含んだプレビューキットがhttp://www.mvista.com/previewkit/index.htmlからダウンロードできます.
次のリンクはpre-buildツールチェインや自分でツールチェインを構築するための解説,そして最後に示すのはMIPSボート用のpre-compiledディストリビューションです.
Brad LaRonde's cross toolchain for Linux
Steve Hill's toolchains for glibc and uClibc
MIPS free toolchains
Distributions for MIPS
クロス開発において,通常シリアルポートはもっとも重要なインタフェースで,起きていることは何でも見ることができる場所です.Linuxに取り組み始める前に,シリアルポートが動作するか確認することは有益でしょう.printfを実行できるスタンドアローンプログラムのsample codeサンプルコード,またはgzipしたtar ballを見つけることができるでしょう.このようなプログラムは後のデバッグ段階においても,ハードウェアレジスタ値を出力するなどに有益です.
'make'と急いでタイプする前に,次に示すコンフィグレーションをチェックし,修正しましょう.
さぁ,"make"コマンドを実行しましょう.
あなたのMIPS boxのダウンローダに応じて,ELFイメージ,バイナリイメージ,またはSRECイメージを生成する必要があります.
bareboneイメージをターゲットにダウンロードして,実行してみましょう! ホストマシンとシリアルポートでつなぎ,minicomを実行すれば,おそらく"Hello, world!"メッセージが見えるはずです.
トラブルシューティング:
Linuxツリーにコードを追加して,Linuxイメージを作ってみましょう.説明のために,AlohaというMIPSボードにLinuxを移植しようとしていることにします.
もっとも簡単な選択はarch/mips/alohaというディレクトリを作ることです.
この例では,arch/mips/alohaディレクトリを作ることにしましょう.
Alohaボード用に,シンボルが見つからないと文句を言われない完全なLinuxイメージを生成できるコードを書いて行きましょう.
このディレクトリに行き,arch/mips/aloha ディレクトリを見てください.もしくはこのディレクトリをgzipしたファイルがダウンロードできます.
このコードは明らかにまだ未完全ですが,次から示すステップにしたがってすべてを修正していくことで,あなただけのLinux/MIPSカーネルイメージを生成できるようになるはずです.
このステップの大半は極めて単純です.
LOADADDRはLinuxイメージをRAMにロードしたときの開始アドレスです.大抵のCPUでは,最初の0x200バイトは例外ベクタとして使われていることに注意してください. いくつかのCPUはより大きな空間を取ります.LOADADDRがその空間よりも高位であるか確認してください.リンク時の制限により,開始アドレスは8K境界にアライメントされます.そこでLOADADDRを0x80002000に設定することは妥当にみえます.# # Hawaii Aloha board # ifdef CONFIG_ALOHA SUBDIRS += arch/mips/aloha LIBS += arch/mips/aloha/aloha.o LOADADDR += 0x80002000 endif
dep_bool 'Support for Hawaii Aloha board (EXPERIMENTAL)' CONFIG_ALOHA $CONFIG_EXPERIMENTAL
if [ "$CONFIG_ALOHA" = "y" ]; then define_bool CONFIG_CPU_R4X00 y define_bool CONFIG_CPU_LITTLE_ENDIAN y define_bool CONFIG_SERIAL y define_bool CONFIG_SERIAL_MANY_PORTS y define_bool CONFIG_NEW_IRQ y define_bool CONFIG_NEW_TIME_C y define_bool CONFIG_SCSI n fi
すぐに試してみたいという要求に答えるために,2001年9月28日のOSS CVSツリーに対してAlohaボートサポートを追加するための完全なパッチを見つけることができます.
これでお気に入りのコンフィグレーションツールを実行する準備ができました.まだそれほど多くのコードを追加していないので,貪欲になりすぎないでください.シリアルやシリアルコンソールなどの2,3の単純なオプションを選択するだけです.
Alohaボードに対する最小限のコンフィグサンプルです.
'make'とタイプする前に'arch/mips/Makefile'を念入りにチェックし,クロスツールチェインプログラムが正しく,PATH環境変数などで実行パスに存在することを確認してください.
さぁ,'make dep',そして'make'をタイプしてください.奇跡が起きるのを待ちましょう!
ところで,Linux/MIPSにおけるCコードの最初の行はarch/mips/setup.cファイル中のinit_arch()関数の先頭です.
カーネルバージョンが2.4.10よりも前の場合は,標準UARTシリアルポートを持つボード用の初期段階のprintkパッチはここで見けることができます.2.4.10以降の場合は,新しいprintkパッチが必要になります.もしすでにスタンドアローンの"hello, world!"プログラムが動作しているのであれば,初期段階のprintkは簡単に動作するはずで,すぐにLinuxからprintkできるようになるでしょう.
初期段階のprintkはなかなか便利ですが,まだ真のシリアルドライバを動かす必要があります.
標準的なシリアルポートを持っているならば,シリアルサポートを追加するには静的定義,実行時セットアップの二つのスタイルが考えられます.
静的定義は,include/asm/serial.hファイルを修正します.そのファイルを見てください.あなたのボード用にサポートを追加する方法を説明するのは難しくありません.
パラメータの設定の大半はほとんど明確です.あまり明確ではないものを次のリストに示します.
非標準的なシリアルポートを持っているのであれば,あなた自身でシリアルドライバを書かねばならないでしょう.
何人かは標準シリアルドライバからコードを派生させました.serial.cファイルは6000行を越えており,残念ながら,これは非常に手ごわい作業です.
幸運なことに別の手段が存在します[THANKS: Fillod Stephane].それは汎用シリアルドライバ(drivers/char/generic_serial.c)です.このファイルはハードウェア独立なシリアル用ルーチンの集合を提供しています.あなたの作業は割込みハンドラルーチンのようなハードウェア依存のルーチンを提供するだけです.たくさんの例があります.drivers/char/Makefileの中身をのぞいて,generic_serial.oファイルをリンクしているドライバを見てみましょう.
[HELP: プロプライエタリなシリアルドライバコードを書いたり,generic_serial.cファイルを使った経験を共有できればうれしく思います.]
int putDebugChar(uint8 byte) uint8 getDebugChar(void)
あなたのボート用にこれら二つの関数を提供する必要があります.例として,ここにDDB5476ボード用のdbg_io.cがあります.DDB5476はこれら二つの関数を実装するために,標準的なUARTシリアルポートを使います.
これら二つの関数を供給すれば,カーネルをデバッグする準備が整います.新しいカーネルイメージを実行しましょう.初期段階のprintkパッチも使っているのであれば,コンソールに次のような何かを見ることができるはずです.
Wait for gdb client connection ...
すでにターゲット上のシリアルポートとホストのシリアルポート(COM0とか)間をシリアルケーブルで接続しているのであれば,次のように適切なボーレートを設定し,ホスト上のクロスgdbを実行できます.
gdbプロンプトが表示されたら,次のようにタイプします.stty -F /dev/ttyS0 38400 mips_fp_le-gdb vmlinux
すると,kgdbを介してカーネルと対話できるようになるはずです.あなたが十分幸運であれば!target remote /dev/ttyS0
kgdbを使う上でのいくつかのコツを次に挙げます.
一つのシリアルポートしか持っていないボードもあります.シリアルポートをシリアルコンソールとして使うのであれば,いくつかのトリックを使わない限り本当はkgdb用に使えません.
二つのソリューションがあります.一つはkgdbコンソールで,もう一つはkgdb demuxingスクリプトを使う方法です.
kgdbコンソールを使うのは簡単です."Kernel hacking"サブメニュ下の"Remote GDB kernel debugging"を選択したとき,"Console output to GDB"が見えるようになります.単にそれを選択するだけです.実際,このオプションは二つ目のシリアルポートを持っていたとしても,使うことができるくらい簡単に使えます.
しかしながら,このオプションには制限があります.カーネルからユーザランドに遷移したとき,コンソールは動作を止めてしまいます[HELP: これを確認してください].私はkgdbコンソールドライバが割込みなどのような他の要求を処理するように完全に作られていないことが理由だと疑っています.[HELP: ボランティアはいませんか?]
二つ目のオプションはBrian Moyleによって書かれたkgdb_demuxと呼ばれるスクリプトを使うことです.それは通常,ttya0とttya1の二つの仮想ポートを作ります.そして(ttyS0のような)本物のシリアルポートを監視し,コンソールへのトラヒックをttya0に,kgdbへのトラヒックをttya1に送ります.あとは/dev/ttya0でminicomを起動し(ポートの設定は気にする必要はありません),/dev/ttya1でkgdbを動かせばよいだけです.
ここからtar ballをダウンロードできます.使い方を次に示します.
割込みコードを書きはじめる前に,まずハードウェアは本当に学びがいがあることをこころしてください.特にすべての割込みソースとそれらに関連するコントローラ,ルーティングを識別することに注意しましょう.
割込みサービスを実現するために,四つのコード部が協調動作します.
このファイルには,割込みをマッチングして,ディスパッチするコードが見つかるでしょう.
CPUのIPピンに直接つながるより多くの割込みソースを持っているかもしれません.その場合は,割込みをカスケードすることによって,セカンダリ,またはサードレベルの割込みコントローラを一つかそれ以上のCPU IPピンにつなぐことができます.
割込みをカスケードする方法にはddb5477,korva,ospreyのようにたくさんの例があります.次に簡単にまとめましょう.
static struct irqaction cascade =
{ no_action, SA_INTERRUPT, 0, "cascade", NULL, NULL };
extern int setup_irq(unsigned int irq, struct irqaction *irqaction);
void __init <my>_irq_init(void)
{
....
setup_irq(CPU_IP3, &cascade);
}
hw_irq_controller構造体はhw_interrupt_type構造体の別名としてinclude/linux/irq.hファイルにて定義されています.
struct hw_interrupt_type {
const char * typename;
unsigned int (*startup)(unsigned int irq);
void (*shutdown)(unsigned int irq);
void (*enable)(unsigned int irq);
void (*disable)(unsigned int irq);
void (*ack)(unsigned int irq);
void (*end)(unsigned int irq);
void (*set_affinity)(unsigned int irq, unsigned long mask);
};
irq_cpu.cはhw_irq_controllerメンバ関数を書くためによいサンプルコードになります.各関数に対するプログラミング上の注意点を次に挙げます.
- const char * typename;
- コントローラ名./proc/interrupts下に表示されます.
- unsigned int (*startup)(unsigned int irq);
- request_irq()やsetup_irqが呼ばれると実行されます.ここでこの割込みを有効にする必要があります.その他にIRQ固有の初期化を行ないたい場合は,ここで行ないます(おそらく割込みのための電力供給など).
- void (*shutdown)(unsigned int irq);
- free_irq()が呼ばれると実行されます.割込みを無効にし,必要であればIRQ固有のクリーンアップを行なう必要があります.
- void (*enable)(unsigned int irq)とvoid (*disable)(unsigned int irq)
- ドライバコードによって交互に使われるenable_irq(), disable_irq(),そして disable_irq_nosync()を実装するために使われます.
- void (*ack)(unsigned int irq)
- 割込みに対する確認応答をする際,do_IRQ()の先頭で,ack()が実行されます.同一の割込みソースに対して再帰的に割り込まないように,ここで割込みを無効にする必要があると思います.[HELP: 誰か確認できますか?]
- void (*end)(unsigned int irq)
- 割込みを処理した後,do_IRQ()によって呼ばれます.ack()関数によって割込みが無効なっていれば,ここで有効にすべきです.[HELP: 一般的にそれ以外にやることはあるのか?]
- void (*set_affinity)(unsigned int irq, unsigned long mask)
- SMPマシンにおいて,特定のCPUに対する割込み処理アフィニティを設定するために使われます.[TODO] [HELP]
IRQの初期化はinit_IRQ()です.現在,各ボードごとに供給されていますが,将来はMIPS共通ルーチンになり,そこからさらにボート固有の関数board_irq_init()が実行されるようになると思われます.board_irq_initは関数ポインタであり,<my_board>_setup()関数によってポインタ値を割り当てる必要があるでしょう.
とにかく,次に示すコードは通常のinit_IRQルーチンに対するスケルトンコードです.
extern asmlinkage void vr4181_handle_irq(void);
extern void breakpoint(void);
extern int setup_irq(unsigned int irq, struct irqaction *irqaction);
extern void mips_cpu_irq_init(u32 irq_base);
extern void init_generic_irq(void);
static struct irqaction cascade =
{ no_action, SA_INTERRUPT, 0, "cascade", NULL, NULL };
static struct irqaction reserved =
{ no_action, SA_INTERRUPT, 0, "reserved", NULL, NULL };
static struct irqaction error_irq =
{ error_action, SA_INTERRUPT, 0, "error", NULL, NULL };
void __init init_IRQ(void)
{
int i;
extern irq_desc_t irq_desc[];
/* hardware initialization code */
....
/* this is the routine defined in int_handler.S file */
set_except_vector(0, my_irq_dispatcher);
/* setup the default irq descriptor */
init_generic_irq();
/* init all interrupt controllers */
mips_cpu_irq_init(CPU_IRQ_BASE);
....
/* set up cascading IRQ */
setup_irq(CPU_IRQ_IP3, &cascade);
/* set up reserved IRQ so that others can not mistakingly request
* it later.
*/
setup_irq(CPU_IRQ_IP4, &reserved);
#ifdef CONFIG_DEBUG
/* setup debug IRQ so that if that interrupt happens, we can
* capture it.
*/
setup_irq(CPU_IRQ_IP4, &error_irq);
#endif
#ifdef CONFIG_REMOTE_DEBUG
printk("Setting debug traps - please connect the remote debugger.\n");
set_debug_traps();
breakpoint();
#endif
}
本章の記述は現在,新しい割込み処理スタイルと呼ばれているものです.おそらく将来は割込み処理コードを書くための唯一有効な方法になると思われます.この方法を動作するにはCONFIG_NEW_IRQを定義する必要があります.
いくつかのボードではCONFIG_ROTTEN_IRQオプションを定義することで,arch/mips/kernel/old-irq.cファイルを使っています.old-irq.cは非常にPC中心的で,ひどく時代遅れです.どちらにするか悩まないでください.
多くの他のボードはどちらのIRQコンフィグオプションも定義していません.それらは独自のIRQルーチンを使っています.将来,新しいスタイルのIRQルーチンに移行すべきです.
Linux/Documentations/mips/time.README にすばらしい文書があります(少々はずかしいですが:-0) かならず読んでください.
次に時間とタイマサービスを実装する上でのコメントをいくつか挙げておきます.
PCIバス自体についてより知りたい方は,MindShare Inc.より出版されている書籍PCI System Architectureをお薦めします.
すべてのIBM PC互換機では,BARはBIOSによって割り当てられるので,Linuxは単にバスをスキャンし,BARの値を記録するだけです.
二番目の方法はLinuxがPCIバスのスキャンを始める「前」に完全にPCI資源を割り当てる方法です.つまり,ファームウェアによって割り当てられたいかなるPCI資源も放棄し,自力で新しい割当てを行ないます.このアプローチはアドレス範囲と資源割当てを完全に制御できます.CONFIG_PCI_AUTOオプションとarch/mips/kernel/pci_auto.cファイルによって,非常に簡単に実現できます.本章ではこのアプローチに焦点をあてます.
本章では,CONFIG_NEW_PCIとCONFIG_PCI_AUTOの両方が有効になっている場合のアプローチに焦点を当てます.
さらに,PCIデバイスがDMA転送するために,システムRAMをPCIメモリバス上のアドレス0x0(そのアドレスが物理アドレス空間に存在することを仮定しています)に見えるようにしなければいけません.
上記の設定は典型的にはボード固有セットアップルーチン内(例えば,<board>_setup())で行なわれます.ioport_resourceとiomem_resourceも次のように設定されます.
ioport_resource.start = 0x0; ioport_resource.end = sz_io; iomem_resource.start = 0x0; iomem_resource.end = sz_mem;
これらの変数は資源のルートです.簡単のためにendが0xffffffffになるようにも設定できます.
最後章になりましたが,もっとも読まれる章になることでしょう.本章では,一般的に使われているデバッギングのコツをいくつか挙げていきます.
私は普段Phil Hollenbackによって書かれたcall2symスクリプトを使っています.呼出しトレース部分をカットアンドペーストしてスクリプトに与えるだけで,クラッシュ時の可能性がある関数呼出しスタックを表示します.いくつかのシンボルは間違っているかもしれませんが,本物も表示されることに注意してください.あなた自身の判断が必要になります.
kernel_entry() - arch/mips/kernel/head.S
set stack;
prepare argc/argp/envp;
jal init_arch - arch/mips/kernel/setup.c
cpu_probe() -
prom_init(...) - arch/mips/ddb5074/prom.c
loadmmu()
start_kernel() - init/main.c
setup_arch(&commaind_line); - arch/mips/kernel/setup.c
ddb_setup() - arch/mips/ddb5074/setup.c
parse_options(command_line);
trap_init();
init_IRQ(); - arch/mips/kernel/irq.c
sched_init();
time_init();
if (board_time_init) board_time_init();
set xtime by calling rtc_get_time();
pick appropriate do_gettimeoffset()
board_timer_setup(&timer_irqaction);
softirq_init();
console_init();
init_modules();
kmem_cache_init();
sti(); /* interrupt first open */
calibrate_delay();
mem_init();
kmeme_cache_sizes_init();
fork_init();
filescahe_init();
dcache_init();
vma_init();
buffer_init();
page_cache_init()
page_cache_init();
kiobuf_setup();
signals_init();
bdev_init();
inode_init();
file_table_init();
ipc_init();
check_bugs();
smp_init();
kernel_thread(init, ...)
cpu_idle();
init() - init/main.c
- lock_kernel();
do_basic_setup();
- [MTRR] mtrr_init();
[SYSCTL] sysctl_init();
[PCI] pci_init();
[SBUS] sbus_init();
[PPC] ppc_init();
[MCA] mca_init();
[ARCH_ACORN] ecard_init();
[ZORRO] zorro_init();
[DIO] dio_init();
[MAC] nubus_init();
[ISAPNP] isapnp_init();
[TC] tc_init();
sock_init();
[BLK_DEV_INITRD] <set up variables>
do_initcalls();
filesystem_setup();
[CONFIG_IRDA] irda_device_init();
[PCMCIA] init_pcmcia_ds();
[HOTPLUG] net_notifier_init();
mount_root();
mount_devfs_fs ();
free_initmem();
unlock_kernel();
if (open("/dev/console", O_RDWR, 0) < 0)
printk("Warning: unable to open an initial console.\n");
(void) dup(0);
(void) dup(0);
if (execute_command)
execve(execute_command,argv_init,envp_init);
execve("/sbin/init",argv_init,envp_init);
execve("/etc/init",argv_init,envp_init);
execve("/bin/init",argv_init,envp_init);
execve("/bin/sh",argv_init,envp_init);
panic("No init found. Try passing init= option to kernel.");
次のリストに挙げた人々は気前よくフィードバックを与えてくれた.できる限り完全なリストを維持する努力にもかかわらず,残念ながら多くの人々がまだ抜けている.
Dirk Behme <dirk.behme@de.bosch.com> Fillod Stephane <FillodS@thmulti.com> Geoffrey Espin <espin@idiom.com> Gerald Champagne <gerald.champagne@esstech.com> Henri Girard <khgirard@broadbandnetdevices.com> Neal Crook <ncrook@micron.com> Steven J. Hill <sjhill@realitydiluted.com> TAKANO Ryousei <takano@axe-inc.co.jp>