(2023/7/21)
[連載8] SOLIDでタスクと割り込み
今回は、タスクと割り込みについて見ていきます。
時間がかかる処理をする必要があるシステムの場合、その処理が終わるまで他の処理は何もできないのは困ります。なので「タスク」が必要になります。
複数タスクを起動し、とあるタスクで時間がかかる処理を行っている場合でも、時間が来たらいったん中断し他のタスクが動ける仕組みが、「マルチタスク」です。
一方、タスクではないものの、ハードウェア的なイベントが発生し、それに関係する処理を素早くしないといけない場合は、割り込みハンドラを登録し関係する処理を起動します。
このように、マルチタスク処理も割り込み処理も、組み込みでリアルタイムプログラミングを行う最も重要なものではないかと思います。
今回は、組み込みリアルタイムOSの機能を使って、これら、二つを使うようなプログラムを書き、説明しつつ動作させてみます。
今回はRaspberry Pi4を使用します。理由は後述します。
以下、それぞれについて見ていきます。
前回少しご紹介しました、タスク作成、起動、実行についてです。
前回は動作についての紹介のみだったので、今回はTOPPERSのシステムコールであるacre_tsk()関数等を使用してタスクを動的に作成、起動、実行してみます。
※FMP3カーネルでの資源の動的生成は、SOLID-OSでの拡張機能です。
タスク1とタスク2本体は以下のようにします。
タスク1は5秒間隔で短めの”------“を表示し、タスク2は1秒間隔で長めの”-----------------“を表示します。
#printf文があるため、厳密に5秒、1秒ではありません。
タスク1:
void test_task1(VP_INT exinf)
{
SOLID_LOG_printf("[TASK1 ] Start TASK1\n");
while (1)
{
dly_tsk(5000'000); //5sec
SOLID_LOG_printf("----------------\n");
}
return;
}
タスク2:
void test_task2(VP_INT exinf)
{
SOLID_LOG_printf("[ TASK2] Start TASK2\n");
while (1)
{
dly_tsk(1000'000); //1sec
SOLID_LOG_printf("------------------------------\n");
}
return;
}
まず、タスク作成のための情報を設定します。
// タスク動的生成のための情報設定
tsk1.tskatr = TA_NULL;
tsk1.task = test_task1;
tsk1.itskpri = MID_PRIORITY;
tsk1.stksz = STACK_SIZE;
tsk1.stk = NULL; // スタックはカーネルが自動的に割り当てる(SOLIDのスタックフェンスが有効)
tsk1.iprcid = 1;
tsk1.affinity = UINT_MAX;
// タスク動的生成のための情報設定
tsk2.tskatr = TA_NULL;
tsk2.task = test_task2;
tsk2.itskpri = MID_PRIORITY;
tsk2.stksz = STACK_SIZE;
tsk2.stk = NULL; // スタックはカーネルが自動的に割り当てる(SOLIDのスタックフェンスが有効)
tsk2.iprcid = 2;
tsk2.affinity = UINT_MAX;
次に、acre_tsk()関数で生成し、act_tsk()関数で起動します。
ercd = acre_tsk(&tsk1);
if (ercd > 0) ercd = act_tsk(ercd);
ercd = acre_tsk(&tsk2);
if (ercd > 0) ercd = act_tsk(ercd);
実行してみます。
タスク2で出力される、約1秒間隔の長い線が5回、
タスク1で出力される、約5秒間隔の短い線が1回、
の周期で繰り返しています。
#タイマでは1秒や5秒を計測していますが、そこにSOLID_LOG_printf()関数によるUART制御時間が加わるため、厳密には+αの時間が存在しているため、”約”と書いています。
タスク間通信として、イベントフラグとデータキューを使ってみます。
main()関数内で、イベントフラグを生成します。
定義すべきイベントフラグの属性については以下に記載されています。
https://solid.kmckk.com/SOLID/doc/latest/os/kernel/kernel_config.html#id7
今回はこのように作成します。
cflg.flgatr = TA_WMUL | TA_TPRI;
cflg.iflgptn = 0;
ercd = acre_flg(&cflg);
flag = ercd;
イベントフラグをあるタスクが設定し、その値を他のタスクが待つことにより、タスク間で通知のやり取りができます。
値を取得したタスクは、その値がもう必要ないのであれば値をクリアする場合が多いです。
・イベントフラグ設定
event_from_task2として定義されたフラグパターン”0x02”を設定します。
FLGPTN event_from_task2 = 0x02;
ercd = set_flg(flag, event_from_task2);
・指定イベントフラグパターンの取得
event_from_task2で定義されたフラグパターンが来るまで待ちます。
ercd = wai_flg(flag, event_from_task2, TWF_ORW, &ptn);
・イベントフラグのクリア
event_from_task2で定義されたフラグパターンをクリアします。
ercd = clr_flg(flag, ~event_from_task2);
main()関数内で、データキューを生成します。
定義すべきデータキューの属性については以下に記載されています。
https://solid.kmckk.com/SOLID/doc/latest/os/kernel/kernel_config.html#id8
今回はこのように作成します。
pk_cdtq.dtqatr = TA_TPRI;
pk_cdtq.dtqmb = NULL;
pk_cdtq.dtqcnt = 3;
ercd = acre_dtq(&pk_cdtq);
dtq = ercd;
あるタスクがデータキューにデータを入力し、その値を他のタスクが取得することにより、タスク間でデータのやり取りができます。
・データキューにデータ入力
データ’A’を入力してみます。
VP_INT data;
data = 'A';
snd_dtq(dtq, data);
・データの取得
データをデータキューから取得します。
VP_INT data;
rcv_dtq(dtq, &data);
以下のようになりました。
※コード簡素化のため、エラーチェック等は省いております。
タスク1:
void test_task1(VP_INT exinf)
{
FLGPTN ptn;
ER ercd;
VP_INT data;
SOLID_LOG_printf("[TASK1 ] Start TASK1\n");
while (1)
{
// TASK2に向け、イベントフラグ設定
SOLID_LOG_printf("[TASK1 ] Wakeup TASK2.\n");
ercd = set_flg(flag, event_from_task1);
// TASK2からのイベントフラグ設定待ち処理
SOLID_LOG_printf("[TASK1 ] Waiting event from TASK2...\n");
ercd = wai_flg(flag, event_from_task2, TWF_ORW, &ptn);
SOLID_LOG_printf("[TASK1 ] Got event from TASK2.\n");
// データキューから'A'をセット
rcv_dtq(dtq, &data);
SOLID_LOG_printf("[TASK1 ] Got data from TASK2 = %c\n", data);
// TASK2から受けたイベントフラグ設定クリア
ercd = clr_flg(flag, ~event_from_task2);
dly_tsk(5000'000); //5sec
SOLID_LOG_printf("----------------\n");
}
return;
}
タスク2:
void test_task2(VP_INT exinf)
{
FLGPTN ptn;
ER ercd;
VP_INT data;
SOLID_LOG_printf("[ TASK2] Start TASK2\n");
while (1)
{
// イベントフラグ待ち処理
SOLID_LOG_printf("[ TASK2] Waiting event from TASK1...\n");
ercd = wai_flg(flag, event_from_task1, TWF_ORW, &ptn);
SOLID_LOG_printf("[ TASK2] Got event from TASK1.\n");
ercd = clr_flg(flag, ~event_from_task1);
// イベントフラグセット
SOLID_LOG_printf("[ TASK2] Reply to TASK1.\n");
ercd = set_flg(flag, event_from_task2);
// データキューに'A'をセット
data = 'A';
SOLID_LOG_printf("[ TASK2] Set data = %c\n", data);
snd_dtq(dtq, data);
dly_tsk(1000'000); //1sec
SOLID_LOG_printf("------------------------------\n");
}
return;
}
実際に動かしてみます。
シリアルターミナルにこのように出力されました。
タスク1とタスク2で、イベントのやり取りが行われていること、データの受け渡しができていることがわかります。
このような排他制御として他に、ミューテックスもよく使われますね。
もちろんSOLIDでも使用できます。
組み込みのリアルタイムOSを扱う際、もう一つの重要な要素といえば、割り込み制御です。
タスクだけでなく、割り込み処理の仕組みを理解することも重要になってきます。
SOLIDには、割り込みを登録・設定・変更するためのAPIが準備されています。
http://solid.kmckk.com/doc/skit/current/os/cs/intc.html
これらを使って、割り込みを使ってみます。
割り込みとしては、GPIOを使用してみます。
Raspberry Pi4ボードの27ピンから出ているGPIO0端子とします。
GPIO0端子にボタンを付けて、ボタン押下を割り込みで取得しようと思います。
以前のRust連載の際に、SPI割り込みを使用するために、「割り込みハンドラ登録用構造体」の設定を行いました。
今回も同じく、この構造体に設定をするところから始めます。
typedef struct _SOLID_INTC_HANDLER_ {
int intno;
int priority;
int config;
int (*func)(void*, SOLID_CPU_CONTEXT*);
void* param;
} SOLID_INTC_HANDLER;
この構造体についての説明は以下URLに記載されています。
http://solid.kmckk.com/doc/skit/current/os/cs/intc.html#solid-intc-handler
メンバについて抜粋すると、以下です。
・int intno:割り込み番号
・int priority:処理優先度
最大レベル値 は、SOLID_INTC_GetPriorityLevel() で取得できる値です。
・int config:割り込み設定(ICFGR)
・int (*func)(void*, SOLID_CPU_CONTEXT*):割り込み発生時に呼び出される関数
・void *param:割り込み発生時に関数に引き渡される第一引数
ここで一番悩むのは、intno。
「割り込み番号って何番?」ですね。
これは割り込みのルーティングを追っていかなければなりません。
以前と同じように考えてみます。
まず、Raspberry Pi4では、
・レガシーコントローラ(独自のもの)
・GIC-400コントローラ(Cortex標準のもの)
の二つの割り込みコントローラが存在し、どちらか片方を使用できます。
SOLID-OSでは、GIC-400コントローラを使用します。
次に、今回対象のGPIO割り込みが、GIC-400のどの割り込み番号に対応するか、を調べます。
BCM2711 ARM Peripherals から、GPIO0はVideoCore (VC) peripheralにIRQ40として入力されていることがわかります。
(BCM2711 ARM Peripherals から抜粋)
このVC peripheralは、GIC-400にどうつながっているかというと、
(BCM2711 ARM Peripherals から抜粋)
GIC-400割り込みに、オフセット96でつながっています。
なので、GPIOの割り込み番号は、96 + 49 = 145 となります。
intnoには145と設定すればOKですね。
このように、CPUが理解できる割り込み番号を知るためには、割り込みのルーティングを追う必要があります。
冒頭に「今回はRaspberry Pi4を使用します」としましたが、Rasbperry Pi4のBCM2711チップでは、このようにルーティングが二段構えになっているため、「ルーティングを追う」という事の具体例としてちょうど良かったことが理由です。
割り込み番号はわかりました。
しかし、ボタン押下で割り込みを発生させるためには、まだまだやることがあります。
・GPIO側の設定:入力モードにし、立下りエッジで割り込みを発生させるように設定
・割り込みハンドラ記述、登録
・GIG-400の設定:割り込み番号を指定して有効化する
・割り込み優先度の決定
次回、実装をしていきます。
今回は、タスクを生成し、タスク間通信を行いました。
ここに割り込みを絡めてどう動くか、まで行こうとしましたが、時間切れとなってしまいました。
次週は割り込みの実装を行い、複数タスク実行&割り込みでの動きを見ていこうと思います。