連載
» 2011年07月25日 10時55分 UPDATE

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

連載「H8マイコンボードで動作する組み込みOSを自作してみよう!」の第4回。今回は、“タスク切り替え”の動作原理を理解しながら、マルチタスクを実装し、よりOSらしく発展させていきます。

[坂井弘亮,@IT MONOist]

 本連載では、学習用・ホビー用の組み込みOS「KOZOS」を使ってマイコンボード上でいろいろと実験をしつつ、フルスクラッチで組み込みOSを自作していく過程を体験していきます。最終的に、ソフトウェア完全自作のWebサーバを動かすことにチャレンジします!

 さて、前回までの内容で、ブートローダーが動作し、そして、ブートローダーからシリアル経由でプログラムをダウンロードして起動させることができました。前回は、その“ブートローダー経由で起動するプログラム”として「Hello World」を動作させたわけですが、今回はこの「Hello World」を発展させて、“タスク切り替えを実装”してみます(いよいよOSらしくなっていきます!)。

1.マイコンボードとソースコード

 本連載では、秋月電子通商の「H8/3069Fネット対応マイコンLANボード(完成品)」(図1)を利用します。マイコンボードの詳細については連載第1回を参照してください。ちなみにこのボードは、3750円(税込)と非常に安価で入手性も良く、個人のホビー用途にオススメです。

本連載で利用するH8/3069Fマイコンボード 図1 本連載で利用するH8/3069Fマイコンボード

 本連載で紹介するソースコードは、KOZOSのWebサイトからダウンロードできます。また、開発環境の構築方法は連載第1回で説明してありますので、興味のある方はぜひソースコードをビルドし、実機での動作を試してみてください(幾つか対応は必要となりますが、シミュレータ上でも動作させることが可能です)。

 なお、上記ソースコードは、筆者の書籍「12ステップで作る組込みOS自作入門」の各章(ステップ)に応じたものになっています。書籍の方にも詳しい説明がありますので、そちらも参考にしてみてください。

参考文献[1]

  • 「12ステップで作る組込みOS自作入門」(カットシステム/著:坂井 弘亮)


2.タスク切り替えとは何か?

 前回、ブートローダーから起動したプログラムは、「Hello World」をシリアルに出力後、コマンド入力待ちになるというものでした。この実装はリスト1のような構造になっています。

  while (1) {
    ……
    gets(buf);
 
    if (!strncmp(buf, "echo", 4)) {
      ……(コマンド処理)……
    }
  }
リスト1 コマンド入力待ちの処理

 リスト1で利用している「gets()」は、シリアルからの1行入力を待ち合わせるサービス関数です。「gets()」の内部では「getc()」が呼ばれ、シリアルから1文字入力されるのを見張って待っています。これはリスト2のような構造になっています。

/* 1文字受信 */
unsigned char serial_recv_byte(int index)
{
  ……
 
  /* 受信文字がくるまで待つ */
  while (!(sci->ssr & H8_3069F_SCI_SSR_RDRF))
    ;
  c = sci->rdr;
  ……
 
  return c;
}
 
/* 1文字受信 */
unsigned char getc(void)
{
  unsigned char c = serial_recv_byte(……);
  ……
  return c;
}
/* 文字列受信 */
int gets(unsigned char *buf)
{
  ……
  do {
    c = getc();
    if (c == '\n')
      c = '\0';
    buf[i++] = c;
  } while (c);
  ……
}
リスト2 gets()の実装

 実際の1文字受信を行うのは、リスト2の「serial_recv_byte()」という関数ですが、ここではシリアルコントローラの「SSR」というレジスタの「RDRF」ビットをループで見張ることで、受信文字が到着するまで待ち合わせています。このような処理のことを「ビジーループ」などと呼びます。つまり、文字到着までwhileループでずっと見張っているわけです。

 これとは別に、例えば、LEDを定期的に点滅させるような処理を考えてみましょう。もし、1秒間隔でLEDを点滅させたいのならば、リスト3のように書けばいいことになります。

  while (1) {
    if (1秒経過した)
      led_turn();
  }
リスト3 LEDの点滅処理

 リスト1とリスト3は、それぞれ単独で動かす分には特に問題なく動作します。しかし、ここで問題になるのは、これらを“同時に動作させたい”というケースです。つまり、「シリアル経由でコマンド受け付けしながらLEDを点滅させる」という処理がしたい場合です。これはリスト4のように、リスト1とリスト3を単純に連結しただけではうまくいきません。

  while (1) {
    if (1秒経過した)
      led_turn();
 
    ……
    gets(buf);
 
    if (!strncmp(buf, "echo", 4)) {
      ……(コマンド処理)……
    }
  }
リスト4 コマンド応答処理とLED点滅処理を連結する

 リスト4では、「gets()」を呼び出しています。しかし、「gets()」の延長ではリスト2の「serial_recv_byte()」が呼ばれ、ビジーループでシリアル受信を待ち合わせてしまいます。このため「gets()」の呼び出しは、文字列受信までブロックしてしまい、「led_turn()」が定期的に呼ばれることはありません。つまり、LEDの点滅が実際には行われないのです。

 この問題を回避するには、「serial_recv_byte()」や「gets()」をリスト5のように修正する必要があります。

/* 1文字受信 */
int serial_recv_byte(int index)
{
  ……
 
  /* 受信文字がない場合には−1を返す */
  if (!(sci->ssr & H8_3069F_SCI_SSR_RDRF))
    return -1;
  ……
 
  return c;
}
 
/* 文字列受信 */
int gets(unsigned char *buf)
{
  ……
  do {
    c = getc();
    /* 受信文字がない場合にはNULLを返す */
    if (c < 0) return NULL;
    ……
  } while (c);
  ……
}
リスト5 ビジーループを行わない例

 リスト5では、受信文字がない場合、「gets()」は“NULL”で返ります。このため、受信文字の到着までビジーループでブロックせずに、「gets()」からは毎回戻ってくることになります。メインループはリスト6のように書けばいいでしょう。

  while (1) {
    if (1秒経過した)
      led_turn();
 
    ……
    /* 受信文字がない場合は処理を続行する */
    if (gets(buf) == NULL)
      continue;
 
    if (!strncmp(buf, "echo", 4)) {
      ……(コマンド処理)……
    }
  }
リスト6 受信文字がない場合は処理を継続する

 リスト6では、「gets()」の戻り値が“NULL”のときにcontinueすることで、1秒経過の判定と、「led_turn()」の実行が定期的に行われるようになっています。これならば前述のような問題は発生しません。

 しかし、これで問題が全て解決したわけではありません。例えば、シリアルからのコマンドに「dump」という、メモリダンプを出力するようなコマンドを追加したとしましょう(リスト7)。

  while (1) {
    if (1秒経過した)
      led_turn();
 
    ……
    /* 受信文字がない場合は処理を続行する */
    if (gets(buf) == NULL)
      continue;
    if (!strncmp(buf, "echo", 4)) {
      ……(コマンド処理)……
    } else if (!strncmp(buf, "dump", 4)) {
      ……
      dump(addr, size);
      ……
    }
  }
 
……
 
void dump(char *addr, int size)
{
  ……
  for (i = 0; i < size; i++) {
    putxval(addr[i]);
    ……
  }
  ……
}
リスト7 dumpコマンドを追加する

 ここで問題が再度発生します。リスト7では、「dump」コマンドで指定したサイズ分だけのメモリダンプをシリアルに出力します。実際のダンプ処理を行うのは「dump()」という関数です。

 「dump()」の処理では、sizeの数だけループしてダンプ処理を行います。しかし、ここの処理もビジーループになっています。つまり、メモリダンプ中は、CPUはダンプ処理に専念していることになります。そのため、やはり、LEDの点滅処理が行えません。1秒以上かかるような大量ダンプの際、LEDの点滅が止まってしまうという問題が発生するわけです。

 これを防止するには、リスト8のように、ダンプ処理の中でもLEDの点滅処理を行ってやる必要があります。

void dump(char *addr, int size)
{
  ……
  for (i = 0; i < size; i++) {
    if (1秒経過した)
      led_turn();
    putxval(addr[i]);
    ……
  }
  ……
}
リスト8 ダンプ処理の中でもLEDの点滅処理を行う

 もしくは、リスト9のように定期的にメインループに戻るような処理を行うという方法もあるでしょう。

  while (1) {
    ……
    } else if (!strncmp(buf, "dump", 4)) {
      ……
      current = dump(addr, size, 0);
      ……
    }
 
    if (current)
      current = dump(addr, size, current);
  }
 
……
 
int dump(char *addr, int size, int current)
{
  ……
  for (i = current; i < size; i++) {
    putxval(addr[i]);
    ……
    if ((i % 10) == 9)
      return i + 1;
  }
  return 0;
}
リスト9 ダンプ処理中に定期的にメインループに戻る

 さてここで、リスト8やリスト9をもう一度見てみてください。「LEDの点滅」という同じ処理を何箇所かで行っていたり、ループの中から定期的に戻ったりといったコードが追加されていて、これはお世辞にもきれいなコードとはいえません。

 もちろん、同じコードは関数化するとか、状態遷移を利用するなど、書き方を工夫すれば見やすくできますが、このコードの根本的な問題点は「ビジーループがあってはいけない」ということです。このように「いつ終わるのか分からないようなループ処理」というのは、致命的な問題を引き起こしかねませんし、これを回避するために行った何らかの配慮・対策がかえって、以降のプログラム拡張の際の“破綻”の原因にもなりかねません。

 そこで、もう1つの解決策は、「タスク」という考えを導入することです。「LEDの点滅」と「コマンド応答」という2つの作業は、OSを利用してタスク分割することで、リスト10のようなイメージで書くことができます。

int led_main()
{
  while (1) {
    if (1秒経過した)
      led_turn();
  }
}
 
int command_main()
{
  while (1) {
    ……
    gets(buf);
    ……
  }
}
リスト10 2つのタスクに分割する

 リスト10は、「led_main()」と「command_main()」という2つのメイン関数で構成されています。OSはこれらの関数を適当に切り替えながら、並列で動作させます。たとえループの部分があったとしても、OSが適当に処理を切り替えてくれるため、他の処理が停止してしまうことはありません(注1)。

※注1:これはあくまで“イメージ”であり、実際にはそのままではダメで、割り込みや同期の仕組みを利用するなどの何点かの考慮が必要になります。しかし、以前に比べてはるかに書きやすくなることは確かです。


       1|2|3 次のページへ

Copyright© 2017 ITmedia, Inc. All Rights Reserved.