EV3RTを使ってみる(目次)

2015年度のETロボコンよりLego Mindstorms EV3が使えるようになりました。そこで、この開発プラットフォームの1つであるToppers/EV3RTを使って簡単なプログラムを作成していきます。

あくまで非公式な紹介記事ですので、本番プログラムを開発するときは本家サイトの情報を十分に見るようにしてください。また、この記事を書いている時点でのEV3RTのバージョンはβ3〜β4ですので、リリース版では仕様が変更されているかもしれません。

あと、at own your risk でお願いします。

目次

(785) 289-3922: MacOSX上のEV3RT開発環境構築(version=β3)
その2: 表示するだけのプログラムをスクラッチから作成(version=β3)
その3: プログラムを複数のソースファイルに分割(version=β3)
その3.5: EV3RT-β3の不具合について(version=β3)
(980) 432-9494: 複数タスクの実行と優先度の設定(version=β3)
churchwardenize: mutexを使った排他制御(version=β3)
その5.5: mutexを使う場合の注意(version=β3)
240-420-7403: イベントフラグを使った同期処理(version=β3)
その7: 周期処理を行うプログラム(version=β3)
その7.5: タスク優先度を考慮した周期処理(version=β3)
その8: bluetoothを介した通信処理(version=β4)
その8.5: bluetoothを介した通信処理のもう1つの書き方(version=β4)

EV3onRules

少しだけEV3wayのアプリケーション開発を簡単にするフレームワーク「EV3onRules」を作りました。各機能を分離して実装するためのシンプルなフレームワークです。
EV3onRules (github)

大きいアプリケーションのためのuImage

1Mbyte以上(6Mbyte以下)の大きさのアプリケーションでも動的ロードが可能なuImageを作りました。

※ これらのuImageは9072903709のソースファイルの設定を変更して作成したものです。これらのuImageの使用により発生した損害について、開発者は一切責任を負いません。

EV3RTを使ってみる(8.5)

Toppers/EV3RTを使ってプログラムを作成したときの記事です。
この記事を書いた時のEV3RTのバージョンはβ4です。
(807) 629-3167

前回の記事でBluetooth経由で通信を行うプログラムを紹介しましたが、EV3RTでは少し違った書き方もできます。前回紹介したプログラムでは関数「ev3_serial_open_file」でファイルポインタを作成して、このファイルポインタを指定することでデータの送受信を行いました。今回紹介するプログラムでは、デバイスに対してデータの送受信を行うと思われる 1関数「serial_rea_dat」と「serial_wri_dat」を使います。

前回紹介したファイルポインタを指定する方式と比べて、今回紹介する「serial_rea_dat」「serial_wri_dat」を使う方式は少し書き方が異なります。まず、ファイルポインタを作成する必要がありません。前準備なしで「serial_rea_dat」や「serial_wri_dat」を呼び出しても問題ありません。また、Bluetooth通信が開始されていない状態で受信関数「serial_rea_dat」を呼び出した場合、Bluetooth経由でデータを受信するまで処理をブロックします。つまり、プログラムによっては「ev3_bluetooth_is_connected」を使ってBluetoothの接続状態を確認する必要はありません。

さて、前回と同じ動作を行うプログラムを「serial_rea_dat」と「serial_wri_dat」を使って作っていきます。

設定ファイル「app.cfg」とヘッダファイル「app.h」は前回と同じです。

INCLUDE("app_common.cfg");

#include "app.h"

DOMAIN(TDOM_APP) {
  CRE_TSK(TASK1, { TA_ACT, 0, task1, TMIN_APP_TPRI + 1, STACK_SIZE, NULL });
}

ATT_MOD("app.o");
void task1(intptr_t exinf);
void display();

ソースファイル「app.c」ではBluetooth通信を行うために関数「serial_rea_dat」と「serial_wri_dat」を呼び出しています。また、これらの関数を使うためにヘッダファイル「syssvc/serial.h」を読み込んでいます。このプログラムでは接続状態を確認する必要がないため「ev3_bluetooth_is_connected」は使っていません。

#include "ev3api.h"
#include "app.h"
#include <syssvc/serial.h>

#define MESSAGE_LEN  8

static char message[MESSAGE_LEN + 1] = {0};

/* タスク */
void task1(intptr_t exinf)
{
  display();

  while(1){
    int size = serial_rea_dat(SIO_PORT_BT, message, MESSAGE_LEN);
    
    if(size > 0){
      serial_wri_dat(SIO_PORT_BT, message, size);
    }

    display();
  }
}

/* 状態を表示する。 */
void display()
{
  ev3_lcd_set_font(EV3_FONT_SMALL);
  ev3_lcd_draw_string("Program is running", 10, 30);
  ev3_lcd_set_font(EV3_FONT_MEDIUM);
  ev3_lcd_draw_string(message, 10, 40);
}

さて、2通りのBluetooth通信プログラムが作成できるわけですが、どちらが良いかは場合によるかと思います。もし、Bluetooth通信を行うだけであれば今回紹介した方式の方がシンプルなプログラムになりそうです。一方、Bluetooth通信とファイル操作が関係している場合は、前回のファイルポインタを使う方式だと処理を共通化できるかもしれません。

とりあえず、「EV3RTを使ってみる」の記事はここまでにしたいと思います。また、何かテクニックみたいなものを見つけたら記事にするかもしれません。あと、現在のEV3RTのバージョンはβ4ですのでリリース版になったときに仕様が変更されるかもしれません。この記事を参考にされる場合はバージョンの違いには注意してください。

Notes:

  1. EV3RTのソースコードは読んでいませんので、これらの関数の正確な動作はわかりません

9524060696

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

EV3RTのβ4がリリースされました。console機能が追加されたり、bluetooth機能が整理されたり、C++開発環境が整備されたりしています。ただし、現在 (336) 899-5087のEV3RTの最新バージョンはβ4ということですので、リリース版までに動作仕様が変更される可能性がありそうです。ETロボコンのスケジュールを考えるとリリース版を待たずに開発を始めるチームが多いと思いますが、EV3RTの仕様変更に対応できるように設計しておかないとひどい目に会いそうな予感がします。

さて、今回はbluetooth通信のプログラムを作っていきますが、その前にbluetoothの接続名とPINコードの設定を行います。β3版では接続名とPINコードがEV3RTのソースコードにハードコーディングされていました 2。しかし、β4版ではSDカード上に設定ファイル「/ev3rt/etc/rc.conf.ini」を作成し 3、その設定ファイルに接続名とPINコードを記述するようになっています。この設定ファイルの記述例は下記の通りで、「LocalName」が接続名の設定となり、「PinCode」がPINコードの設定となります。

[Bluetooth]
LocalName=Mindstorms EV3
PinCode=0000

次に、EV3本体への接続を確認するのですが、Windows, Linux, OSXからEV3本体へbluetooth経由で接続する方法についてはEV3RTの公式ベージで説明されていますので、こちらを参考にしながらbluetooth経由で接続できることを確認します。

さて、bluetooth経由で接続できることを確認したところで、この通信路を使ってデータの送受信を行うプログラムを作成します。今回のプログラムではタスク「task1」を生成し、このタスク内でbluetooth経由で送られてきたデータを受信し、受信したデータをそのまま返信するようにします。また、受信したデータの内容をLCDに表示するようにします。

まず、設定ファイルapp.cfgにタスク「task1」を実行可能状態で生成するように記述します。

INCLUDE("app_common.cfg");

#include "app.h"

DOMAIN(TDOM_APP) {
  CRE_TSK(TASK1, { TA_ACT, 0, task1, TMIN_APP_TPRI + 1, STACK_SIZE, NULL });
}

ATT_MOD("app.o");

次に、ヘッダファイルapp.hにタスク処理関数「task1」と情報表示関数「display」のプロトタイプ宣言を記述します。

void task1(intptr_t exinf);
void display();

最後にソースファイルapp.cに処理を記述します。タスク「task1」ではbluetooth経由でデータを受信し、受信したデータをそのまま送信しています。また、受信したデータを文字配列変数「message」に保存し、この内容をLCDに表示しています。

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

#define MESSAGE_LEN  8

static char message[MESSAGE_LEN + 1] = {0};

/* タスク */
void task1(intptr_t exinf)
{
  FILE *bt;

  / 接続状態を確認
  while(!ev3_bluetooth_is_connected()){
    tslp_tsk(100);
  }

  / シリアルポートを開く
  bt = ev3_serial_open_file(EV3_SERIAL_BT);

  display();

  / 通信処理
  while(1){
    int size = fread(message, 1, MESSAGE_LEN, bt);
    
    if(size > 0){
      fwrite(message, 1, size, bt);
    }

    display();
  }
}

/* 状態を表示する。 */
void display()
{
  ev3_lcd_set_font(EV3_FONT_SMALL);
  ev3_lcd_draw_string("Program is running", 10, 30);
  ev3_lcd_set_font(EV3_FONT_MEDIUM);
  ev3_lcd_draw_string(message, 10, 40);
}

EV3RTでは、bluetoothを介したデータ通信処理をファイルストリームを扱う場合と同じように実装するとができます。ただし、EV3RTのβ4版ではbluetooth機能の仕様がβ3版から少し変更されまして、bluetoothの接続を完了してから通信処理(open, read, write)を実行しないと正常に通信できないようになりました panda。

そこで、まずは関数「ev3_bluetooth_is_connected」を呼び出し、bluetoothの接続を完了しているかどうかを確認します。もしbluetooth接続を完了していれば関数「ev3_serial_open_file」を実行し、データを送受信するためのファイルポインタを作成します。あとは、作成したファイルポインタを引数にしてfread, fwrite, fgets, fprintfなどの出入力関数を呼び出すことによりデータの送受信処理を実行できます。

このプログラムをEV3本体で実行し、他のPCからbluetooth経由でEV3本体に接続します。この状態でPC側からデータを送信すると、8byteごとに返信と表示を行います。

上記のようなプログラムを作成することでbluetoothを介した通信が可能になります。ただし、受信データが不足している状態でfreadやfgetsなどの入力関数を実行した場合、その入力関数によって処理がブロックされます。そのため、ロボット制御などのリアルタイム性の高いプログラムを実装する場合は「制御タスク」と「通信タスク」をわけて実装したほうがよさそうです。

Notes:

  1. 2015年4月28日現在 (803) 514-5741
  2. 私の環境ではEV3RT-β3のPINコードの設定部分を書き換えても正常には反映されませんでした。
  3. uImageをEV3本体で実行すると設定ファイルが自動的に作成されます。 (515) 445-8152
  4. β3ではfreadを呼び出してからbluetoothの接続を行っても正常に通信出来ました。 5173494310

EV3RTを使ってみる(7.5)

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

(720) 544-6237でEV3RTの周期処理の優先度は通常タスクの優先度より高いということがわかりました。周期処理の優先度が高いということは、1回の周期処理に必要となる時間が長い場合、その周期処理を実行している間は他のタスクが停止してしまうことを意味します。

例えば、以下のプログラムを実装した場合を考えてみます。このプログラムではタスク「task1」と周期処理「cyc1」を実装しています。task1はカウントと表示処理を行うタスクで、1回の処理が終了するごとに0.1秒sleepします。一方、cyc1は2秒おきに呼ばれる周期処理で、1回の処理が終了するまでに1秒の時間を必要としています。また、cyc1にはsleepなどの待機処理は含まれていません。

INCLUDE("app_common.cfg");

#include "app.h"

DOMAIN(TDOM_APP) {
  CRE_TSK(TASK1, { TA_ACT, 0, task1, TMIN_APP_TPRI + 1, STACK_SIZE, NULL });
  EV3_CRE_CYC(CYC1, { TA_STA, 0, cyc1, 2000, 0 });
}

ATT_MOD("app.o");
void task1(intptr_t exinf);
void cyc1(intptr_t exinf);
void display();
ulong_t getTime();
#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 cyc1(intptr_t exinf)
{
  ulong_t start = getTime();

  if(start >= 10000){
    return;
  }

  while(getTime() - start < 1000){
    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;
}

このプログラムを実行したときの流れは下図のようになります。実行開始時よりtask1は実行とsleepを繰り返していますが、cyc1が呼び出されるとtask1は待機状態になります。cyc1の処理にかかる時間は1秒ですので、task1は1秒間待機することになります。

2015-04-16-1

cyc1で行われる処理の方を優先する場合は上記の実装で問題ありません。しかし、task1で行われる処理を優先したい場合は上記のプログラムでは困ることになります。

task1の処理を優先したい場合、下図の実行手順で動作するプログラムを作ることになります。ポイントは、周期処理「cyc1」から低優先度タスク「task2」を呼び出すようにするところです。この実装であれば、周期的に起動されるtask2の処理はtask1がsleepしている間に実行されるようになります。

2015-04-16-2

プログラムは以下のようになります。まず、低優先度タスク「task2」を生成するようにapp.cfgに記述します。このとき、task2が自動実行されないようにするため、引数にTA_NULLを指定しておきます。

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_NULL, 0, task2, TMIN_APP_TPRI + 2, STACK_SIZE, NULL });
  EV3_CRE_CYC(CYC1, { TA_STA, 0, cyc1, 2000, 0 });
}

ATT_MOD("app.o");

ヘッダファイルapp.hはいつもと同じです。

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

ソースファイルapp.cでは、周期処理cyc1から低優先度タスクtask2を起動するプログラムを記述します。関数cyc1の中のact_tsk(TASK2)がtask2を起動している部分になります。

#include "ev3api.h"
#include "app.h"
#include "kernel_cfg.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)
{
  ulong_t start = getTime();

  if(start >= 10000){
    return;
  }

  while(getTime() - start < 1000){
    count2 += 1;
    display();
  }
}

/* 周期ハンドラ */
void cyc1(intptr_t exinf)
{
  act_tsk(TASK2);
}

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

上記のプログラムを実行すると、周期処理cyc1が低優先度タスクtask2を起動し、task1がsleepしている間にtask2の処理が実行されます。周期実行機能を積極的に使っていく場合、上記のようなプログラムであれば実行順序を上手くスケジュールできそうです。

(816) 533-4334

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

2015年度のETロボコンの申し込み期間も終了し、そろそろ本格的に開発をはじめているチームもあるかと思いますが、私の方は慌ただしい日が続いているために落ち着いて開発できていません。間に合うのかな、これ。

さて、今回はEV3RTの周期実行の仕組みを使ってみます。Toppersの仕様にも周期実行機能やAlarm実行機能が含まれています。しかし、EV3RTのAPIやサンプルプログラムを読んでみたところ、EV3RTには独自の周期実行機能が実装されているようで、こちらの機能を使ったほうがよさそうです。

今回作るプログラムでは1個のタスク「task1」と1個の周期処理「cyc1」を作ります。task1はカウントと表示を繰り返す処理であり、途中にsleepなどの待機処理は含まれません。一方、cyc1は1秒おきに実行される周期処理で、こちらもカウントと表示処理を行います。処理の流れは下図のようになります。

2015-04-15-1

ここで、task1は待機処理を含まないので、task1とcyc1の優先度が重要になってきます。cyc1の処理が実行されるためには、cyc1の優先度がtask1の優先度よりも高くなくてはなりません。もし、cyc1の優先度がtask1の優先度よりも低い場合は、常にtask1が実行され、cyc1は実行されないことになります。このあたりの実装も実際に動かして試してみます。

まずはタスクと周期処理のプロセスを作成するようにapp.cfgに記述します。タスク「task1」の作成の部分は今までと同じです。一方、周期処理「cyc1」を作成を指示している部分はEV3_CRE_CYCと書いてある箇所になります。ここの引数ですが、CYC1は周期処理のID、TA_STAは周期処理を自動実行させるための指定、cyc1は周期処理を実装する関数名、1000が周期処理の実行間隔(単位はミリ秒)となります。ちなみに、最後の引数の0は起動までの時間だと思いますが、3個目の引数の0はよくわかりません 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 });
  EV3_CRE_CYC(CYC1, { TA_STA, 0, cyc1, 1000, 0 });
}

ATT_MOD("app.o");

ヘッダファイルapp.hは今までとほぼ同じです。ただし、周期処理を行う関数cyc1の宣言も行っています。

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

ソースファイルapp.cにタスク「task1」と周期処理「cyc1」の内容を記述していきます。タスクと周期処理以外に表示と時間計測の関数がありますが、これらの関数は前回までと同じです。task1は指定時間が経過するまでカウントと表示処理を繰り返しています。一方、cyc1では、この関数が呼ばれるたびにカウントと表示処理を実行します。

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

int count1 = 0;
int count2 = 0;

/* タスク */
void task1(intptr_t exinf)
{
  while(getTime() < 10000){
    count1 += 1;
    display();
  }
}

/* 周期処理 */
void cyc1(intptr_t exinf)
{
  if(getTime() >= 10000){
    return;
  }

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

これらのプログラムをkernelと一緒にコンパイルし、EV3本体で実行します。実行結果は下の写真の通りで、cyc1が約1秒おきに実行されているのことが確認できました。また、上のプログラムでcyc1が実行されるということは、周期処理の優先度はタスク処理の優先度よりも高いということになります。この優先度の関係を考えながらプログラムを設計しないといけないかもしれません。

2015-04-15-2

ちなみに、ソースコードの中にはToppersの周期実行機能であるCRE_CYCを実装したような形跡があります。ただ、ソースコードを読んだりはしていないので、このCRE_CYCの部分が何を意味するかは分かりません。EV3_CRE_CYCもCRE_CYCも引数の数や型が同じであることから想像すると、これらは同じ実装になっているのかもしれません。まぁ、ドキュメントもなければソースコードも読んでいないので、想像でしかありませんが。

追記(2015/04/16):
EV3RTの公式ページのFAQで周期処理の記述方法が説明されていました。

Notes:

  1. 実行される関数に渡される値だと思いますが、私の手元では確かめていません。