こんにちは、ナナです。
組み込み開発でシステム開発を行う上で割り込みに関する技術は必ず必要となります。
割り込みというのは特殊な処理であり、リアルタイムOSを使う上でも割り込みを扱うのは繊細な技術が必要となります。
割り込みに関する基礎知識が整っていな方はマイコン入門編のこちらの記事を参照ください。
本記事では次の疑問点を解消する内容となっています。
では、割り込みを使った周期ハンドラの仕組みを学んでいきましょう。
割り込みはCPUが持つもう一人の労働者
タスクの章にてリアルタイムOSが搭載されていない環境においても、main関数を実行する労働者がいると解説しました。実はCPUにはもう一人、影の労働者が存在しています。
それは割り込みです。
割り込みとは黒子という労働者
割り込みという労働者はタスクとは違う性質を持つ労働者です。
システムの表舞台に立つのがタスクとするならば、割り込みは舞台を裏からこっそりと支える黒子のような存在です。
リアルタイムOSを用いずとも、CPUには本来2人の労働者が存在しているのです。
マイコン入門編で割り込みを経験された方は、割り込みというイベントが発生することで、割り込みハンドラと呼ばれる関数が呼ばれることを学びました。
この割り込みハンドラのプログラムを動かしている労働者こそがこの黒子なのです。
割り込みにはタスクとは異なる労働者としての性質があるため、その特徴を押さえておかないとプログラムが正しく動きません。
4つの特徴を順に説明しましょう。
特徴1:割り込み処理の優先順位とは
割り込みという労働者はどんなタスクよりも優先順位が高いです。これは大きな特徴です。
そのため、タスクが処理している最中に割り込みイベントが発生した場合は、即座に割り込みハンドラにプログラムが移動します。
特徴2:割り込みは「非タスク」である
割り込みという労働者は「非タスク」と呼ばれます。つまり、タスクという労働者とは違うということです。
そのため、割り込みにはタスク状態というものが存在しません。「実行状態」「実行可能状態」「待ち状態」といった状態が存在しないのです。
タスクは働いた後は「時間経過待ち」や「起床待ち」など休みをとりますが、割り込みには待ち状態といった状態がありません。呼べばすぐに駆け付け処理をしてくれる献身者なのです。
特徴3:割り込みから呼べるサービスコールと呼べないサービスコール
タスク状態を持たないという特徴から、割り込みから呼び出せるサービスコールには制限があります。
割り込みではdly_tskといった自タスクを待ち状態に遷移させるようなサービスコールを呼ぶことはできません。
なぜなら、待ち状態なんてものには遷移できないからです。
よって、皆さんはプログラムを作る際に「この関数は割り込みで動くのか?そうでないのか?」ということを常に意識しておかなければなりません。
割り込みから呼ぶことが可能なサービスコールは「i」から始まるサービスコールです。
ITRONカーネル内では「i」がついたサービスコールは割り込みから呼べるように特別なプログラムが組まれているのです。
特徴4:割り込みでは高負荷の処理はしてはいけない
黒子である割り込みとはシステムという舞台の主役ではありません。
長々と割り込みハンドラ内で処理をし続けてはなりません。割り込み処理というのは必要最小限に留めるような設計が求められます。
黒子が舞台の準備をしている間は主役となるタスクは何もできずに待っているのですから。
タスクコンテキストと割り込みコンテキスト
割り込みが登場したため、「コンテキスト」と呼ばれるものを理解しておきましょう。
実行環境を「コンテキスト」と呼ぶ、としてありますがいったい何を言っているのかよくわかりませんね。実は「コンテキスト」という概念はかなり掴みづらいものです。
まず、コンテキストの種類を明確化しておきましょう。コンテキストには2種類しかありません。
- タスクコンテキスト
- 非タスクコンテキスト(割り込みコンテキストとも呼ぶ)
それでは、それぞれの説明をしてみましょう。
タスクコンテキストとは
プログラムというのはCPUによって命令を読み込み、演算し、結果を出力する過程においてCPU内部に様々な情報を管理します。
タスクコンテキストとはタスクという労働者がプログラムを動かしているCPU情報のことです。
マルチタスクの場合、各タスクにおいてこのタスクコンテキスト情報が個別に管理されます。
実はマルチタスクにおいてタスクの実行状態を切り替えるとは、このタスクコンテキスト情報を切り替えることにより実現しています。
非タスクコンテキスト(割り込みコンテキスト)とは
非タスクコンテキストとは割り込みという労働者が動かしているCPU情報のことです。
タスク処理の実行中に割り込みイベントが発生すると、タスクコンテキスト情報から非タスクコンテキスト情報にCPU情報が差し変わり割り込みハンドラが動き出すことになります。
コンテキストを知る意味
コンテキストという用語はITRON仕様の中でも頻繁に出現します。
この用語はタスクと割り込みを意識するときに出てくる用語です。「非タスクコンテキスト」という用語が登場したときは「割り込み」と読み替えてもよいでしょう。このことを頭に入れておくと仕様書なども読み解くことができると思います。
開発者同士の会話の中でも出てくることもあるので覚えておくとよいでしょう。
「この関数はタスクAのコンテキストで動いています」とか「この関数は割り込みコンテキストで動いているため、サービスコールは割り込み用を呼んでいます」とか表現したりします。
ITRONで定義されるタイムイベントハンドラ
ITRONにはタイムイベントハンドラと呼ばれる機能があります。ITRON仕様書の「4.7 時間管理機能」に所属している機能になります。
- 周期ハンドラ
- アラームハンドラ
- オーバーランハンドラ
これらの機能は非タスクコンテキスト(割り込みコンテキスト)にて動作する機能です。
そのため、扱う際にはITRON特有の割り込みに関する注意点にケアする必要があります。
周期ハンドラを使ってみよう
周期ハンドラとはITRONでは次のように定義されています。
システムを構築する際に一定時間毎に何か処理をしたいというニーズは非常に多いです。そんな時に利用できるのがこの周期ハンドラです。
周期ハンドラオブジェクトの作成(CRE_CYC)
タスクと同様に周期ハンドラは独立したオブジェクトとして存在します。そのため、皆さんが周期ハンドラを使いと思ったら、オブジェクトの生成を行う必要があります。
CRE_CYCの仕様
周期オブジェクトは次の静的APIにて作り出すことができます。
CRE_CYC(ID cycid, { ATR cycatr, VP_INT exinf, FP cychdr, RELTIM cyctim, RELTIM cycphs });
引数パラメータを解説しておきます。
ID | cycid | 生成する周期ハンドラのID。整数として一意のIDを割り付ける。 |
ATR | cycatr | 周期ハンドラ属性。高級言語を示すTA_HLNGは必ず指定。 OS起動時に自動で周期を開始させたいならTA_STAをOR指定する。 |
VP_INT | exinf | 周期ハンドラの引数に渡したい情報があれば指定。なければ0でよい。 |
FP | cychdr | 周期ハンドラが実行する関数ポインタ。関数名を書く。 |
RELTIM | cyctim | 周期ハンドラの起動周期。ミリ秒で指定。 |
RELTIM | cycphs | 周期ハンドラの起動位相。ミリ秒で指定。起動タイミングをずらす。 |
CRE_CYCによる周期ハンドラの生成定義
では、皆さんに周期ハンドラの定義をsystem.cfgに追加していただきましょう。前回のタスクはそのままで、周期ハンドラの定義を1つ追加しましょう。
追加する周期ハンドラ生成情報
- IDは「CYCID_CYC1」を指定する。
- 属性は「TA_STA」を指定する。
- 関数ポインタは「CYC1」を指定する。
- 起動周期は「5000」を指定する。
- 起動位相は「0」を指定する。
system.cfg(一部抜粋)
//------------------------------------------------
// タスク定義
//------------------------------------------------
CRE_TSK(TSKID_MAIN, {TA_HLNG|TA_ACT, 0, MAIN, 1, 128, NULL});
CRE_TSK(TSKID_TASK1, {TA_HLNG|TA_ACT, 0, TASK1, 2, 128, NULL});
CRE_TSK(TSKID_TASK2, {TA_HLNG|TA_ACT, 0, TASK2, 2, 128, NULL});
//------------------------------------------------
// 周期ハンドラ定義
//------------------------------------------------
system.cfg(一部抜粋)
//------------------------------------------------
// 周期ハンドラ定義
//------------------------------------------------
CRE_CYC(CYCID_CYC1, {TA_HLNG|TA_STA, 0, CYC1, 5000, 0});
周期ハンドラ関数の定義
周期ハンドラにはタスクと同様に周期的に起動する関数を指定する必要があります。CRE_CYCで指定したCYC1関数をmain.cに追加しましょう。
この関数では、TASK1に対して周期的に起床要求を行う処理を記述してください。
main.c(一部抜粋)
//------------------------------------------------
// 概 要:練習用周期ハンドラ関数
//------------------------------------------------
void CYC1(VP_INT exinf)
{
}
main.c(一部抜粋)
//------------------------------------------------
// 概 要:練習用周期ハンドラ関数
//------------------------------------------------
void CYC1(VP_INT exinf)
{
// 5秒毎に周期起動する
// TASK1を起床要求
iwup_tsk(TSKID_TASK1);
}
起床要求は割り込み用のiwup_tskを利用することに注意。
周期ハンドラ関数はmain.hにプロトタイプ宣言を追加しておきましょう。
main.h(一部抜粋)
//------------------------------------------------
// プロトタイプ宣言(Prototype declaration)
//------------------------------------------------
void MAIN(VP_INT exinf);
void Main_init(VP_INT exinf);
void TASK1(VP_INT exinf);
void TASK2(VP_INT exinf);
void CYC1(VP_INT exinf);
タスク関数の定義
起床されるTASK1の関数の中身を作り変えましょう。TASK1に与えるミッションは5秒周期でオレンジLEDを点灯/消灯を繰り返しながら眠ることです。
眠ってしまったTASK1は周期ハンドラから5秒周期で起床されます。
MAIN関数とTASK2関数は特に役割がないのでdly_tsk(1000);で眠っていただきましょう。皆さんはTASK1の中のミッションを作り出してください。
main.c(一部抜粋)
//------------------------------------------------
// 概 要:MAINタスク
//------------------------------------------------
void MAIN(VP_INT exinf)
{
while(1)
{
dly_tsk(1000);
}
return;
}
//------------------------------------------------
// 概 要:練習用TASK1関数
//------------------------------------------------
void TASK1(VP_INT exinf)
{
long i;
while(1)
{
}
return;
}
//------------------------------------------------
// 概 要:練習用TASK2関数
//------------------------------------------------
void TASK2(VP_INT exinf)
{
while(1)
{
dly_tsk(1000);
}
return;
}
main.c(一部抜粋)
//------------------------------------------------
// 概 要:練習用TASK1関数
//------------------------------------------------
void TASK1(VP_INT exinf)
{
long i;
int flg = 0;
while(1)
{
// オレンジLEDを切り替え
if (flg == 0)
{
Led_setLight(D_LED_KIND_ORANGE, D_LED_LIGHT_ON);
flg = 1;
}
else
{
Led_setLight(D_LED_KIND_ORANGE, D_LED_LIGHT_OFF);
flg = 0;
}
// 仕事実施。約2秒程度
for (i=0 ; i < 5000000 ; i++);
// 眠る
slp_tsk();
}
return;
}
プログラムができたら動かしてみましょう。5秒周期でLEDが点滅すれば正常に周期ハンドラからタスクの起床ができています。
dly_tskによる周期違いと周期ハンドラによる周期処理の違い
今回は周期ハンドラにてLEDの点滅を繰り返しましたが、dly_tskを実施しながら周期的にLEDを点滅させることもできますね。
この2つの方法には違いがあるのでしょうか?
次のようにslp_tskをdly_tsk(5000)にプログラムを書き換えて動かしてみてください。
//------------------------------------------------
// 概 要:練習用TASK1関数
//------------------------------------------------
void TASK1(VP_INT exinf)
{
long i;
static flg = 0;
while(1)
{
// オレンジLEDを切り替え
if (flg == 0)
{
Led_setLight(D_LED_KIND_ORANGE, D_LED_LIGHT_ON);
flg = 1;
}
else
{
Led_setLight(D_LED_KIND_ORANGE, D_LED_LIGHT_OFF);
flg = 0;
}
// 仕事実施。約2秒程度
for (i=0 ; i < 5000000 ; i++);
// 5秒眠る
dly_tsk(5000);
}
return;
}
動かしてみるとLEDの点滅周期が7~8秒毎になったことに気づくでしょう。理由は皆さんわかりますか?
dly_tskによる周期処理の考察
dly_tskを使った周期処理ではdly_tsk以外の処理の時間は+αとして加算されます。そのため、周期の厳密な調整が難しくなります。
一旦は正確に調整したとしても、仕様変更などにより処理が新たに追加・削除される度にdly_tskの時間を調整しなければなりません。
周期ハンドラを使った起床要求による周期処理の考察
それに対し、周期ハンドラを経由した起床要求の処理の場合は、周期ハンドラ側にて正確5秒を計測しています。TASK1側の処理内容に依存しません。
そのため、TASK1の処理が5秒以内に眠りに着くのであれば、その都度5秒の間隔で起床がされることになります。
どちらの周期処理が優れているのか?
正確な5秒を実現できる周期ハンドラの方がよいように思います。
しかし、周期ハンドラはオブジェクトを生成するコストを払わなくてはなりません。それに対し、dly_tskによる待ち処理は他の処理の負荷が十分に小さい場合は誤差は小さくなります。
どちらかが優れているというわけではなく、このような特性を知った上でどの方法による実装がよいのかを設計できる力こそが大事なものです。