EV3RTを使ってみる(5)

Toppers/EV3RTを使ってプログラムを作成したときの記事です。
この記事を書いた時のEV3RTのバージョンはβ3です。
目次はこちら

前回の記事で「1人でETロボコンに参加予定」という趣旨のことを書きましたが、1人で参加すると色々と大変なのでお勧めできません。特に、モデル締め切り前や大会前になると作業が多く、1人チームの場合はわけがわからない状態になります。あと、レース前にキャリブレーションの時間があるのですが、この間にインタビューにも応える必要がありますので、キャリブレーションや車体の位置合わせに使える時間は20秒程度しかありません。実際、私が去年の参加したときは、車体を適当にスタート地点近くに置いて命令を出すだけでキャリブレーションと位置合わせを自動で行うように準備していました 1

というわけで、1人チームが大変だということはわかっているのですが、私は今年も1人チームです 2

さて、マルチタスクのプログラムを書くときに必須となる排他制御のプログラムを紹介していきます。Toppers/EV3RTでは複数の排他制御機能がサポートされているのですが、今回はその排他制御機能の中のmutexを使っていきます。ちなみに、他にはsemaphoreを使った排他制御ができますし、event queueやmessage queueを使って同期制御もできるようです。

今回のプログラムでは2個のタスク「task1」「task2」が並列に実行され、それぞれのタスクでは変数「count1」もしくは「count2」の値を増加させた後に一定時間sleepします。ただし、10回分(count1もしくはcount2の値が10だけ増加するまで)の処理中にはもう一方の処理が行われないように排他制御を行います。図で書くと下図のような流れになって、それぞれの実行フェーズで「変数countの値の増加」「一定時間sleep」を10回繰り返します。

2015-03-17-1

このような動作をするプログラムをmutexの機能を使って実装していきます。

まずは、ファイルapp.cfgにタスクやmutexに関する記述を書いていきます。タスクに関する記述は今までと同じです。一方、CRE_MTXと記述しているところがmutexのに関する記述で、MTX1というIDをもつmutexを作成しています。あと、TA_TPRIと書いておくとタスクの優先度を考慮してキューイングしてくれるみたいですが、今のところ私は効果をよく理解していません。

INCLUDE("app_common.cfg");

#include "app.h"

DOMAIN(TDOM_APP) {
  CRE_TSK(TASK1, { TA_ACT, 0, task1, TMIN_APP_TPRI + 1, STACK_SIZE, NULL });
  CRE_TSK(TASK2, { TA_ACT, 0, task2, TMIN_APP_TPRI + 2, STACK_SIZE, NULL });
  CRE_MTX(MTX1, { TA_TPRI, 0 });
}

ATT_MOD("app.o");

次にヘッダファイルapp.hですが、こちらは今までと同じです。

void task1(intptr_t exinf);
void task2(intptr_t exinf);
void display();
ulong_t getTime();

最後にプログラムの本体であるapp.cです。まず、プログラムの中でapp.cfgで定義したID(MTX1)を使うことになりますので、このIDを引っ張ってくるためにkernel_cfg.hをincludeしています。ただし、このkernel_cfg.hはkernelごとコンパイルするときのヘッダファイルでして、ダイナミックローダを使う場合はmodule_cfg.hをincludeすることになります 3。次に、mutex(MTX1)で排他制御を開始する場合はloc_mtx(MTX1)と書き、排他制御を終了する場合はunl_mtx(MTX1)と書きます。loc_mtxとunl_mtxの間に書かれた処理が排他制御の対象領域となります。

#include "ev3api.h"
#include "app.h"
#include "kernel_cfg.h"

int count1 = 0;
int count2 = 0;

/* 高優先度タスク */
void task1(intptr_t exinf)
{
  while(getTime() < 10000){
    loc_mtx(MTX1);

    for(int i = 0; i < 10; i++){
      count1 += 1;
      display();
      tslp_tsk(100);
    }

    unl_mtx(MTX1);
    tslp_tsk(100);
  }
}

/* 低優先度タスク */
void task2(intptr_t exinf)
{
  while(getTime() < 10000){
    loc_mtx(MTX1);

    for(int i = 0; i < 10; i++){
      count2 += 1;
      display();
      tslp_tsk(100);
    }

    unl_mtx(MTX1);
    tslp_tsk(100);
  }
}

/* 状態を表示する。 */
void display()
{
  char message1[32];
  char message2[32];
  char message3[32];
  int time = (int) getTime();

  sprintf(message1, "time = %d", time);
  sprintf(message2, "count1 = %d", count1);
  sprintf(message3, "count2 = %d", count2);
  
  ev3_led_set_color(LED_GREEN);
  ev3_lcd_set_font(EV3_FONT_MEDIUM);
  ev3_lcd_draw_string(message1, 20, 30);
  ev3_lcd_draw_string(message2, 20, 50);
  ev3_lcd_draw_string(message3, 20, 70);
}

/* 経過時間を返す。 */
ulong_t getTime()
{
  static ulong_t start = -1;
  ulong_t time;

  get_tim(&time);

  if(start < 0){
    start = time;
  }

  return time - start;
}

上のプログラムの例では、関数task1の中にloc_mtxとunl_mtxがあり、その間でcount1を増加させています。また、関数task2の中にもloc_mtxとunl_mtxがあり、その間でcount2を増加させています。これらの処理は同時には行われないため、count1とcount2の値は10ずつ増加することになります。EV3で実行してみると下のような結果になりました。

2015-03-18-2

これで排他制御もできます。ただ、使い方にも注意点があるようで、その辺は次回にでも書こうかなと思っています 4

Notes:

  1. それでもバタバタしてしまった。
  2. ぼっちだから仕方ないね。
  3. ただし、ダイナミックローダを使った場合、mutex機能は動作しません。
  4. mutexを使うときの注意点はToppersの仕様書にしっかり書かれています。