マルチタスクを実装し、その原理を理解しようH8マイコンボードで動作する組み込みOSを自作してみよう!(4)(2/3 ページ)

» 2011年07月25日 10時55分 公開
[坂井弘亮,@IT MONOist]
※本記事はアフィリエイトプログラムによる収益を得ています

3.タスク切り替えをどのように実装するか?

 タスク切り替えを行う際に必要なことは、以下の2点です。

  • 現在の状態を保存する
  • 以前に保存されていた状態に復帰する

 実はこれはCPUの「割り込み」の処理と似ています。タスク切り替えは、CPUの割り込み機能をうまく利用することで実現できます。

 図2は、一般的な割り込み処理の概略図です。図中の「PC」は「プログラムカウンタ」で、現在の実行位置を指しています。「SR」は「ステータスレジスタ」で、CPUの状態を保存しているレジスタです。多くのCPUは、割り込みの発生と同時にPCとSRの値をどこかに保存します(注2)。

割り込み処理の概略図 図2 割り込み処理の概略図
※注2:多くの場合、スタックか専用レジスタに保存します。


 「RFI」は“Return From Interrupt”の略で、「割り込み復帰命令」などとも呼ばれます。RFI命令を実行すると、PCとSRの値が保存先から復元されます。割り込み発生時のPCとSRの退避と、RFI実行時のそれらの復元は、CPUが自動的に行います。このため、割り込み処理ルーチンの内部では、必要な処理を行った最後にRFIを呼び出せば、割り込み発生前の状態に復帰することになります。

 しかし、これだけでは実際には不十分です。というのは、割り込み処理ルーチンの中でも汎用レジスタを使ったりしなければ、現実的な処理ルーチンは書けません。このため、実際にはPCとSRだけでなく、汎用レジスタの退避も必要になるのですが、一般的なCPUはこれらの汎用レジスタまで自動的に退避・復元してはくれません(注3)。

※注3:これを行うと、CPUの内部構造が複雑になってしまうためです。「レジスタウィンドウ」という機構を使ってハードウェア的に汎用レジスタの退避を行ったり、「シャドーレジスタ」という機構で汎用レジスタの退避を不要にしたりしているCPUもあります。


 実際の割り込み処理は多くの場合、図3のように書きます。PCとSRの退避はCPUが自動的に行いますが、その後の割り込み処理ルーチンの先頭では、汎用レジスタをスタックなどに退避する処理を行った後、本来の処理ルーチンを呼び出します。割り込み処理の終了後には、退避してある汎用レジスタの値を復元し、さらにRFIを呼び出すことでPCとSRの値も復元され、元の処理に戻ります。つまり、汎用レジスタの退避と復元処理をソフトウェア的に行っているわけです。

汎用レジスタの退避を行う 図3 汎用レジスタの退避を行う

 図3では、レジスタの退避・復元先は1箇所だけでしたが、ここで、レジスタの退避場所を複数用意することを考えてみましょう。そして、さらに、割り込み処理から元の処理に戻る際(つまり、RFI命令を呼び出す際)、幾つかの退避箇所の中から適当なものを選んで復元するようにしてみましょう(図4)。

レジスタの退避場所を複数にすることで、マルチタスクが実現できる 図4 レジスタの退避場所を複数にすることで、マルチタスクが実現できる

 図4のようにすれば、複数の処理状態を切り替えながら実行を進めることができます。これがマルチタスクの実装原理です。

 なお、タスクごとの保存する情報を「コンテキスト」、次に動作させるタスクを選択する処理を「スケジューリング」、そのタスクの情報を復帰して動作を再開することを「ディスパッチ」と呼びます。

4.タスク切り替えを試してみよう

 では、KOZOSのタスク切り替えを試してみましょう。まずは、KOZOSのWebサイトから「osbook_03.zip」をダウンロードし解凍してください。

 前回は、「06」というフォルダのプログラムを動作させました。これはオリジナルのブートローダーと、ブートローダーから起動する「Hello World」のプログラムでした。今回動作させるのは「09」というフォルダです。ここにも前回の「06」と同様に、「bootload」「os」という2種類のソースコードがあります。「bootload」がブートローダー、「os」がブートローダーから起動して動作させるプログラムになります。手順としては前回と同様、マイコンボードのフラッシュROMにブートローダーを書き込んでおき、電源ONでブートローダーを起動し、シリアル経由でOSを転送し、起動するというものになります。

 ビルドの方法は前回と同様です。まずは「bootload」のフォルダに入って「make」コマンドを実行することで、ブートローダーの実行モジュールを作成します。さらに、「make image」を実行することにより「kzload.mot」というファイルが作成されます。これがブートローダーのマイコンボードへの書き込み用のファイルになります。

 さらに「os」のフォルダに入って「make」を実行することで、「kozos」というファイルが作成されます。これがOSの実行モジュールになります。

 それでは、実際に動作させてみましょう。前回と同様の操作でブートローダーをマイコンボードのフラッシュROMに書き込みます。方法については前回までの説明を参考にしてください。

 なお現在、「h8write」の幾つかの不具合を修正し、新たに実装しなおされた「kz_h8write」がshintaさんによって作成されています。Linux版とWindows版があり、また、FreeBSDでの動作も確認が取れています。フラッシュROMへの書き込みがうまくいかない場合には、こちらも利用してみてください。


 書き込み後、マイコンボードを起動します。PC側では「Tera Term」などの端末エミュレータによってマイコンボードと接続します。ブートローダーが起動し、シリアル経由でPC側にリスト11のような「kzload>」というプロンプトが出力されます。

kzload (kozos boot loader) started.
kzload> 
リスト11 起動時の出力

 この状態で「load」を実行し、XMODEM経由でOSの実行モジュールである「kozos」をダウンロードし、続けて「run」を実行することでOSを起動します。具体的な方法は、前回の説明を参考にしてください。

 リスト12は、OS起動後の出力です。リスト12を見ると、「test09_1」「test09_2」「test09_3」というタスクがいろいろと切り替わりながら動作を進めていることが分かります。

kzload> run
starting from entry point: ffc020
kozos boot succeed!
test09_1 started.
test09_1 sleep in.
test09_2 started.
test09_2 sleep in.
test09_3 started.
test09_3 wakeup in (test09_1).
test09_1 sleep out.
test09_1 chpri in.
test09_3 wakeup out.
test09_3 wakeup in (test09_2).
test09_2 sleep out.
test09_2 chpri in.
test09_1 chpri out.
test09_1 wait in.
test09_3 wakeup out.
test09_3 wait in.
test09_2 chpri out.
test09_2 wait in.
test09_1 wait out.
test09_1 trap in.
test09_1 DOWN.
test09_1 EXIT.
test09_3 wait out.
test09_3 exit in.
test09_3 EXIT.
test09_2 wait out.
test09_2 exit.
test09_2 EXIT.
リスト12 転送したモジュールの起動

5.タスク構成

 リスト12で動作させた3つのタスクのメイン関数は、それぞれリスト13、リスト14、リスト15のようになっています。

……(中略)……
 
int test09_1_main(int argc, char *argv[])
{
  puts("test09_1 started.\n");
 
  puts("test09_1 sleep in.\n");
  kz_sleep();
  puts("test09_1 sleep out.\n");
 
  puts("test09_1 chpri in.\n");
  kz_chpri(3);
  puts("test09_1 chpri out.\n");
 
  puts("test09_1 wait in.\n");
  kz_wait();
  puts("test09_1 wait out.\n");
 
  puts("test09_1 trap in.\n");
  asm volatile ("trapa #1");
  puts("test09_1 trap out.\n");
 
  puts("test09_1 exit.\n");
 
  return 0;
}
リスト13 タスク1のメイン関数(test09_1.c)

……(中略)……
 
int test09_2_main(int argc, char *argv[])
{
  puts("test09_2 started.\n");
 
  puts("test09_2 sleep in.\n");
  kz_sleep();
  puts("test09_2 sleep out.\n");
 
  puts("test09_2 chpri in.\n");
  kz_chpri(3);
  puts("test09_2 chpri out.\n");
 
  puts("test09_2 wait in.\n");
  kz_wait();
  puts("test09_2 wait out.\n");
 
  puts("test09_2 exit.\n");
 
  return 0;
}
リスト14 タスク2のメイン関数(test09_2.c)

……(中略)……
 
int test09_3_main(int argc, char *argv[])
{
  puts("test09_3 started.\n");
 
  puts("test09_3 wakeup in (test09_1).\n");
  kz_wakeup(test09_1_id);
  puts("test09_3 wakeup out.\n");
 
  puts("test09_3 wakeup in (test09_2).\n");
  kz_wakeup(test09_2_id);
  puts("test09_3 wakeup out.\n");
 
  puts("test09_3 wait in.\n");
  kz_wait();
  puts("test09_3 wait out.\n");
 
  puts("test09_3 exit in.\n");
  kz_exit();
  puts("test09_3 exit out.\n");
 
  return 0;
}
リスト15 タスク3のメイン関数(test09_3.c)

 各タスクのメイン関数は、リスト16の処理で起動されます。

……(中略)……
 
/* システムタスクとユーザースレッドの起動 */
static int start_threads(int argc, char *argv[])
{
  test09_1_id = kz_run(test09_1_main, "test09_1",  1, 0x100, 0, NULL);
  test09_2_id = kz_run(test09_2_main, "test09_2",  2, 0x100, 0, NULL);
  test09_3_id = kz_run(test09_3_main, "test09_3",  3, 0x100, 0, NULL);
 
  ……
}
 
int main(void)
{
  ……
 
  /* OSの動作開始 */
  kz_start(start_threads, "idle", 0, 0x100, 0, NULL);
  /* ここには戻ってこない */
 
  ……
}
リスト16 各タスクの起動(main.c)

Copyright © ITmedia, Inc. All Rights Reserved.