カテゴリー別アーカイブ: ETロボコン

EV3RTを使ってみる(6)

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

2015年度の新学期がはじまり、ETロボコンに向けて本格的に活動を開始している人達もいるかと思います。そのような人にとって少しでも役に立てばと思ってこの記事を書いていますが、記事を書くのが遅いので役に立っているか分かりません。さて、今回はイベント機能について書いていきます。あと、近日中に周期処理とBluetooth通信についても書いていく予定です。

イベント機能といっても、「ボタンを押したら○○する」といった処理を行うものではなく、イベントフラグを設定したり、イベントフラグが設定されるのを待機したりというような同期機能のことです。この機能を使うことで「他のタスクの処理が終了するまで待機する」といった同期処理を実装することができます。

では、今回のプログラムですが、下図に描いた流れで動作するプログラムを作成していきます。まず、高優先度タスクtask1と低優先度タスクtask2を作成し、これらを並列で実行します。そして、task2の実行中はtask1は待機し、task2の処理終了後にtask1の処理を実行するようにします。このように動作するプログラムをイベント機能を使って実装していきます。

2015-04-07-1

まずは、イベントフラグを作成するようapp.cfgに記述します。CRE_FLGの部分がイベントフラグを作成している部分で、ここではFLG1というIDのイベントフラグを作成しています。また、TA_TPRIで優先度を考慮したキューイングをするように設定しており、最後の引数0x00でイベントフラグの初期値を設定しています。これで、0x00に設定されたイベントフラグFLG1が作成されます。それ以外の部分(タスク生成など)は今までと同じですね。

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_FLG(FLG1, { TA_TPRI, 0x00 });
}

ATT_MOD("app.o");

ヘッダファイルapp.hは今までと同じです。

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

ソースファイルapp.cにtask1とtask2の内容を書いていきます。task2はcount2の値を増やしながら表示処理を時間制限まで行い、制限時間を過ぎるとイベントフラグFLG1の値を0x01に変更して終了します。関数task2の中のset_flgの部分がイベントフラグFLG1の値を0x01に変更している部分です。一方、task1はイベントフラグFLG1の値が0x01に変化するまで待機し、イベントフラグFLG1の値が0x01になるとcount1の値を増加させて表示処理を行います。関数task1の中のwai_flgの部分がイベントフラグFLG1の値が0x01になるまで待機している部分です。また、clr_flgの部分でイベントフラグの値を0x00に戻しています 1

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

int count1 = 0;
int count2 = 0;

/* 高優先度タスク */
void task1(intptr_t exinf)
{
  FLGPTN flag;

  wai_flg(FLG1, 0x01, TWF_ANDW, &flag);
  clr_flg(FLG1, 0x00);

  count1 += 1;
  display();
}

/* 低優先度タスク */
void task2(intptr_t exinf)
{
  while(getTime() < 10000){
    count2 += 1;
    display();
    tslp_tsk(100);
  }

  set_flg(FLG1, 0x01);
}

/* 状態を表示する。 */
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;
}

kernelと一緒にコンパイルして実行してみると、task2の処理が行われてからtask1の処理が行われているのを確認できます。

2015-04-07-2

イベント機能は単純な機能ですが、初期化処理が終了するのを待機したり、通信データが到着するのを待機したりと使い勝手のよい機能だと思います。詳しい使い方はToppersのマニュアルに書かれていますので、イベント機能を使い倒したい方はマニュアルを参考にしてください 2

Notes:

  1. このプログラムでは何の意味もありません。
  2. 本気で開発するのであれば、一次資料を参考にするのは当たり前ですけど。

EV3RTを使ってみる(5.5)

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

前回の記事で排他制御の1つであるmutex機能の紹介をしましたが、このmutex機能を使うときに注意しなくてはならないことがあるようですので、今回は下記のプログラムを使ってmutexの実装方式を確認してみます。今回使うプログラムは前回のプログラムとほとんど同じですが、高優先度タスクであるtask1の中のアンロック処理(unl_mtx)とロック処理(loc_mtx)との間にあったsleep処理(tslp_tsk)を削除しています。つまり、task1はMTX1をアンロックすると待機処理を含まずに再びロックを試みるようなプログラムになっています。

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");
void task1(intptr_t exinf);
void task2(intptr_t exinf);
void display();
ulong_t getTime();
#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);
    /* 待機せずにMTX1のロックを試みる */
  }
}

/* 低優先度タスク */
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;
}

さて、この排他制御の実装方式には大きく分けて2通りが存在するため、このプログラムを実行した場合に予想される実行結果も2通り存在します。1つ目の実装方式は、アンロック処理(unl_mtx)では資源の解放だけを行い、ロック処理(loc_mtx)が呼ばれたときに資源を獲得するタスクを判定する方式です。この場合、高優先度タスクであるtask1が資源を専有することになりますので、下図の実行パターンAのようにtask1が資源を使い続けることになります。2つ目の実装方式は、アンロック処理(unl_mtx)が呼ばれた時に待機状態となっているタスクに資源解放を通知する方式です。この方式の場合、task1のアンロック処理(unl_mtx)からtask2のロック処理(loc_mtx)に処理が移ることになりますので、下図の実行パターンBのようにtask1とtask2が交互に実行されることになります。

2015-04-01-1

実際に上のプログラムを実行してみたところ、実行結果は上図の実行パターンBと同じ結果になりました。EV3RTのmutex実装では、アンロック処理(unl_mtx)を呼び出した時に待機状態のタスクに対して実行可能であることを通知する仕組みになっているようです 1

2015-04-01-2

さて、この実装方式を採用しているmutex機能ですが、使用上の注意点があります。それは、高優先度タスクがアンロック処理を実行した時点で、低優先度タスクのロック処理が実行される可能性があるという点です。例えば、高優先度タスクの中でロック処理とアンロック処理を繰り返している場合、高優先度タスクの処理途中に低優先度タスクの処理が割り込んでしまう可能性があります。そして、低優先度タスクの排他制御範囲内の処理が長い時間を要する場合、それは高優先度タスクの実行時間に決定的な影響を与えるかもしれません。

以前、ETロボコンのモデリング審査委員の先生から排他制御の設計についても書いて欲しいという話を聞いたのですが、この辺りの機能や特性を理解してモデリングしているのかを見たいのかもしれません。5ページしかないモデリング資料に排他制御の設計まで書くスペースなんか無いよと思ったりもしますけどね。

Notes:

  1. まさに教科書通りの正統派の実装方式ですね。

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の仕様書にしっかり書かれています。

EV3RTを使ってみる(4)

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

2015年度のETロボコンの申し込みが開始されました。私もさっそく申し込んだのですが「今年もあなたのチームは1人だけなの?マジで大丈夫??去年も同じようなこと聞いたけどさ。」という内容(もちろん、本当の文面は十分に丁寧なものです)のメールをもらったりしています。一緒に参加してくれるような友達がいないので仕方がありません。そんなことよりも、若手技術者を対象とした大会なのに、おっさんが参加してもよいものか悩んでいます 1

さて、EV3RTでは複数のタスクを並列に実行することができます。ただし、並列に実行するといっても、最初に最も優先度の高いタスクが実行され、そのタスクが停止している間に次に優先度の高いタスクが実行されるという方式です 2。今回は、このマルチタスク実行時の優先度制御機能を確認していきます。

先日の記事で書いたように、現在のToppers/EV3RTのバージョンでは、ダイナミックローダを使うとマルチタスクを制御するための排他制御機能が正しく動きません。というわけで、今回からはkernelと一緒にコンパイルしてSDカードからロードする方式で実行確認をしています。

今回作成するプログラムは、優先度の高いタスク「task1」と優先度の低いタスク「task2」を作成し、これらのタスクを起動時に実行します。ただし、「task1」にはsleep時間が存在し、「task2」にはsleep時間が存在しません。予想される実行順序は下図のようになります。

2015-03-13-1

優先度の高い「task1」が実行されている間は「task2」は待機状態になっており、「task1」がsleep状態になっている間に「task2」が実行されるプログラムです。

まずは、2個のタスクを定義するapp.cfgを作成します。優先度の高い「task1」と優先度の低い「task2」を定義しています。ここで、task1の優先度をしている場所は「TMIN_APP_TPRI + 1」ですが、この優先度の設定値が低いタスクほど優先度の高いタスクとなります。

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 });
}

ATT_MOD("app.o");

つぎに、ヘッダファイルapp.hです。それぞれのタスクに対応する関数「task1」「task2」の他に、状態表示のための関数「display」と実行時間を計算する関数「getTime」を定義しています。

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

最後に、プログラムの本体app.cです。関数task1が優先度の高いタスクとして実行される関数で、count1の値を増やして表示処理を行った後にsleep(tslp_tsk)するように実装しています。一方、関数task2は優先度の低いタスクとして実行される関数で、処理の途中にsleepする箇所はありません。

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

int count1 = 0;
int count2 = 0;

/* 高優先度タスク */
void task1(intptr_t exinf)
{
  while(getTime() < 10000){
    count1 += 1;
    display();
    tslp_tsk(100);
  }
}

/* 低優先度タスク */
void task2(intptr_t exinf)
{
  while(getTime() < 10000){
    count2 += 1;
    display();
  }
}

/* 状態を表示する。 */
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;
}

上記のプログラムをEV3の実機で実行してみたところ、count1の値も増加しているのが確認できました。上の図に書いたように、task1がsleepしている間にtask2が実行されているようです 3

2015-03-18-1

組み込み分野のプログラムを作成していると、「優先度は低いけど長時間かかる処理」を実行しながら「優先度は高いけど短時間で終了する処理」を実行しなければならないという状況が多いのですが、これであれば問題なく対応できそうです。

Notes:

  1. 年齢的にはおっさんですが、組み込み分野の技術力という意味でははまだまだ初心者レベルなので、大会に参加させてください
  2. Mindstorms EV3はSitara ARM Processorを採用しているので、COREの数は1個だけです。その上、リアルタイム性重視の組み込み環境であることを考えると、TSSよりも現実的な実装方法ですね。
  3. そうでなければ、task1は1回しか実行されないはずです

EV3RTを使ってみる(3.5)

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

ここ最近、時間を見つけてはToppers/EV3RTの動作確認をしていたりします。ただ、私はToppersの中の人ではない 1ので、動作仕様については公式ドキュメントに書かれている以上のことは知りません。ですから、あまり難しいことは考えず、「あー、こんな動作するんだー」という感じのいい加減な動作確認をしています。

さて、現在 2のEV3RT最新版はβ3-1となっていますが、まだまだ開発中の段階のようで、センサーポート1番がタッチセンサしか認識しないといった不具合が報告されています 3。また、ダイナミックローダを使った場合、ミューテックス機能はサポートされておらず、セマフォ機能は動作しない 4ので、まともに排他制御をするのは難しいと思われます。というわけで、マルチタスクのプログラムを作る場合は、ダイナミックローダを使う方式ではなく、カーネルと一緒にコンパイルしたプログラムをSDカード経由でロードする方式をとった方がよさそうです。SDカードをEV3本体から引き抜くときに苦労してしまいますが。。。

Toppers/EV3RTの方も開発が継続されているようですので、今後はダイナミックローダでも排他制御機能を使えるようになるかもしれません。とはいっても、現在のバージョンであっても、ダイナミックローダを使わななければ排他制御機能を問題なく利用できますし、シングルタスクのプログラムであれば排他制御を必要としませんのでダイナミックローダを使っても問題ありません。

Notes:

  1. Toppers会員でもありません。というか、私は組み込み分野の人ではないのでToppersの中の人とは接点がないかも。
  2. 2015年3月12日
  3. 最初にサンプルプログラムを実行したときは、ポート1を指定したときだけフリーズしたので、ハードウェアの初期不良かと勘違いしてしまいました。
  4. さらに、アプリケーションのロードができなくなります。カーネルかダイナミックローダにバグがあるのかも?