マルチタスク

実際のアプリケーションでは,LEDの点灯処理(タスク)だけではなく,ディスプレイの表示内容の変更タスクや音楽を再生するタスク,プログラムの実質的な計算を行うタスク(例えばゲームでは次のキャラクタの位置を計算するタスクなど)を同時並行的に実行したいことは多い. ここでは,様々なタスクを並行に実行する方法を考える.

複数のタスクを,それぞれ独自の周期でタイミングよく実行するには,各タスクの目標時刻(次に実行すべき時刻)を管理すればよい. プログラムの中心は「カウンタレジスタの値を読み出して,いずれかのタスクの目標時刻を過ぎていればそのタスクが行うべき処理を実行する(ディスパッチすると言う)」という動作を繰り返すループになる.各タスクが行うべき処理は,ループの中に書いてもよいし,関数にしておいて,目標時刻を過ぎた時にその関数を呼び出すのでもよい.タスクを実行した時は,次の周期のために,そのタスクの目標時刻を更新する(目標値に1周期分を足す)のを忘れないように.

前の章で作ったLEDを3秒周期で点滅させるプログラムを考える. このプログラムは,

  • 3秒周期でLEDを点灯するタスク(タスク1)と
  • それとは 1.5 秒ずれて,3秒周期でLEDを消灯するタスク(タスク2)

の2つのタスクを並行に実行することで実現できる.このプログラムをC言語風に書くと次のようになる.

#define TIMER_HZ    (1000 * 1000)    /* タイマ−の周波数 1 MHz */

  /*
   * r0: 次に LED を点灯するタスクを実行する時刻(タスク1の目標値)
   * r1: 次に LED を消灯するタスクを実行する時刻(タスク2の目標値)
   */
  r0 = 現在時刻 (フリーランニングカウンタの下位32ビット);
  r1 = r0 + 3 * TIMER_HZ / 2;
  for (;;) {
    r2 にフリーランニングカウンタの下位32ビットを読み出す;

    /* タスク1 */
    if (r0 < r2) {
      LEDを点灯する;
      r0 = r0 + 3 * TIMER_HZ;  /* 3秒足す */
    }

    /* タスク2 */
    if (r1 < r2) {
      LEDを消灯する;
      r1 = r1 + 3 * TIMER_HZ;  /* 3秒足す */
    }
  }

演習3.4-1  chap4 のディレクトリを作り,その中にLEDを1秒周期で0.75秒点灯させ,0.25秒消灯させるプログラム ledtask.s を作りなさい. LEDを点灯するタスクと消灯するタスクを並行実行することで実現すること. ledtask.imgを作るMakefileとREADME.mdもpushすること.

(ヒント: 2つのタスクの周期はどちらも1秒. タスク2をタスク1からどれだけ遅らせるかによって, LEDが点灯している時間が変わる.)

演習3.4-2  ディスプレイの一番上の行のドットを左から順に点灯し,その後右から順に消灯するという動作を繰り返すプログラムbar.sを作りなさい.ただし,各ドットについて,点灯させるタスクと消灯させるタスクの二つのタスクを作りなさい(ドットが8個なのでタスクは全部で16個).全てのタスクの周期は3秒とし,各タスクは下の表の時間(マイクロ秒)だけずらしなさい.

bar.imgを作るMakefileとREADME.mdもpushすること.

(1行目しか点灯しないので,最初に1行目のGPIO出力値を0,他の行を1にしておき, その後は各列のGPIO出力値のみ制御すればよい. 演習3.4-1等に比べて,制御対象のLEDが8個に増えるだけ.)

タスク番号タスク1からの遅れ役割
10左端のLED点灯
216,000左から2番目のLED点灯
380,000左から3番目のLED点灯
4192,000左から4番目のLED点灯
5352,000左から5番目のLED点灯
6560,000左から6番目のLED点灯
7816,000左から7番目のLED点灯
81120,000左から8番目のLED点灯
92500,000左端のLED消灯
102484,000左から2番目のLED消灯
112420,000左から3番目のLED消灯
122308,000左から4番目のLED消灯
132148,000左から5番目のLED消灯
141940,000左から6番目のLED消灯
151684,000左から7番目のLED消灯
161380,000左から8番目のLED消灯

補足: タスク数がレジスタ数より多い状況を表した演習問題である. 各タスクの目標時刻をそれぞれレジスタに入れるとレジスタが足りないので, 配列に格納する必要がある. それ以外は上記の3秒周期の例や演習3.4-1と同じだが, どうせ配列で管理するなら, 16個のタスクを個別に記述するより, 以下のようにループを使う方がプログラムが簡潔になる (16個個別に記述しても誤りではない).

  for (;;) {
    r2 にフリーランニングカウンタの下位32ビットを読み出す;

    for (i = 0; i < 16; i++) {
      /* タスク i */
      if (target_time[i] < r2) { /* 目標時刻を過ぎた */
        i に応じた仕事を実行;
        target_time[i] += 周期;  /* 次の目標時刻 */
      }
    }
  }

(この問題では全タスクの周期が等しいので,周期は配列に格納しなくてよい.)

動画

2章では,ディスプレイに画像を表示するときに,1行分表示した後「少し待つ」という処理を挿入していた.しかし,マルチタスクの技法を使えば,定期的に1行分表示するという処理を自然にプログラムすることができる.また,定期的にフレームバッファを書きかえて動画を表示する場合,フレームバッファを書きかえる処理もひとつのタスクにすることで,見通しのよいプログラムを作ることができる.

フレームバッファの内容を表示する処理は,前章までは以下のようなアルゴリズムで実装していた.

  row = 0;
  for (;;) {
    frame_buffer[row]の内容をrow行目に表示;
    少し待つ;
    row行目を消灯;
    row++;
    if (row >= 8) { row = 0; }
  }

「row行目を表示」という処理をループの末尾に移動しても行われる処理は変わらない.そのように移動すると,「『少し待』った後,ひとまとまりの処理を行う」という形になる(下記).

  row = 0;
  for (;;) {
    少し待つ;

    row行目を消灯;
    row++;
    if (row >= 8) { row = 0; }
    frame_buffer[row]の内容をrow行目に表示;
  }

「少し待つ」代わりに「目標時刻になったらこのタスクを実行する」ようにすれば,この章で説明してきたタスクの実現方法と同じ形になる(下記).

  row = 0;
  for (;;) {
    r2 にフリーランニングカウンタの下位32ビットを読み出す;

    if (行表示タスクの目標時刻 < r2) { /* 目標時刻を過ぎた */
      row行目を消灯;
      row++;
      if (row >= 8) { row = 0; }
      frame_buffer[row]の内容をrow行目に表示;

      行表示タスクの目標時刻 += 行表示タスクの周期;
    }
  }

「行表示タスク」以外のタスクも,「目標時刻を過ぎたら実行し,目標時刻を更新する」ように記述すればよい. プログラムのメインループは以下のような形になる. これはこの章の最初のプログラム(LEDを3秒周期で点滅させる)と全く同じ構造であるし,演習3.4-2のプログラムの構造とも同じである(タスク1,タスク2,…,タスク16と並べる代わりにループ化しているだけ).

  for (;;) {
    r2 にフリーランニングカウンタの下位32ビットを読み出す;

    if (タスク1の目標時刻 < r2) {
      タスク1を実行;
      タスク1の目標時刻 += タスク1の周期;
    }
    if (タスク2の目標時刻 < r2) {
      タスク2を実行;
      タスク2の目標時刻 += タスク2の周期;
    }
    ...
  }

もちろん,「タスク X を実行」の部分が複雑な場合は,サブルーチンとして切り出すと見通しがよくなるだろう.

注意: 「タスク X を実行」の中では「時刻待ち」や「空ループでの時間つぶし」をしないこと! それらは必要ないはずだし,ほかのタスクの実行を妨害することになる. 「現在時刻を調べ,目標時刻に達したタスクを起動する」のはメインループに任せ,各タスクは「できるだけ速やかに処理を終わらせてメインループに帰る」よう作るべきである.

演習3.4-3  演習3.2-7 (count.s) で作ったプログラムと同じような動作をするが,タイマを使って精確に1秒毎に表示される数が増えるプログラムを作りなさい.ただし,プログラムは次のタスクで構成しなさい.

  • 1秒毎に動作する,次に表示する数をフレームバッファに書き込むタスク
  • 0.001秒毎に動作する,ディスプレイの1行分を表示するタスク(前の行を消灯して次の行を表示するタスク)

上記の説明に従って,現在時刻を調べるメインループから各タスクが起動される構造のプログラムにすること.

メインのプログラムのファイル名は count100sec.s としなさい.count100sec.imgを作るMakefileとREADME.mdもpushすること. ファイル名はcount100secだが,00から09までを繰り返せばよい.00から99まで繰り返してもよい.

演習3.4-4  次の仕様を満たすストップウォッチのプログラムを作りなさい.

  • ディスプレイに00〜99を表示する.
  • スイッチ1を押すと動作を開始する(または再開する).
  • スイッチ2を押すと動作を停止する.
  • 動作中は 0.1 秒毎に表示する数を増やす.99の次は00を表示する.
  • ストップウォッチの動作とは関係なく,常時1秒の周期(0.5秒点灯,0.5秒消灯)でLEDを点滅させる.

次の数をフレームバッファに書き込むタスク,ディスプレイの1行分を表示するタスク,LEDを点灯するタスク,LEDを消灯するタスクなど,複数の周期的タスクを並行実行することで実現すること. (現在時刻を調べるメインループから各タスクが起動される構造のプログラムにすること.)

メインのプログラムのファイル名は stopwatch.s としなさい. stopwatch.imgを作るMakefileとREADME.mdもpushすること.

補足: スイッチの押下状態の読み出しは,一定周期ごとではなく,フリーランニングカウンタの値の読み出しと一緒に実行するのがよい(つまりタイマーとスイッチの両方をポーリングする). 「計時を開始する」タスクや「時計を止める」タスクはそれぞれ,「目標時刻を過ぎたら」ではなく「スイッチ1が押されていたら」「スイッチ2が押されていたら」実行する.

「計時を開始する」タスクは「表示する数を増やす(フレームバッファを書き換える)」タスクの最初の目標時刻を設定する必要がある(「計時を開始」した時刻を基準に0.1秒ごとに「表示する数を増やす」). 「1行分表示するタスク」「LEDを点灯するタスク」「LEDを消灯するタスク」は,それとは無関係に,プログラム実行開始時から一定の周期で実行し続ければよい.

results matching ""

    No results matching ""