はじめに
前編ではタスク管理APIと同期API(イベント・リソース)を取り上げました。後編ではTOPPERS/ATK1の時間管理API(アラーム)と、システムの構成を静的に定義するOILファイルの読み方を取り上げます。このシリーズの締めくくりです。
時間管理API
基礎編でアラームとスケジュールテーブルの考え方を整理しました。ここではTOPPERS/ATK1のアラームAPIを具体的に見ていきます。アラームは組み込みシステムの周期処理を実装するうえで非常に多用するAPIです。使い方を正確に理解しておくことが重要です。
アラームの動作
アラームはカウンタと紐付いており、カウンタが指定値に達したときにアクションを実行します。アクションとして指定できるのは以下の3種類です。
- タスクの起動(
ActivateTask()相当) - イベントの設定(
SetEvent()相当) - アラームコールバック関数の呼び出し
どのアクションを使うかはOILファイルで静的に定義します。
SetRelAlarm ― 相対アラームを設定する
StatusType SetRelAlarm(AlarmType AlarmID, TickType increment, TickType cycle);
現在のカウンタ値からの相対的な時間後にアラームを発火させます。
-
AlarmID:設定するアラームのID -
increment:最初の発火までのティック数(0より大きい値) -
cycle:繰り返し周期のティック数(0の場合は1回だけ発火)
/* 10ティック後に初回発火し、以降10ティックごとに繰り返す */
SetRelAlarm(alarm_sensor, 10, 10);
/* 100ティック後に1回だけ発火する */
SetRelAlarm(alarm_timeout, 100, 0);
周期処理の起動に使う場合は、increment と cycle を同じ値にするのが一般的です。
SetAbsAlarm ― 絶対アラームを設定する
StatusType SetAbsAlarm(AlarmType AlarmID, TickType start, TickType cycle);
カウンタの絶対値が start に達したときに発火します。
-
start:発火するカウンタの絶対値 -
cycle:繰り返し周期のティック数(0の場合は1回だけ)
/* カウンタが100になったときに発火し、以降50ティックごとに繰り返す */
SetAbsAlarm(alarm_control, 100, 50);
複数のアラームを同期させたい場合に便利です。たとえばシステム起動時のカウンタ値を基準に、複数の処理を決まった位相関係で起動できます。
CancelAlarm ― アラームをキャンセルする
StatusType CancelAlarm(AlarmType AlarmID);
設定済みのアラームをキャンセルします。1回だけ発火するアラームをタイムアウト処理に使い、処理が完了したらキャンセルするパターンでよく使います。
/* 処理開始時にタイムアウトアラームをセット */
SetRelAlarm(alarm_timeout, TIMEOUT_TICKS, 0);
do_process();
/* 処理完了時にタイムアウトアラームをキャンセル */
CancelAlarm(alarm_timeout);
GetAlarm ― アラームの残りティックを取得する
StatusType GetAlarm(AlarmType AlarmID, TickRefType Tick);
次の発火までの残りティック数を取得します。デバッグや動的なタイミング調整に使います。
TickType remaining;
GetAlarm(alarm_sensor, &remaining);
GetAlarmBase ― アラームのカウンタ情報を取得する
StatusType GetAlarmBase(AlarmType AlarmID, AlarmBaseRefType Info);
アラームが使用するカウンタの特性(最大値・最小周期など)を取得します。
よくある使い方パターン
周期タスクの起動
/* システム起動時に周期アラームを設定 */
SetRelAlarm(alarm_sensor_task, SENSOR_PERIOD, SENSOR_PERIOD);
OILファイルでアクションを ACTIVATETASK に設定しておくと、アラームが発火するたびにタスクが起動されます。
イベントによる周期起動
/* アラームのアクションをSETEVENTに設定した場合 */
TASK(task_control) {
while (1) {
WaitEvent(event_period);
ClearEvent(event_period);
do_control();
}
}
拡張タスクをループさせてイベント待ちにし、アラームでイベントを設定するパターンです。ActivateTask で毎回タスクを起動する場合と比べてオーバーヘッドが小さく、タスクの状態を保持したまま周期処理できます。
よくある落とし穴
すでに設定済みのアラームに再度SetRelAlarm/SetAbsAlarmを呼ぶ
すでに動作中のアラームに対して SetRelAlarm() や SetAbsAlarm() を呼ぶとエラー(E_OS_STATE)が返ります。再設定したい場合は一度 CancelAlarm() してから設定し直してください。
incrementに0を指定する
SetRelAlarm() の increment に0を指定するとエラーになります。必ず1以上の値を指定してください。
カウンタの最大値をまたぐ計算
カウンタには最大値(MAXALLOWEDVALUE)があり、最大値を超えると0に戻ります(ラップアラウンド)。絶対アラームを使う場合はラップアラウンドを考慮した計算が必要です。
OIL設定ファイルの読み方
OSEK/ATK1の大きな特徴の一つが、システムの構成を「OIL(OSEK Implementation Language)ファイル」で静的に定義することです。タスク・イベント・アラーム・リソースといった、これまで基礎編・設計編・API編で扱ってきたすべての要素がOILファイルで定義されます。
OILファイルを読めるようになると、既存システムの構成を把握したり、新しい要素を追加したりする際に迷わなくなります。
OILファイルの基本構造
OILファイルは OIL_VERSION の宣言と CPU ブロックで構成されます。
OIL_VERSION = "2.5";
CPU sample_app {
OS sample_os {
/* OSの設定 */
};
TASK task_control {
/* タスクの設定 */
};
EVENT event_sensor_ready {
/* イベントの設定 */
};
ALARM alarm_sensor {
/* アラームの設定 */
};
RESOURCE res_shared_data {
/* リソースの設定 */
};
};
OSの定義
OS sample_os {
STATUS = EXTENDED; /* エラーステータス:STANDARD or EXTENDED */
STARTUPHOOK = FALSE; /* スタートアップフックの使用有無 */
ERRORHOOK = FALSE; /* エラーフックの使用有無 */
SHUTDOWNHOOK = FALSE; /* シャットダウンフックの使用有無 */
PRETASKHOOK = FALSE; /* プレタスクフックの使用有無 */
POSTTASKHOOK = FALSE; /* ポストタスクフックの使用有無 */
};
STATUS = EXTENDED にするとより詳細なエラーコードが返るようになります。開発中は EXTENDED、リリース時は STANDARD に切り替えることでオーバーヘッドを削減できます。
タスクの定義
TASK task_control {
PRIORITY = 10; /* 優先度(数値が大きいほど高優先度) */
ACTIVATION = 1; /* 最大同時起動要求数 */
SCHEDULE = FULL; /* スケジューリング:FULL or NON */
AUTOSTART = FALSE; /* 自動起動の有無 */
STACKSIZE = 512; /* スタックサイズ(バイト)(実装依存の拡張属性) */
EVENT = event_sensor_ready; /* 使用するイベント(拡張タスクのみ) */
RESOURCE = res_shared_data; /* 使用するリソース */
};
SCHEDULE = FULL はフルプリエンプティブ、SCHEDULE = NON はノンプリエンプティブを意味します。通常は FULL を使います。
EVENT を定義したタスクが拡張タスク、定義しないタスクが基本タスクになります。
イベントの定義
EVENT event_sensor_ready {
MASK = AUTO; /* イベントマスク値:AUTOで自動割り当て */
};
MASK = AUTO にするとツールが自動的にビットマスク値を割り当てます。特別な理由がない限り AUTO を使えば問題ありません。
アラームの定義
ALARM alarm_sensor {
COUNTER = system_counter; /* 使用するカウンタ */
ACTION = ACTIVATETASK { /* アクション */
TASK = task_sensor; /* 起動するタスク */
};
AUTOSTART = TRUE {
ALARMTIME = 10; /* 最初の発火タイミング */
CYCLETIME = 10; /* 繰り返し周期 */
APPMODE = OSDEFAULTAPPMODE;
};
};
ACTION にはタスク起動(ACTIVATETASK)・イベント設定(SETEVENT)・コールバック(ALARMCALLBACK)が指定できます。
/* イベント設定の場合 */
ACTION = SETEVENT {
TASK = task_control;
EVENT = event_sensor_ready;
};
リソースの定義
RESOURCE res_shared_data {
RESOURCEPROPERTY = STANDARD; /* STANDARD / LINKED / INTERNAL */
};
通常は STANDARD を使います。INTERNAL はタスク内部でのみ使う場合に指定します。
カウンタの定義
COUNTER system_counter {
MINCYCLE = 1; /* 最小周期 */
MAXALLOWEDVALUE = 65535; /* カウンタの最大値 */
TICKSPERBASE = 1; /* 1ティックの基本単位 */
};
OILファイルとAPIの対応まとめ
これまで扱ってきたAPIとOILファイルの定義の対応を整理します。
| OILの定義 | 対応するAPI |
|---|---|
TASK |
ActivateTask() TerminateTask() ChainTask()
|
EVENT |
SetEvent() WaitEvent() ClearEvent() GetEvent()
|
ALARM |
SetRelAlarm() SetAbsAlarm() CancelAlarm() GetAlarm()
|
RESOURCE |
GetResource() ReleaseResource()
|
COUNTER |
GetAlarmBase()(アラームに関連付けられたカウンタ情報を取得。カウンタを直接操作するAPIはない) |
OILファイルで定義した要素がAPIで操作できる、というのがOSEK/ATK1の基本的な考え方です。なお TASK 一つに対して複数のAPIが対応するように、必ずしも1対1ではありません。OILファイルはシステムの設計図であり、APIはその設計図に従って動作を制御する手段です。
OILファイルからコードが生成される仕組み
TOPPERS/ATK1ではOILファイルをもとにジェネレータ(sg)がシステム構成ファイル(kernel_cfg.c など)を自動生成します。
OILファイル → ジェネレータ(sg) → kernel_cfg.c / kernel_cfg.h
生成されたファイルにはタスクIDの定義やタスク初期化テーブルなどが含まれており、これをカーネルが参照して動作します。組み込みCの現場テクニック編で触れた関数ポインタによるタスク登録も、この自動生成ファイルの中で行われています。
まとめ
-
SetRelAlarm()は現在からの相対時間でアラームを設定する -
SetAbsAlarm()はカウンタの絶対値でアラームを設定する -
CancelAlarm()でアラームをキャンセルする - 周期処理にはタスク起動よりイベント通知パターンの方がオーバーヘッドが小さい
- OILファイルはシステムの静的な構成を定義するファイルで、タスク・イベント・アラーム・リソース・カウンタをすべて定義する
- OILファイルからジェネレータがシステム構成ファイルを自動生成する
以上でRTOSシリーズ全6回が完結です。基礎編でRTOSの仕組みを理解し、設計編でタスク設計の勘所を押さえ、API編で具体的なAPIの使い方とOILファイルでの定義のつながりを理解できたと思います。組み込みRTOSの開発に少しでも役立てば幸いです。