(2023/8/2)
[連載9] SOLIDでタスクと割り込み―2
前回に引き続き、タスクと割り込みについて見ていきます。
今回は「割り込み」を行うプログラムを作ってみます。
今回も引き続き、Raspberry Pi4を使用します。
タスク1とタスク2、それに割り込みを加え、プログラムを作ってみます。
前回作成した、タスク1とタスク2の間でイベント受け渡し&データキューによるデータ受け渡しを行うプログラムをベースとします。
以下のように動作していました。
ここに割り込みを追加するのですが、どういう風にしましょうか。
ボタンを押すと、タスク2にイベントが発生する、というのはどうでしょうか。
タスク2はそのイベントを待つ。割り込みが発生しない限り、無限に待つ。
・・・うん、いいですね。そうしましょう。
ボタン押下(GPIO割り込み入力)によって起動する割り込みハンドラで、タスク2にイベントを送信します。
タスク2はそのイベントを受けないと、先に進みません。
タスク2が進まないのでタスク1に対してのイベントも投げられない。
という事はタスク1も止まる。
そして、ボタンを押すと、タスク2もタスク1も動き出す。
しばらくすると、またタスク2は割り込み待ちに入る。。。
という動きになるはず、です。
使用するGPIOは、前回はGPIO0端子としようと思ったのですが、よく考えるとRaspberry Pi 4においてはGPIO0に他機能(ID_SD)も想定されています。
公式サイトのピン配置図を見てみます。
https://www.raspberrypi.com/documentation/computers/raspberry-pi.html
#https://www.raspberrypi.com/documentation/computers/raspberry-pi.html から引用
ですので、安全のため、他に何も接続されていないGPIO26を選択することにします。
左下のGroundの上にある端子です。
割り込み番号は、前回計算した値と変わらず、145 です。
では、書いていきましょう。
実装していきます。
ボタンには、タクトスイッチを使いました。
接続図は以下です。
#https://www.raspberrypi.com/documentation/computers/raspberry-pi.html の図を引用
接続した様子を写真に撮りました。
GPIO26端子を入力モードにし、かつ、立ち上がりエッジで割り込みを発生させるようにします。
前回、「立ち下りエッジ」と書きましたが、「立ち上がり」にします。
深い意味はありませんが、ボタンを接続する際に通常状態でLowに引っ張りボタン押下でHighになるようにしたので、そのようにします。
では、一気に設定します。
void gpio_init()
{
//GPIO26を入力にする
volatile uint32_t *reg_GPFSEL2 = (volatile uint32_t *)(GPIO_BASE + 0x08);
*reg_GPFSEL2 = *reg_GPFSEL2 & ~(0x001c0000); //bit18-20 <= 0b000
// GPIO26立ち上がりエッジを検出するよう設定
volatile uint32_t *reg_GPREN0 = (volatile uint32_t *)(GPIO_BASE + 0x4c);
int mode = 1;
*reg_GPREN0 = (*reg_GPREN0 & ~(GPIO_NUM % 32)) | (mode << (GPIO_NUM % 32));
// (念のための処置) GPIO26立ち上がりエッジ検出フラグをクリアしておく
// 次回このフラグが1になれば、GIG-400に対し割り込みを要求する
volatile uint32_t *reg_GPEDS0 = (volatile uint32_t *)(GPIO_BASE + 0x40);
*reg_GPEDS0 = 0x04000000; //Clear detected event
}
次に、割り込みが発生した際の飛び先である関数を記述します。
タクトスイッチを押したら、何かシリアルターミナルに表示するようにしてみます。
加えて、GPIO26立ち上がりエッジ検出フラグをクリアしておきましょう。
int gpioint_handler(void *param, SOLID_CPU_CONTEXT *cnt)
{
// GPIO26立ち上がりエッジ検出フラグをクリアしておく
// 次回このフラグが1になれば、GIG-400に対し割り込みを要求する
volatile uint32_t *reg1 = (volatile uint32_t *)(GPIO_BASE + 0x40);
*reg1 = 0x04000000;
SOLID_LOG_printf("Button was pressed!\n");
return 0;
}
最後に、この割り込みを使えるようにします。
・割割り込み発生時にこの割り込みハンドラに飛んできてもらうための設定
・GIG-400割り込みコントローラに対し、割り込み番号や優先順位、その他を設定
・設定した割り込みを有効化
SOLIDのAPIをコールすると、一気に行ってくれます。
手順は以下です。
①割り込みハンドラ登録用構造体を作成
②登録
③有効化
まず、前回に書いた通り、割り込みハンドラ登録用構造体を作成するところから始めます。
割り込みハンドラ登録用構造体とは以下です。
typedef struct _SOLID_INTC_HANDLER_ {
int intno;
int priority;
int config;
int (*func)(void*, SOLID_CPU_CONTEXT*);
void* param;
} SOLID_INTC_HANDLER;
この構造体に設定をし、SOLID-OSのAPIで登録&有効化を行います。
SOLID-OSのAPI を使用するためには、#include <solid_intc.h>が必要です。
#この構造体も含め、割り込み関連のAPIは以下のURLに記載されています。
http://solid.kmckk.com/doc/skit/current/os/cs/intc.html
void gpioint_init()
{
// ① 割り込みハンドラ登録用構造体を作成
g_handler.intno = 145;
g_handler.priority = 10;
g_handler.config = 0b10; // SPI, エッジトリガ
g_handler.func = gpioint_handler;
g_handler.param = NULL;
//割り込み対象のCPUコアを指定する場合は以下のマルチコア用APIを使用する。
// int target_processor = 1;
// int ret = SOLID_INTC_RegisterWithTargetProcess(&g_handler, 1 << target_processor);
// ret = SOLID_INTC_EnableM(g_handler.intno);
// ② 登録
int ret = SOLID_INTC_Register(&g_handler);
// ③ 有効化
ret = SOLID_INTC_Enable(g_handler.intno);
}
ソースコード内のコメントで記載していますが、今回は特にマルチコア用APIを使用しません。
割り込み先のCPUを固定したい場合、上記コメントアウト内のマルチコア用APIを使用します。今回は特に気にしないので、使用しません。
実行してみます。
タクトスイッチを押すと、上記のように表示されました。
では次に、割り込み発生をタスクに伝える、という事を試してみます。
割り込みハンドラから、イベントを発行し、そのイベントをタスク2で待つようにしてみましょう。
割り込みハンドラで発行するイベントは、以下のように定義してみます。
FLGPTN event_from_interrupt = 0x04;
以下のように、setflag関数でイベントを発行します。
int gpioint_handler(void *param, SOLID_CPU_CONTEXT *cnt)
{
// GPIO26立ち上がりエッジ検出フラグをクリアしておく
// 次回このフラグが1になれば、GIG-400に対し割り込みを要求する
volatile uint32_t *reg1 = (volatile uint32_t *)(GPIO_BASE + 0x40);
*reg1 = 0x04000000;
SOLID_LOG_printf("Button was pressed!\n");
ER ercd;
ercd = set_flg(flag, event_from_interrupt);
return 0;
}
以下のように、wai_flag関数で該当するイベント発生を待ちます。
イベントを受け取ったら、次回のためにクリアしておきます。
SOLID_LOG_printf("[ TASK2] Waiting event from interrupt...\n");
dly_tsk(100'000); //0.1sec待つ。上記printfが終わってからフラグを待つようにしたいため。
ercd = wai_flg(flag, event_from_interrupt, TWF_ORW, &ptn);
SOLID_LOG_printf("[ TASK2] Got event from interrupt.\n");
ercd = clr_flg(flag, ~event_from_interrupt);
wai_flag関数直前にdly_tsk関数で0.1秒待っているのは、その上にあるSOLID_LOG_printf関数によるターミナルへの文字送信が終わるまで待ちたい、という意図です。
でないと、後々ターミナル上で動作を確認する際に、タクトスイッチを押してからおもむろに"[ TASK2] Waiting event from interrupt...“ が表示されてしまい、「???」となるからです。
では実行してみましょう。
タスク2は、割り込みハンドラからのイベントを待っています。
タスク1は、タスク2からのイベントを待っています。
割り込みが発生しないことには、ずっとここで両者待っています。
では、タクトスイッチを押してみます。
割り込みが発生し、タスク2に対してイベントが発行できたので、無事、タスク2が動きました。
そのおかげで、タスク1もタスク2からイベントを受け取ることができ、動くことができました。
そしてまた、割り込み待ち状態になりました。
意図通りに動いていますね。
タスク同様、割り込みにも、優先度を設定することができます。
優先度の高い割り込み、とは、その事象が発生したらできるだけ速やかに対応する処理を行いたい割り込みです。
例えば、
割り込みA:
定期的に発生するが、即時応答できなくても比較的大丈夫
割り込みB:
いつ発生するかわからない。発生したらすぐに対応して欲しい
という、二種類の割り込みがあったとします。
この場合、割り込みAの優先順位<割り込みBの優先度、としておけば、割り込みAの処理中に割り込みBが発生した場合、割り込みBの処理を優先して行う事ができます。
逆に、割り込みBの処理中に割り込みAが発生した場合、割り込みAの処理は待たされるか、無視されるか、、、それは割り込みの設定によって異なります。
こういった、割り込み処理中に別の割り込みが発生することを、多重割り込み、と言います。
しかし多重割り込みの実現には、ソフトウェアの協力が必須です。
割り込みの発生時は、CPUが自動でスタック等を退避してくれます。CPUが自動で退避してくれたものを、ありがたくそのまま使ってしまうと、多重割り込み時に、なんと!全部上書きされてしまいます。
なので、ソフトウェアでさらに退避させる、といった処理が必要です。
さらに優先度は、状況によって変わるかもしれません。
でも大丈夫です。優先順位は途中で変更することができます。
SOLID-OSでは、多重割り込みを使うことができ、さらに優先順位を簡単に変更できるAPIがあります。
他にも、割り込みの状態の確認、指定した割り込みの優先順位の取得、待たされている割り込みのクリア等、割り込み操作に関するAPIは以下URLにまとまっています。
https://solid.kmckk.com/SOLID/doc/latest/os/cs/intc.html#solid-intc-setintpriority
割り込みにより実行される機能のなかでも、良く目につくものの一つに「周期ハンドラ」があります。
先程のタスク1,タスク2には、5秒待つ、1秒待つ、等の操作がありました。
もう一度見てみます。
dly_tsk(5000'000); //5sec
SOLID_LOG_printf("----------------\n");
}
return;
}
この dly_tsk()です。
この関数は、ここで5秒待ちます。
すなわち、このタスクは、厳密に5秒周期で動作しているわけではありません。
何やら行って⇒5秒待って⇒何やら行って⇒5秒待って、、、
の繰り返しなので、5秒以上の周期ですね。
厳密な周期で行いたい物事がある場合は、タイマ割り込みを使用した周期ハンドラというものを使う事になります。
SOLID-OSでは、周期ハンドラを簡単に使用できるようにするため、タイマ割り込み専用のAPIがあります。割り込み番号等調べることも必要ありません。
https://solid.kmckk.com/SOLID/doc/latest/os/cs/timer.html
このAPIを使ったサンプルはこちらにあります。
https://github.com/KyotoMicrocomputer/solid-rapi4-examples/tree/main/cpp-blinky-cs
前回、今回と、タスクに加え、割り込みを見てきました。
また即時応答である「割り込み」が実装できること、は組み込みシステムにおいて必須の要素なので、実装の助けとなるAPIがある事はありがたいですね。
ベクタへの登録、ARMコアの割り込みコントローラの各レジスタへの設定、、、等々、デバイスマニュアルとにらめっこが必要な要素が少ないのはラクだし、間違いが発生する要素も少ないので助かります。
次回は、マルチコアCPUでのリアルタイムOSについて書く予定です。