本連載では、小学生向きのプログラミング教育などに使われているBBC micro:bit(以下「micro:bit」)の上で動くようになったµT-Kernel 3.0をご紹介している。連載第6回の本号では、リアルタイムOSの処理として不可欠な「割込み」について説明する。micro:bitのボタンスイッチを使って割込みを発生させ、µT-Kernel 3.0の割込みハンドラを起動する動作を確認しよう。
「割込み」とは、デバイスなどの外部ハードウェアの入力信号によって、直接的にプログラムの実行の流れを変え、別のプログラムを実行する機能である。
割込みの概念的な動作を図1に示す。割込みが発生しなかった場合のプログラムの流れは(1)→(2)→(5)→(6)である。一方、(2)の実行中に割込みが発生し、割込みハンドラが動作した場合のプログラムの流れは(1)→(2)→(3)→(4)→(5)→(6)となる。つまり、(3)→(4)の割込みハンドラの実行が、通常の(1)→(2)→(5)→(6)のプログラムに「割り込んだ」形になっている。
割込み発生の原因となったハードウェアの状態変化を割込み要因とよぶ。デバイスとのデータ転送や通信に割込みを使う場合は、新しいデバイスの接続、新しいデータの到着、データ転送の開始や終了などが割込み要因となる。たとえば、開発用ホストPCのUSBポートにmicro:bitを接続すると、その旨のメッセージがPCの画面に表示されるが、このような動作を効率よく行うには、USBポートへのmicro:bitの接続を割込み要因として割込みを発生させ、その割込みハンドラの中でmicro:bitとの通信などの処理を起動すればよい。
別の方法として、割込みの機能を使わずに、USBポートを定期的に監視するようなプログラムを常時動かしておく方法(「ポーリング」とよぶ)でも同様の動作を実現することはできる。しかし、定期的な監視という動作には無駄な部分がある。監視の間隔を短くすれば無駄な処理が多くなって消費電力が増えるし、逆に監視の間隔を長くすれば、micro:bitを接続したことが直ちに認識されず、通信などの処理が始まるまでの応答時間が延びてしまう。処理の応答性を高めつつ、無駄なプログラムの実行を避けるためには、割込みの利用が欠かせない。
割込みの発生時に実行されるプログラムを「割込みハンドラ」とよぶ。リアルタイムOSを利用している場合は、OSの機能を使ってその割込みハンドラを定義できる。µT-Kernel 3.0では、割込みハンドラを定義するシステムコールとしてtk_def_intを使う。
今回は、micro:bitのボタンスイッチAまたはBを押したときに割込みが発生し、その旨のメッセージをコンソールに出力するプログラムを作成する。コンソールへの出力にはT-Monitor互換ライブラリのtm_printfを使用するが、この処理には時間を要する。そこで、割込みハンドラの中で直接tm_printfを実行するのではなく、コンソール出力用の別タスクを用意して、その中でtm_printfを実行する。
割込みハンドラからコンソール出力用タスクに対して、ボタンスイッチが押されたという情報を伝えるには、イベントフラグを使う。割込みハンドラは、ボタンスイッチの押下による割込みで起動され、その情報をコンソール出力用タスクに伝えるためにイベントフラグをセットする。ボタンスイッチにはAとBがあるので、Aが押された場合はイベントフラグの最下位のビットをセットし、Bが押された場合は最下位より一つ上位のビットをセットする。
コンソール出力用タスクでは、イベントフラグがセットされるのを待っていて、イベントフラグがセットされたらtm_printfを実行してメッセージを出力する。その際には、セットされたビットを見て、ボタンスイッチA、Bのどちらが押されたのかを区別する。これらの処理を繰り返し行うために、for文を使った無限ループの中に入れる。作成したプログラムの全体構成を図2に、プログラムリストをリスト1に示す。
/*--------------------------------------------------------------- * ボタンスイッチによる割込み発生の動作確認(μT-Kernel 3.0用) * * Copyright (C) 2022-2023 by T3 WG of TRON Forum *---------------------------------------------------------------*/ #include <tk/tkernel.h> #include <tm/tmonitor.h> // GPIOTEのレジスタ定義 #define GPIOTE(r) (GPIOTE_BASE + GPIOTE_##r) #define GPIOTE_BASE 0x40006000 #define GPIOTE_EVENTS_IN(n) (0x100 + (n) * 4) #define GPIOTE_INTENSET 0x304 #define GPIOTE_CONFIG(n) (0x510 + (n) * 4) // GPIOTE CONFIG[n]のレジスタの設定値 #define GPIOTE_CONFIG_Event 1 #define GPIOTE_CONFIG_Task 3 #define GPIOTE_CONFIG_LoToHi 1 #define GPIOTE_CONFIG_HiToLo 2 #define GPIOTE_CONFIG_Toggle 3 // GPIOTEの割込み番号と割込み優先度レベル #define INTNO_GPIOTE 6 // GPIOTEの割込み番号(※G) #define INTPRI_GPIOTE 6 // GPIOTEの割込み優先度レベル(※J) // ボタンスイッチA,Bに対応するGPIO P0のピン番号 #define GPIO_P0_SW_A 14 #define GPIO_P0_SW_B 23 // オブジェクトID番号 ID tskid; // メッセージ表示用タスクのID ID flgid; // 割込み通知用イベントフラグのID // ボタンスイッチ入力のためのGPIOの初期設定 LOCAL void btn_init(void) { // ボタンスイッチに対応するGPIO P0のピンを入力に設定 out_w(GPIO(P0, PIN_CNF(GPIO_P0_SW_A)), 0); out_w(GPIO(P0, PIN_CNF(GPIO_P0_SW_B)), 0); } // GPIOTEの割込みハンドラ LOCAL void btn_inthdr(UINT intno) { INT n; // GPIOTEのチャンネル番号(0または1) for (n = 0; n < 2; n++) { // GPIOTEのチャンネル0とチャンネル1を順に処理 // EVENTS_IN[n]の最下位ビットを見てイベント発生状態を確認(※D) if (in_w(GPIOTE(EVENTS_IN(n))) & (1 << 0)) { // イベントがあった場合はイベント発生状態をクリア(※E) out_w(GPIOTE(EVENTS_IN(n)), 0); // 割込み通知用イベントフラグの対応ビットをセット tk_set_flg(flgid, (1 << n)); } } } // メッセージ表示用のタスク void task(INT stacd, void *exinf) { BOOL btn_a, btn_b; UINT flgptn; for(;;){ // 永久に繰り返し // 割込み通知用イベントフラグの下位2ビットをOR待ち tk_wai_flg(flgid, 0b11, (TWF_ORW | TWF_CLR), &flgptn, TMO_FEVR); if((flgptn & (1 << 0)) != 0) // 最下位ビットが1の場合 tm_printf("Button_SW_A: pushed\n"); // ボタンスイッチAの押下を表示 if((flgptn & (1 << 1)) != 0) // 最下位から2番目のビットが1の場合 tm_printf("Button_SW_B: pushed\n"); // ボタンスイッチBの押下を表示 } } // タスクとイベントフラグの生成情報 const T_CTSK ctsk = {0, (TA_HLNG | TA_RNG3), &task, 10, 1024, 0}; const T_CFLG cflg = {0, (TA_TFIFO | TA_WSGL), 0}; // 割込みハンドラ定義情報 const T_DINT dint = {TA_HLNG, btn_inthdr}; // メインプログラム EXPORT void usermain( void ) { btn_init(); // ボタンスイッチ入力のためのGPIOの初期設定 // 以下、ボタンスイッチの押下でGPIOTEのイベントを発生するための設定 // ボタンスイッチAでチャンネル0のイベントを発生(※A) out_w(GPIOTE(CONFIG(0)), // チャンネル0を指定 (GPIOTE_CONFIG_Event << 0) // MODEにEventを指定 | (GPIO_P0_SW_A << 8) // PSELにSW_AのGPIOピン番号14を指定 | (0 << 13) // PORTにSW_AのGPIOポート番号0を指定 | (GPIOTE_CONFIG_HiToLo << 16)); // POLARITYにHiToLo(立ち下がり)を指定 // ボタンスイッチBでチャンネル1のイベントを発生(※B) out_w(GPIOTE(CONFIG(1)), // チャンネル1を指定 (GPIOTE_CONFIG_Event << 0) // MODEにEventを指定 | (GPIO_P0_SW_B << 8) // PSELにSW_BのGPIOピン番号23を指定 | (0 << 13) // PORTにSW_BのGPIOポート番号0を指定 | (GPIOTE_CONFIG_HiToLo << 16)); // POLARITYにHiToLo(立ち下がり)を指定 // GPIOTEのチャンネル0とチャンネル1の割込みを許可(※C) out_w(GPIOTE(INTENSET), (1 << 0) | (1 << 1)); flgid = tk_cre_flg(&cflg); // 割込み通知用のイベントフラグ作成 tskid = tk_cre_tsk(&ctsk); // メッセージ表示用のタスク生成 tk_sta_tsk(tskid, 0); // メッセージ表示用のタスク起動 tk_def_int(INTNO_GPIOTE, &dint); // 割込みハンドラ定義(※F) EnableInt(INTNO_GPIOTE, INTPRI_GPIOTE); // 割込み許可(※H) tk_slp_tsk(TMO_FEVR); return; } |
ボタンスイッチの押下により割込みを発生させるには、どうすればよいだろうか。前回の記事で説明したとおり、micro:bitのボタンスイッチAはGPIO P0のピン14に、ボタンスイッチBはGPIO P0のピン23に接続されている。したがって、GPIOのこれらのピンの状態変化により割込みを発生できればよい。
micro:bitのGPIOで割込みを発生させるには、GPIOTE(GPIO tasks and events)とよばれる機能を使う。この機能は、GPIOのいずれかのピンに、タスク(task)あるいはイベント(event)を割り当てる機能である。なお、ここでいうタスク(task)とイベント(event)の語は、Target MCUであるnRF52833のGPIOの持つハードウェア機能の一部を指すものであり、µT-Kernel 3.0のタスクやイベントフラグを意味するわけではない。
GPIOTEのタスク(task)の機能を使うと、GPIOの特定のレジスタへの書き込みによって、指定したGPIOピンへの出力を柔軟に制御することができる。また、GPIOTEのイベント(event)の機能を使うと、GPIOピンの入力状態の変化によりGPIOの内部でイベントを発生し、そのイベントによってCPUに割込みをかけることができる。GPIOTEは最大で8チャンネルまで設定することが可能であり、チャンネルの番号n(n=0..7)を使って複数のチャンネルを区別する。
GPIOTEの仕様や動作の詳細については、nRF52833のマニュアルのGPIOTEのセクション(*1) に詳しい説明がある。このマニュアルから、GPIOTEの動作に関連するレジスタの説明を要約したものを表1に示す。なお、GPIOTEのベースアドレスは0x40006000である。
Register | Offset | Description |
---|---|---|
TASKS_OUT[n] | 0x000+(n*4) | Task for writing to pin specified in CONFIG[n].PSEL. Action on pin is configured in CONFIG[n].POLARITY. |
TASKS_SET[n] | 0x030+(n*4) | Action on pin is to set it high. |
TASKS_CLR[n] | 0x060+(n*4) | Action on pin is to set it low. |
EVENTS_IN[n] | 0x100+(n*4) | Event generated from pin specified in CONFIG[n].PSEL |
INTENSET | 0x304 | Enable interrupt |
INTENCLR | 0x308 | Disable interrupt |
CONFIG[n] | 0x510+(n*4) | Configuration for OUT[n], SET[n], and CLR[n] tasks and IN[n] event |
図3 CONFIG[n]レジスタの構成と各ビット
フィールドの機能(*1の資料の一部を要約)
図4 INTENSETレジスタの構成と各ビット
フィールドの機能(*1の資料の一部を要約)
今回のプログラムでは、ボタンスイッチAの押下による状態変化をGPIOTEのチャンネル0のイベントとして扱い、ボタンスイッチBの押下についてはチャンネル1のイベントとして扱うことにする。GPIOTEのタスク(task)の機能は使用しない。
まず、ボタンスイッチAの押下によってチャンネル0のイベントが発生するように、GPIOTEを設定する。設定には表1のCONFIG[n]のレジスタを使う。このレジスタは32ビット幅であるが、32ビットのデータをいくつかのビットフィールドに分けて使用する。CONFIG[n]レジスタの構成と各ビットフィールドの位置や機能を図3に示す。
ボタンスイッチAの押下でイベントを発生させる場合、MODEにはEventを表す1を、PSELにはボタンスイッチAに接続されたGPIO P0のピン番号である14を、PORTにはGPIO P0を表す0を設定する。次のPOLARITYにはHiToLoを設定する。ボタンスイッチからのGPIO入力は、押した場合に0、離した場合に1となるので、ボタンスイッチを押したときにはGPIO入力が1から0に変化する(*2) 。そのタイミングでイベントを発生させるには、POLARITYにHiToLoを設定して、GPIO入力の立ち下がり(falling edge)でIN[n]のイベントを発生させればよいのである。なお、最後のOUTINITはタスクモードで使用する機能なので、設定の必要はない。
このイベントではチャンネル0を使うので、これらの設定値をGPIOTEのCONFIG[0]のレジスタに書き込む。書込みの際は、設定値を図3で示すビットフィールドの位置に合わせる必要がある。たとえばPSELの場合、PSELのビットフィールド(BBBBB)が最下位より8番目のビット位置から始まっている。したがって、PSELに14を設定するには、設定値の14を8ビットだけ左シフトする必要がある。他の設定値も同様の処理をしたうえで、全設定値のビット単位の論理和(OR)をCONFIG[0]に書き込む。実際のGPIOTEの初期設定プログラムは、リスト1の(※A)の箇所をご覧いただきたい。ボタンスイッチBの押下でチャンネル1のイベントを発生させるための初期設定のプログラムも同様である(リスト1の(※B))。
次に、GPIOTEの各チャンネルで発生したイベントによって割込みを発生させるための設定を行う。そのために使うのが表1のINTENSETレジスタである。このレジスタの構成と機能を図4に示す。GPIOTEの各チャンネルのイベントによる割込みの許可を示すビットが8チャンネル分並んでおり、最下位ビットがチャンネル0に対応する。今回はチャンネル0と1のイベントに対する割込みを発生させたいので、最下位のビットとその一つ上位のビットのみを1にした数値をこのレジスタに書き込む(リスト1の(※C))。なお、最上位ビットのIは使用しないので0のままでよい。これで、割込みを使うためのGPIOTEの初期設定は完了した。
このほか、実際に割込みが発生した場合には、割込みハンドラの中で割込み要因を確認する処理も必要である。GPIOTEの各チャンネルのイベント発生の有無は、EVENTS_IN[n]レジスタの最下位ビットから取得できるので、このレジスタを読み出すことにより各チャンネルのイベント発生状態を確認し、割込みの原因を特定する。今回はボタンスイッチA、Bという二つの割込み要因があるので、最初にEVENTS_IN[0]レジスタを見てチャンネル0のイベント発生を確認し(リスト1の(※D))、ボタンスイッチAによる割込みの有無を判断する。次に、EVENTS_IN[1]レジスタを見てチャンネル1のイベント発生を確認し、ボタンスイッチBによる割込みの有無を判断する。
イベントが発生してその状態を確認した場合には、イベント発生状態(EVENTS_IN[n]レジスタの最下位ビット)をクリアしておく必要がある(リスト1の(※E))。そうしないと、イベントが発生したままの状態が継続するため、もう一度ボタンスイッチを押しても、新しいイベントの発生が検出できなくなるからだ。また、GPIOTEからの割込み発生の要求も出続けたままとなるので、割込みハンドラから戻っても、直後に再度同じ割込みハンドラが起動されてしまう。ボタンスイッチの押下を止めても、自動的にEVENTS_IN[n]レジスタがクリアされるわけではないし、発生したイベントが消えるわけでもないので、プログラムで明示的にこのレジスタをクリアしなければならない。
ボタンスイッチの押下によりGPIOTEが発生する割込みに対して、µT-Kernelのシステムコールtk_def_intを使って割込みハンドラを定義する(リスト1の(※F))。その際の引数として、割込み番号(intno)を指定する必要がある。
GPIOTEに対する割込み番号を知るには、Target MCUであるnRF52833のマニュアルのうち、Peripheral interfaceの章を参照する(*3) 。この中のPeripheral IDの説明を読むと、各周辺デバイスに割り当てられたメモリアドレスの範囲と周辺デバイスID(peripheral ID)が1対1で直接対応しているという説明がある。一つの周辺デバイスに割り当てられたメモリアドレスの範囲は0x1000(10進数では4096バイト)であり、メモリアドレスが0x40001000..0x40001FFFの周辺デバイスに対応するPeripheral IDは1、0x4001F000..0x4001FFFFの周辺デバイスに対応するPeripheral IDは31(16進数の1F)である。GPIOTEについてはベースアドレスが0x40006000であり、メモリアドレスの範囲が0x40006000から始まるので、周辺デバイスIDは6であるとわかる。
また、同じ章のInterruptsの説明を読むと、割込み番号(interrupt number)は周辺デバイスID(peripheral ID)に従うという説明がある。さらに、µT-Kernel 3.0のソースコードに付属している実装仕様書の「4.5.1 割込み番号」の項目を見ると、「OSの割込み管理機能が使用する割込み番号はマイコンの外部割込みの番号と同一」と書かれている。結局、GPIOTEの周辺デバイスIDである6が、そのままハードウェア上の割込み番号となり、tk_def_intの引数として使用する割込み番号INTNO_GPIOTEにもなる(リスト1の(※G))。
割込みハンドラを定義した後には、同じ割込み番号に対して割込みを許可するシステムコールEnableIntを発行しておく(リスト1の(※H))。EnableIntの2番目の引数には割込み優先度レベルINTPRI_GPIOTEを指定するが、実装仕様書の「4.6.1 割込みの優先度」には「割込みの優先度は2から6が使用可能」と書かれているので、この中で最も低い優先度である6を設定する(リスト1の(※J))。
ここまでの説明で、micro:bitのハードウェアに依存した割込み関係の情報がすべて揃い、リスト1のプログラムが完成した。さっそくこのプログラムをEclipseの画面に入れてコンパイルし、micro:bitの上で実行してみよう。ボタンスイッチA、Bを押すたびに、コンソールにメッセージが出力される様子を確認できる(リスト2)。見かけの動作は前回のプログラムとほとんど同じだが、前回はボタンスイッチの状態を約0.5秒(500ms)の間隔で監視していたのに対し、今回は割込みを使うことによって定期的な監視に伴う無駄な処理が無くなり、応答速度も速くなっているはずだ。
microT-Kernel Version 3.00 Button_SW_A: pushed ←ボタンスイッチAを押す Button_SW_B: pushed ←ボタンスイッチBを押す Button_SW_A: pushed ←ボタンスイッチAを押す Button_SW_A: pushed ←ボタンスイッチAを押す Button_SW_B: pushed ←ボタンスイッチBを押す |
* * *
今回はmicro:bitで割込みを使うプログラムを紹介した。割込みの動作もハードウェアに依存する部分が多いため、MCUやハードウェアのマニュアルを参照して技術情報を確認する手順が不可欠である。本稿ではその具体的な方法を説明した。
次回はmicro:bitのLEDを操作するプログラムを作成する予定だ。