7.OILファイル TOPへ 9.タスク間の排他制御

8.マルチタスク


※この章を学習するには、前章の OILファイルを習得することが必要です。

ここでは、マルチタスクについて学習します。
まず、タスクとはOSから見た処理の実行単位のことです。これまでのプログラムではタスクを意識することがなかったと思います。 なぜなら、これまでは一つのタスク内の処理を順に実行するだけのシングルタスクのプログラムしか組んでいなかったからです。
しかしシングルタスクでは、複数の処理を同時に(並行して)実行させることはできません。 そのためには複数のタスクを切り替えながら処理するマルチタスクを用いる必要があります。
ここでは、マルチタスクの概念とマルチタスクを扱うプログラミングについて学習します。


1.複数タスクのプログラム

まずは、タスクが2つあるプログラムを組んでみましょう。
次のプログラムを作成してください。ここでは tasks.c , tasks.oilとします。

tasks.c
#include "kernel.h"
#include "kernel_id.h"
#include "ecrobot_interface.h"

#define COUNT 500		/* カウント数を500に定義 */

DeclareTask(Task1);				/* Task1を宣言 */
DeclareTask(Task2);				/* Task2を宣言 */

void ecrobot_device_initialize(){}

void ecrobot_device_terminate(){}

void user_1ms_isr_type2(void){}

TASK(Task1)
{
	int i;
	for(i=0 ; i<=COUNT ; i++){
		display_goto_xy(0,1);
   		display_string("TASK1 = ");		
		display_goto_xy(8,1);
		display_int(i, 5);
		display_update();
		systick_wait_ms(10);		
	}
	TerminateTask();					/* 処理終了 */
}


TASK(Task2)
{
	int j;
	for(j=0 ; j<=COUNT ; j++){
		display_goto_xy(0,2);
   		display_string("TASK2 = ");		
		display_goto_xy(8,2);
		display_int(j, 5);
		display_update();
		systick_wait_ms(20);		
	}

	TerminateTask();					/* 処理終了 */
}

tasks.oil
#include "implementation.oil"

CPU ATMEL_AT91SAM7S256
{
  OS LEJOS_OSEK
  {
    STATUS = EXTENDED;
    STARTUPHOOK = FALSE;
    SHUTDOWNHOOK = FALSE;
    PRETASKHOOK = FALSE;
    POSTTASKHOOK = FALSE;
    USEGETSERVICEID = FALSE;
    USEPARAMETERACCESS = FALSE;
    USERESSCHEDULER = FALSE;
  };


  APPMODE appmode1{}; 	/* アプリケーションモードを定義 */

  TASK Task1						/* Task1 を定義 */
  {
    AUTOSTART = TRUE { APPMODE = appmode1; };
    PRIORITY = 1;
    ACTIVATION = 1;
    SCHEDULE = FULL;
    STACKSIZE = 512;
  };

  TASK Task2						/* Task2 を定義 */
  {
    AUTOSTART = TRUE { APPMODE = appmode1; };
    PRIORITY = 2;
    ACTIVATION = 1;
    SCHEDULE = FULL;
    STACKSIZE = 512;
  };
};

このプログラムを動かすとLCDを見れば分かるように、まずTask2が500までインクリメントします。 次に、Task1が500までインクリメントして処理が終了します。
タスクの起動される順番は、タスクの優先度によって決まります。このプログラムでは、Task1の優先度は1、Task2の優先度は2になっています。
優先度は値が大きいほど高いので、Task2の方が優先度が高く、Task2が先に起動されたのです。
※2つのタスクの優先度が同じだった場合、先に起動されたタスクが処理されます。

しかし、これでは、同時に処理をしているとはいえません。優先度を同じにしても、先に宣言されていたタスクが先に処理されるので、同時には処理されません。
普通の処理系では、同じ優先度のタスクはラウンドロビンという方式により、ほぼ同時に処理されますが、nxtOSEKにはその仕組みがありません。
つぎは、どうしたらタスクが同時に処理されるかを考えて見ましょう。

※ラウンドロビン…タスクをを一定時間ずつ順番に実行する方式。持ち時間を使い果たしたタスクは一旦中断され、待ち行列の最後に回される。

2.同時に処理をする

同時に処理をするといっても、CPUは一つしかありませんから、一度に2つの計算をすることは出来ませんが、 それらのタスクを短い時間単位で切り替えて実行することによって、見かけ上、同時に処理が出来るようにします。
ここで、前節で学んだ、COUNTERやALARMを用います。 次のプログラムを作成してください。 task_cycle.c が Task1, Task2 を定義したソースファイル、task_bg.c がバックグランド実行される Task_bg を定義したソースファイル、task_cycle.oil がそれらのタスクの動作を定義した OIL ファイルです。なお、task_cycle.c ではタスクの宣言、初期化と終端フック関数の定義を行っています。
task_cycle.c
#include "kernel.h"
#include "kernel_id.h"
#include "ecrobot_interface.h"

#define COUNT 500		/* カウント数を500に定義 */


DeclareCounter(SysTimerCnt);
DeclareTask(Task1);				/* Task1を宣言 */
DeclareTask(Task2);				/* Task2を宣言 */
DeclareTask(Task_bg);				/* Task_bgを宣言 */

void ecrobot_device_initialize(){}

void ecrobot_device_terminate(){}

void user_1ms_isr_type2(void){
	SignalCounter(SysTimerCnt);    /* カウンタをIncrementする */
}

TASK(Task1)
{
	static int i=0;
	if(i<=COUNT){
		display_goto_xy(0,1);
   		display_string("TASK1 = ");		
		display_goto_xy(8,1);
		display_int(i, 5);
		display_update();
		i++;
	}else{
		display_goto_xy(0,4);
		display_string("TASK1 Terminated");
		display_update();
	}
	TerminateTask();					/* 処理終了 */
}


TASK(Task2)
{
	static int j=0;
	if(j<=COUNT){
		display_goto_xy(0,2);
   		display_string("TASK2 = ");
		display_goto_xy(8,2);
		display_int(j, 5);
		display_update();
		j++;
	}else{
		display_goto_xy(0,5);
		display_string("TASK2 Terminated");
		display_update();
	}
	TerminateTask();					/* 処理終了 */
}

task_bg.c

#include "kernel.h"
#include "kernel_id.h"
#include "ecrobot_interface.h"

#define TEMPO 10
#define VOLUME 50

static void RingTone(int freq, int time, int vol){	/* 音符を再生するユーザー関数を定義 */
	ecrobot_sound_tone(freq, time-5, vol);
	systick_wait_ms(time*10);
}

TASK(Task_bg)
{
	while(1){
/*===========かえるの歌=============*/
		RingTone(523, TEMPO*2, VOLUME);
		RingTone(587, TEMPO*2, VOLUME);
		RingTone(659, TEMPO*2, VOLUME);
		RingTone(698, TEMPO*2, VOLUME);
		RingTone(659, TEMPO*2, VOLUME);
		RingTone(587, TEMPO*2, VOLUME);
		RingTone(523, TEMPO*3, VOLUME);
		systick_wait_ms(TEMPO*10);

		RingTone(659, TEMPO*2, VOLUME);
		RingTone(698, TEMPO*2, VOLUME);
		RingTone(784, TEMPO*2, VOLUME);
		RingTone(880, TEMPO*2, VOLUME);
		RingTone(784, TEMPO*2, VOLUME);
		RingTone(698, TEMPO*2, VOLUME);
		RingTone(659, TEMPO*3, VOLUME);
		systick_wait_ms(TEMPO*10);

		RingTone(523, TEMPO*2, VOLUME);
		systick_wait_ms(TEMPO*2*10);
		RingTone(523, TEMPO*2, VOLUME);
		systick_wait_ms(TEMPO*2*10);
		RingTone(523, TEMPO*2, VOLUME);
		systick_wait_ms(TEMPO*2*10);
		RingTone(523, TEMPO*2, VOLUME);
		systick_wait_ms(TEMPO*2*10);

		RingTone(523, TEMPO, VOLUME);
		RingTone(523, TEMPO, VOLUME);
		RingTone(587, TEMPO, VOLUME);
		RingTone(587, TEMPO, VOLUME);
		RingTone(659, TEMPO, VOLUME);
		RingTone(659, TEMPO, VOLUME);
		RingTone(698, TEMPO, VOLUME);
		RingTone(698, TEMPO, VOLUME);
		RingTone(659, TEMPO, VOLUME);
		systick_wait_ms(TEMPO*10);
		RingTone(587, TEMPO, VOLUME);
		systick_wait_ms(TEMPO*10);
		RingTone(523, TEMPO*3, VOLUME);
		systick_wait_ms(TEMPO*10);
/*==================================*/
	}

	display_goto_xy(0,6);
   	display_string("TASKbgTerminated");		/* 終了メッセージ */
	display_update();

	TerminateTask();					/* 処理終了 */
}

task_cycle.oil
#include "implementation.oil"

CPU ATMEL_AT91SAM7S256
{
  OS LEJOS_OSEK
  {
    STATUS = EXTENDED;
    STARTUPHOOK = FALSE;
    SHUTDOWNHOOK = FALSE;
    PRETASKHOOK = FALSE;
    POSTTASKHOOK = FALSE;
    USEGETSERVICEID = FALSE;
    USEPARAMETERACCESS = FALSE;
    USERESSCHEDULER = FALSE;
  };


  APPMODE appmode1{}; 	// アプリケーションモードを定義 

  TASK Task1						// Task1 を定義 
  {
    AUTOSTART = FALSE;
    PRIORITY = 2;
    ACTIVATION = 1;
    SCHEDULE = FULL;
    STACKSIZE = 512;
  };

  TASK Task2						// Task2 を定義 
  {
    AUTOSTART = FALSE;
    PRIORITY = 2;
    ACTIVATION = 1;
    SCHEDULE = FULL;
    STACKSIZE = 512;
  };

  TASK Task_bg						// Task_bg を定義 
  {
    AUTOSTART = TRUE { APPMODE = appmode1; };
    PRIORITY = 1;
    ACTIVATION = 1;
    SCHEDULE = FULL;
    STACKSIZE = 512;
  };

  COUNTER SysTimerCnt			// SysTimerCntを定義 
  {
    MINCYCLE = 1;
    MAXALLOWEDVALUE = 10000;
    TICKSPERBASE = 1; // One tick is equal to 1msec  
  };

  ALARM cyclic_alarm1		// 周期アラーム1を定義 
  {
    COUNTER = SysTimerCnt;
    ACTION = ACTIVATETASK
    {
      TASK = Task1;
    };
    AUTOSTART = TRUE
    {
      ALARMTIME = 1;
      CYCLETIME = 10; // Task1は10msecごとに起動 
      APPMODE = appmode1;
    };
  };

  ALARM cyclic_alarm2		// 周期アラーム2を定義 
  {
    COUNTER = SysTimerCnt;
    ACTION = ACTIVATETASK
    {
      TASK = Task2;
    };
    AUTOSTART = TRUE
    {
      ALARMTIME = 1;
      CYCLETIME = 20; // Task2は20msecごとに起動 
      APPMODE = appmode1;
    };
  };
};

プログラムを実行してみると「かえるの歌」が流れながら、画面では2つのタスクが500までにインクリメントします。(ただしタスク2はTask1が2カウントする毎に1カウントします。「かえるの歌」のプログラムについては、基礎編の7. 音を鳴らすを参照。)
タスクの動作が終わると、画面に終了メッセージが表示されます。
Task_bg はプログラム起動に自動実行される(従来から使っている)タスク、Task1とTask2はそれぞれのアラームオブジェクトから起動される周期タスクです。
Task1, Task2 には タスク切り替えの様子を下図に表しました。



プログラムの動作の過程を上図をもとに説明します。

1. Task_bgの処理が行われている。
2. 10msec経過すると、Alarm1 が expire になり、Task_bg の処理が中断され、Task1が処理される。
3. Task1の処理が終わるとTask1は、suspended状態になり、Task_bg の処理が再開される。
4. さらに10msec経過すると、Alarm1とAlarm2 が expire になり、Task_bg の処理が中断され、Task1, Task2 が処理される
   Alarm1によりTask1が実行されようとするが、優先度は同じだが、先に宣言されたTask1が実行される。
5. Task1, Task2 の処理が終わるとTask_bg の処理が再開される。
   Task1, Task2 はそれぞれの処理が終わった時点で、suspended 状態になる。
6. 2~5を繰り返す。

ここでは、周期タスク Task1, Task2 の処理は、CYCLETIMEより早く終わることを前提としています。


3.リアルタイム処理

今回説明した Alarm オブジェクトによって実行される周期タスク Task1, Task2 は、厳密のその周期で実行が可能となります。そのため、その処理がどのタイミングで終了するかも予測できるため、厳密な時間制約を守ることができます。このような時間制約を課す処理をリアルタイム処理と言い、そのような処理を必要とする制御をリアルタイム制御と言います。また、リアルタイム処理を定義したタスクをリアルタイムタスクと言います。
これに対し、Task1, Task2 の実行の合間をぬって実行される Task_bg はリアルタイムタスクではありません。

一方、これまで行ってきたモータ制御を一定の周期で繰り返すプログラムでは、while のループ内に systic_wait_ms()を入れて、指定時間 wait させる方法で行ってきました。 この方法では、ループ内の他の処理の実行時間に wait で指定した時間が加わって1ループを回ることになります。また、マルチタスクで動作させた場合には、他のタスクの動作時間をこれに加わることになり、厳密な周期実行を行うことはできなくなります。


4.タスク間の変数の共有

グルーバル変数として宣言した値は、同じファイル内であれば別タスクであっても値が共有される。 また、別ファイルに定義されたタスクであっても、「6. 分割プログラミング」の項目で記したように、 グローバル変数として宣言(他方はグローバル変数で extern 宣言)されていれば、共通の変数とし てその値は共有される。
ただし、別タスクによって値が変更される可能性のある変数は、volatile 修飾子を付けておいた 方が安全である。(volatile 修飾子がないと処理系によっては値が更新されないことがある)
例: volatile int uss_data;

5.Makefile

上記のソースファイル、OILファイルに対する Makefile は以下のようになる。
Makefile
# Target specific macros
TARGET = tasks

# User application source(ここにソースファイルの名前を指定する)
TARGET_SOURCES = task_bg.c task_cycle.c

# 自分のフォルダから見たnxtOSEKフォルダの場所またはそのフルパスを指定する。ここでは次のフルパスを指定する。
NXTOSEK_ROOT = /nxtOSEK

# OSEK OIL file(ここにoilファイルの名前を指定する)
TOPPERS_OSEK_OIL_SOURCE = task_cycle.oil

# below part should not be modified
O_PATH ?= build
include $(NXTOSEK_ROOT)/ecrobot/ecrobot.mak

6.課題
  1. 「2.同時に処理する」のサンプルプログラムを実行し、動作を確認せよ。
    • 複数のソースファイル(この場合は task_cycle.c と task_bg.c)から実行ファイルを作成する方法は、6. 分割プログラミングを参照すること。
    • Makefile の OILファイルあるいは OILファイルの中身を変更した場合は、make clean してから make all すること。

  2. バックグランドでかえるの歌を演奏しながら、10msec 周期で直進制御をし、100msec 周期で超音波計測を行うプログラムを作成せよ。
    この3つの処理を別タスクとして実装し、歌の演奏はバックグランドで実行し、正方形走行と超音波計測は ALARMオブジェクトから起動させるようにせよ。
    直進制御
    左右のエンコーダ値を比較し、その値の差に応じて左右のモータの速度差を与える(回転量が多 い方を減速し、少ない方を加速する)ことで、直進性を高める。

    Task宣言・フック関数定義とTask定義のファイル分割
    上記の task_cycle.c では、そのファイル内で Task宣言・フック関数定義とTask1, Task2 定義を行っていたが、 Task宣言(Declare*)およびフック関数(ecrobot_device_*, user_1ms_irs_type2)の定義と、Task定義を完全に分離してファイルを作成した方がわかりやすい。 この課題であれば、Task宣言・フック関数定義を行うファイルを multi_task.c とし、それぞれのTaskを宣言したファイルを task_bg.c, straight.c, sonar.c として分割する(ファイル名の任意)。なおこのとき、Task宣言・フック関数定義は multi_task.c のみで定義すること(重複定義はNG)。


7.OILファイル TOPへ 9.タスク間の排他制御