割り込み

割り込み管理機能

前回までは主にタスクが主体となって行う処理を中心にT-Kernelの機能を説明してきました。それに対して割り込みは、タスクとは独立して実行される処理です。そこで、T-Kernelにおける割り込みの利用方法に加えて、実行時のコンテキストの違いから生じる動作の違い、割り込みハンドラの作成方法や動作の詳細を説明します。

割り込みとは?

割り込みとは、その名の通り、実行中の処理を中断して(割込んで)別の処理を実行することです。割り込みは、主に周辺機器が状態の変化をCPUに通知する時や、CPU例外をプログラムで処理する時に用いられます。例えば、キーボードを叩くとキーに対応した文字が画面に表示されますが、この「キーが叩かれたこと」は割り込みを用いてCPUに通知されることが多いです。

図1

【図1:割り込み】

割り込みの利点は何でしょう?割り込みを利用すると、CPU資源を効率的に使用しながら応答性を向上することができます。割り込みを用いない場合は、CPUが定期的に周辺機器の状態を調べる必要があります。これをポーリングと呼びます。ポーリングでは最悪の場合、周期時間だけ遅れてから周辺機器の状態変化を知ることになります。そのため、高い応答性を得るには十分に短い時間間隔でポーリングを行わなければなりません。しかし、ポーリングの時間間隔を短くすると、その分だけCPU処理が増加してCPU負荷が高くなります。一方、割り込みを用いると、CPUは周辺機器が割り込みを要求するまでは他の処理を実行できます。もし他に実行すべき処理がなければCPUを休ませることもできます。割り込み要求がきたら実行中の処理を中断して割り込みに対応した処理を行うので高い応答性も実現できます。

しかし、割り込みも利点ばかりではありません。実行中の処理を中断して別の処理を行うということは、その別の処理が完了した後に元の処理へ復帰する必要があります。そのためには、割り込み発生後に実行中の処理を中断する時、再びその処理へ復帰できるように現在の状態を適切に保存しなければなりません。元の処理へ復帰する時も、保存された情報から適切に状態を戻さなければなりません。このように割り込み処理には若干のオーバーヘッドがあるため、極めて高頻度で割り込みが発生するような状況であれば、ポーリングで定期的に状態変化を調べる方が効率的な場合もあります。

図2

【図2:割り込みとポーリング】

T-Kernelの割り込み管理機能

割り込みが発生すると、実行中の処理が中断され、その割り込みに対応したコールバックルーチンが実行されます。このコールバックルーチンを割り込みハンドラと呼びます。T-Kernelの割り込み管理機能は、周辺機器からの外部割り込みとCPU例外を対象に割り込みハンドラの定義などの操作を行う機能です。T-Kernelでは割り込み管理用に以下のシステムコールを提供します。

tk_def_int 割り込みハンドラ定義
tk_ret_int 割り込みハンドラから復帰

tk_def_intで登録する割り込みハンドラは、高級言語(C言語)またはアセンブリ言語で記述することができます。

高級言語で記述する場合、カーネルの高級言語対応ルーチンを経由して割り込みハンドラが起動されます。高級言語対応ルーチンは実行中の状態の保存と復元を行います。具体的には、レジスタを退避したり復帰したりすることにより実行中の状態を保持します。割り込みハンドラは、C言語関数からのリターンによって終了します。その後、カーネルの高級言語対応ルーチンはtk_ret_intを呼び出します。

一方、アセンブリ言語で記述する場合、原則として、割り込みハンドラの起動時にはカーネルが介入しません。実装によってはプログラムによる処理が含まれる場合もありますが、割り込み発生時には、ハードウェアの割り込み処理機能により、tk_def_intで定義した割り込みハンドラが直接起動されます。割り込みハンドラの先頭と最後では、割り込みハンドラで使用するレジスタの退避と復帰を行う必要があります。割り込みハンドラは、tk_ret_intまたはCPUの割り込みリターン命令(またはそれに相当する手段)によって終了します。

図3

【図3:高級言語とアセンブリ言語】

また、システムコール以外にも、ライブラリ関数またはC言語のマクロとして提供されるAPIがあります。

【CPU割り込み制御】
DI すべての外部割り込み禁止
EI すべての外部割り込み許可
isDI すべての外部割り込み禁止状態の取得
【割り込みコントローラ制御】
DINTNO 割り込みベクタから割り込みハンドラ番号へ変換
EnableInt 割り込み許可
DisableInt 割り込み禁止
ClearInt 割り込み発生のクリア
EndOfInt 割り込みコントローラにEOI発行
CheckInt 割り込み発生の検査
SetIntMode 割り込みモード設定

ただし、割り込みに関する機能はハードウェアに強く依存するため、これらのライブラリ関数やマクロが実装されていない場合もあります。詳細は各実装仕様書を参考にしてください。実際、T-Kernel 2.0仕様書にも以下のような記述があります。

割り込み関係はハードウェア依存度が高く、システムごとに異なっているため共通化することが難しい。下記を標準仕様として定めるが、システムによってはこの通りに実装することが難しい場合がある。できる限り標準仕様に合わせた実装を求めるが、実装不可能なものは実装しなくてもよい。標準仕様とは別の機能を追加することも許されるが、その場合は関数名などは標準仕様と異なるものでなければならない。ただし、DI(), EI(), isDI() は標準仕様にしたがって、必ず実装しなければならない。

割り込みハンドラ実行時のコンテキスト

割り込みハンドラはタスクの進行とは全く別の要因で動作が始まり、どのタスクにも属さないコンテキストで処理が実行されます。言い換えれば、割り込みハンドラは割り込みが発生した時に実行中のタスクとは無関係のコンテキストで処理されます。一方、タスクが処理を実行している間は常に実行中のタスクに属するコンテキストで処理されます。T-Kernelでは、前者の割り込みなどのタスクには属さないコンテキストをタスク独立部と呼び、後者のタスクに属するコンテキストをタスク部と呼びます。

タスク独立部でもタスク部と同じ形式でシステムコールを発行することは可能です。タスク独立部の特徴は、タスク独立部に入る直前に実行中だったタスクを特定することに意味がないことです。そのため、「自タスク」の概念が存在しません。したがって、タスク独立部からは、待ち状態に入るシステムコールや、暗黙で自タスクを指定するシステムコールを発行することはできません。

また、タスク独立部では現在実行中のタスクを特定できないので、タスクの切り換え(ディスパッチ)も起きません。ディスパッチが必要になると、それはタスク独立部を抜けるまで遅らされます。これを遅延ディスパッチ(delayed dispatching)の原則と呼びます。このようにすることにより、タスクの処理よりも割り込みの処理を優先して実行することができます。

図4

【図4:遅延ディスパッチ】

今度は割り込みがネストして発生する場合を考えてみましょう。例えば、図5は、タスクAの実行中に割り込みXが発生し、その割り込みハンドラ中でさらに高優先度の割り込みYが発生した状態を示しています。この場合、割り込みハンドラBのtk_wup_tsk発行直後にディスパッチを起こすと、以降の割り込みハンドラの処理がタスクの処理よりも後回しになります。また、仮に(1)の割り込みYからのリターン時に即座にディスパッチを起こしてタスクBを起動すると、割り込みXの(2)~(3)の部分の実行がタスクBよりも後回しになり、タスクAが実行状態になった時にはじめて(2)~(3)が実行されることになります。これでは、低優先度の割り込みXのハンドラが、高優先度の割り込みばかりではなく、それによって起動されたタスクBにもプリエンプトされる危険を持つことになります。したがって、割り込みハンドラがタスクに優先して実行されるという保証がなくなり、割り込みハンドラが書けなくなってしまいます。これを避けるため、割り込みがネストした場合は、全ての割り込みハンドラの処理が完了してから、実行可能なタスクの実行が再開されます。

図5

【図5:割り込みのネストと遅延ディスパッチ】

上記の内容を整理すると、割り込みハンドラの処理には以下のような制限がつきます。

  • ●暗黙に自タスクを指定するシステムコールは発行できません。
  • ●自タスクを待ち状態にするシステムコールは発行できません。
  • ●遅延ディスパッチが適用され、割り込みハンドラの処理が終了するまでタスクディスパッチは発生しません。

このような制約があることに加えて、一般に割り込みハンドラの実行中は同じ(あるいはより低い)優先度を持つ他の割り込みも禁止されるため、基本的に割り込みハンドラでは複雑な処理はしません。複雑な処理が必要な場合は、タスクを利用してそこで必要な処理を行うようにプログラムを設計する必要があります。

割り込みを用いたプッシュスイッチ押下の検出

それでは、割り込みを用いたサンプルプログラムを見ていきましょう。このサンプルプログラムでは、T-Kernel 2.0の機能を利用して、T-Engineリファレンスボードのプッシュスイッチの押下を割り込みで検出します。

プッシュスイッチはSW1, SW2, SW3, SW4の合計4種類で、それぞれ異なる割り込みを発生させます。サンプルプログラムでは、プッシュスイッチが押されたらどれが押されたかコンソールに出力します。この「コンソールに文字を出力する」処理は、出力が完了するまでに多くの時間を要します。割り込みハンドラの中でこのような長い処理を実行すると、その間は他の割り込みが禁止され、タスクのディスパッチも遅延しますので、そのようなプログラム設計は適切ではありません。そこで今回は、コンソールに文字を出力するタスクを割り込みハンドラとは別に生成し、割り込みハンドラではイベントフラグを使ってそのタスクにプッシュスイッチの押下を通知します。

まずは割り込みハンドラを定義します。SW1の割り込みハンドラのコード例は以下の通りです。

 									   /* 各プッシュボタンに対応するフラグパターンの定義 */
									    #define FLGPTN_SW1 (0x01)
 									   #define FLGPTN_SW2 (0x02)
									    #define FLGPTN_SW3 (0x04)
 									   #define FLGPTN_SW4 (0x08)
									    #define FLGPTN_ALL (FLGPTN_SW1 | FLGPTN_SW2 | FLGPTN_SW3 | FLGPTN_SW4)
									    /* プッシュボタンの数 */
									    #define NUM_SW     (4)
    
									    /*
									     * 各プッシュボタンに対応する割り込みハンドラ
									     */
									    LOCAL void inthdr_sw1(UINT dintno)
									    {
									            tk_set_flg(flgid, FLGPTN_SW1);
									            ClearInt(dintno);
									    }
									

この割り込みハンドラは、押下されたプッシュボタンに対応するフラグパターンをイベントフラグに設定し、ClearIntで割り込み発生をクリアします。SW2,SW3, SW4 にもinthdr_sw1と同様の関数を定義します。

ところで、inthdr_sw1では最後にClearIntで割り込み発生をクリアしていますが、ClearIntで割り込み発生をクリアしない場合はどのようになるでしょうか?割り込みコントローラは、割り込みが発生している状態を保ち続けるので、割り込みハンドラから戻ろうとしても、再度同じ割り込みハンドラが呼ばれ続けます。その結果、タスクが呼び出されることはなく、コンソールには何も表示されません。なお、T-EngineリファレンスボードではClearIntを呼ぶ必要がありますが、システムによってはClearIntを呼ぶ必要がないこともあります。ハードウェア仕様書や実装仕様書の内容を確認して、システムに合うように記述してください。

続いて、tk_def_intを用いて割り込みハンドラを定義します。

									    /* 割り込みハンドラを登録する関数 */
									    LOCAL void define_inthdr(UINT dintno, FP inthdr)
									    {
									            T_DINT dint;
    
									            dint.intatr = TA_HLNG;
									            dint.inthdr = inthdr;
									            tk_def_int(dintno, &dint);
									    }
    
									    EXPORT INT usermain(void)
									    {
									            ....
    
									            /* 各プッシュボタンの割り込みハンドラの登録 */
									            define_inthdr(DINTNO(IV_GPIO(8)), inthdr_sw1);
									            define_inthdr(DINTNO(IV_GPIO(7)), inthdr_sw2);
									            define_inthdr(DINTNO(IV_GPIO(6)), inthdr_sw3);
									            define_inthdr(DINTNO(IV_GPIO(4)), inthdr_sw4);
    
									            ....
									    }
									

IV_GPIOは、GPIO割り込みの割り込みベクタを求めるT-Engineリファレンスボード固有のマクロです。SW1, SW2, SW3, SW4はそれぞれGPIO8, GPIO7, GPIO6, GPIO4に対応しています。また、割り込みハンドラはC言語(高級言語)で記述されているので、tk_def_intにTA_HLNG属性を指定します。

それから、割り込みに関する設定を行います。

									    /* 割り込みを許可する関数 */
									    LOCAL void enable_int(INTVEC intvec)
									    {
									            SetIntMode(intvec, IM_ENA | IM_EDGE | IM_HI);
									            ClearInt(intvec);
									            EnableInt(intvec);
									    }
    
									    EXPORT INT usermain(void)
									    {
									            ....
    
									            /* 各プッシュボタンの割り込みの許可 */
									            enable_int(IV_GPIO(8));
									            enable_int(IV_GPIO(7));
									            enable_int(IV_GPIO(6));
									            enable_int(IV_GPIO(4));
    
									            ....
									    }
									

まず、SetIntModeで割り込みモードを設定します。SetIntModeで設定可能な項目は実装依存です。T-Engineリファレンスボードでは以下の項目を指定することができます。

									    #define IM_ENA          0x0001  /* 割込線有効 */
									    #define IM_DIS          0x0000  /* 割込線無効 */
									    #define IM_INV          0x0002  /* 極性反転  */
									    #define IM_LEVEL        0x0200  /* レベル   */
									    #define IM_EDGE         0x0000  /* エッジ   */
									    #define IM_HI           0x0000  /* ハイレベル/立ち上がりエッジ */
									    #define IM_LOW          0x0100  /* ローレベル/立ち下がりエッジ */
									    #define IM_BOTH         0x0400  /* 両エッジ  */
									    #define IM_ASYN         0x0800  /* 非同期   */
									

プッシュスイッチの論理は、押し下げると1に、離すと0になるので、立ち上がりエッジを検出できるように割り込みモードを設定します。立ち上がりエッジに対応する項目は、IM_EDGE(エッジ割り込み)とIM_HI(立ち上がりエッジ)の論理和です。それからClearIntで割り込み発生をクリアして、最後にEnableIntで割り込みを許可します。

割り込みベクタテーブル

これまでは、T-Kernelの割り込み管理機能の使い方を中心に、サンプルプログラムを交えて説明してきました。ここでは、割り込みハンドラがシステムの中でどのように管理されているか説明します。

割り込みベクタテーブルとは、割り込みの要因ごとに割り込みハンドラに関する情報を登録するテーブルです。割り込みベクタテーブルの内容は、割り込みハンドラの先頭アドレスや割り込みハンドラへの分岐命令などです。tk_def_int で登録された割り込みハンドラは、この割り込みベクタテーブルに設定されます。

T-Engineリファレンスボードでは以下のようなベクタテーブルを実装しています。

図6

【図6:ベクタテーブル】

割り込みハンドラが呼ばれるまで

それでは、tk_def_intで登録された割り込みハンドラが実際に呼ばれるまでの流れを見てみましょう。ここでは、T-Engineリファレンスボード(tef_em1d)上でのT-Kernel 2.0の動作を説明します。

T-EngineリファレンスボートにはARM11が搭載されています。ARM11の割り込み処理では、各割り込み要因に対応する割り込みハンドラへの分岐をソフトウェアが行います。

  • ①周辺機器から割り込み信号が入力されると、割り込みコントローラがCPUに割り込み要求を通知します。
  • ②割り込み要求がCPUに通知されると、CPUの例外ベクタから割り込みエントリルーチンへ分岐します。
  • ③割り込みエントリルーチンは、割り込みコントローラの状態から割り込み要因を特定し、割り込みベクタテーブルを用いて対応する割り込みハンドラへ分岐します。
  • ④割り込みハンドラが高級言語で記述されている場合、いったん高級言語対応ルーチンを経由してC言語の関数呼び出しのレジスタ規則に合わせてから、C言語で書かれた割り込みハンドラが呼び出されます。
  • ⑤C言語で記述された割り込みハンドラがreturn文で終了すると、高級言語対応ルーチンに復帰して、その中のtk_ret_intが呼び出されます。これによって割り込みハンドラから復帰するとともに、遅延されていたディスパッチが処理されます。

上記手順の中で①、②はハードウェアで、③、④、⑤はソフトウェアで処理されます。

図7

【図7:割り込み処理の流れ】

なお、ARMの例外処理の詳細については、「Cortex-M編 第13回」が参考になります。

割り込みハンドラ番号と割り込みベクタ

最後に、T-Kernelの割り込み管理機能における割り込みの指定方法に関する説明をします。

割り込みの指定方法は、システムコールとそれ以外で異なります。システムコールでは割り込みハンドラ番号を用いて割り込みを指定し、ライブラリ関数やマクロでは割り込みベクタを用いて割り込みを指定します。

システムコール(tk_def_int)は割り込みハンドラ番号を用いて割り込みを指定し、1つの割り込みハンドラ番号に対して1つの割り込みハンドラを定義できます。割り込みハンドラ番号は複数の割り込みハンドラを区別するための番号です。その具体的な意味は実装ごとに定義されます。一般には、CPUハードウェアの割り込み処理で定義される割り込みベクタをそのまま使うか、割り込みベクタとの対応付けが可能な何らかの番号を使います。割り込みベクタから割り込みハンドラ番号を得るには、DINTNOマクロを利用します。

ただし、T-Engineリファレンスボードでは、割り込みハンドラ番号として割り込みベクタをそのまま用います。そのため、DINTNOマクロは以下のように定義されており、そのままの値を返すようになっています。

									    #define DINTNO(intvec)  (intvec)  /* convert to interrupt definition number */
									

まとめ

応答性の高いリアルタイムシステムを構築するためには、割り込みによる処理は避けては通れません。しかし、一般的に割り込みに関する機能はハードウェアに強く依存するために実装が難しく、有効に活用するにはハードウェアとソフトウェアの両方に関する知識が必要です。

T-Kernelを利用することで、ユーザーが割り込み処理の実装にかける手間を減らすことができます。実際、サンプルプログラムのように、割り込みハンドラ番号や割り込みモードなどの一部を除けば、T-Kernel APIと標準的なC言語で割り込み処理を記述することが可能です。そのため、移植性の高いソフトウェアを開発することができます。

また、タスクと割り込みを複数使用する場合は、処理の流れが複雑になりがちです。しかし、T-Kernelの割り込みハンドラの処理に関する適切な制約と遅延ディスパッチの原則により、処理の優先度が明確になり、ユーザーが期待した処理を実現しやすくなります。

ぜひ、T-Kernelの機能を有効に活用してリアルタイム性の高いシステムを構築してください。