Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

NVIC と割り込み優先度

これまで見てきたように、割り込みによってプロセッサはコード内の別の関数へ即座にジャンプできます。では、これを可能にしている背後では何が起きているのでしょうか? この節では、本書の残りを読むうえでは必須ではない技術的な詳細をいくつか扱いますので、興味がなければ先へ進んでも構いません。

割り込みコントローラ

割り込みによって、プロセッサは GPIO 入力ピンの状態変化、タイマーの周期完了、UART が新しいバイトを受信したことなどのペリフェラルイベントに応答できます。ペリフェラルには、そのイベントを検知して専用の割り込み処理用ペリフェラルに通知する回路が含まれています。Arm プロセッサでは、この割り込み処理用ペリフェラルは NVIC — Nested Vector Interrupt Controller と呼ばれます。

注記 RISC-V のような他のマイクロコントローラアーキテクチャでは、ここで説明する名前や詳細は異なりますが、根底にある原理は一般的によく似ています。

NVIC は、多くのペリフェラルから割り込み発生要求を受け取れます。1 つのペリフェラルに複数の割り込み候補があることも一般的で、たとえば GPIO ポートが各ピンごとの割り込みを持っていたり、UART が「データ受信」と「データ送信完了」の両方の割り込みを持っていたりします。NVIC の役割は、これらの割り込みに優先順位を付け、どの割り込みがまだ処理待ちかを記憶し、そのうえで関連する割り込みハンドラコードをプロセッサに実行させることです。

割り込み優先度

NVIC には、各割り込みに対して設定可能な「優先度」があります。設定によっては、NVIC は新しい割り込みを実行する前に現在の割り込みが完全に処理されることを保証できますし、より高い優先度の別の割り込みを処理するために、ある割り込みの途中でプロセッサを「プリエンプト」することもできます。

プリエンプションにより、プロセッサは重大なイベントに非常に素早く応答できます。たとえば、ロボットコントローラでは、低優先度の割り込みを使ってオペレータへのステータス情報送信を管理しつつ、センサーが差し迫った衝突を検知したときには高優先度の割り込みを受けて、モーターを即座に停止できるようにするかもしれません。停止する前にデータパケットの送信が終わるまでロボットが待つようでは困ります!

同一優先度またはそれより低い優先度の割り込みが ISR の実行中に発生した場合、その割り込みは「保留」されます。つまり、NVIC はその新しい割り込みを記憶し、現在の ISR が完了した後のどこかの時点でその ISR を実行します。ISR 関数が戻ると、NVIC は ISR の実行中に処理が必要な他の割り込みが発生していないかを確認します。もしあれば、NVIC は割り込みテーブルを確認し、そこにベクタリングされている最優先の ISR を呼び出します。そうでなければ、CPU は実行中だったプログラムに戻ります。

割り込みが完全に無効化されている場合、入ってくるすべての割り込みは保留されることに注意してください。保留中の割り込みは、割り込みが再び有効になった時点で処理されます。

組み込み Rust では、cortex-m crate を使って NVIC をプログラムできます。この crate は、割り込みの有効化と無効化(unmaskmask と呼ばれます)、割り込み優先度の設定、ソフトウェアからの割り込み発生を行うメソッドを提供します。RTIC のようなフレームワークは NVIC の設定を代行でき、NVIC の柔軟性を活用して、便利なリソース共有やタスク管理を提供します。

NVIC についての詳細は Arm’s documentation で読むことができます。

ベクタテーブル

NVIC を説明する際に、「関連する割り込みハンドラコードをプロセッサに実行させる」と述べました。しかし、実際にはそれはどのように動作するのでしょうか?

まず、各割り込みに対してどのコードを実行すべきかをプロセッサが知るための仕組みが必要です。Cortex-M プロセッサでは、これはベクタテーブルと呼ばれるメモリ領域に関係します。これは通常、私たちのコードを含むフラッシュメモリの先頭に配置されており、そのフラッシュメモリは新しいコードをプロセッサに書き込むたびに再プログラムされます。そしてそこには、すべての割り込み関数のアドレス、つまりメモリ上の位置の一覧が含まれています。メモリ先頭部分の具体的なレイアウトは Architecture Reference Manual で Arm によって定義されています。ここでは、64 バイト目から 256 バイト目までに、私たちが使用する nRF プロセッサの 48 個すべての割り込みハンドラのアドレスが、1 アドレスあたり 4 バイトで格納されていることが重要です。各割り込みには 0 から 47 までの番号があります。たとえば、TIMER0 は割り込み番号 8 なので、96 バイト目から 100 バイト目にはその割り込みハンドラの 4 バイトアドレスが含まれています。NVIC がプロセッサに割り込み番号 8 を処理するよう指示すると、CPU はそのバイト列に格納されたアドレスを読み取り、そこへ実行をジャンプします。

このベクタテーブルは、私たちのコードではどのように生成されるのでしょうか? 私たちは cortex-m-rt crate を使用しており、これがその処理を担ってくれます。この crate は、使用されていない各位置に対してデフォルトの割り込みを提供し(すべての位置が埋まっていなければならないため)、独自の割り込みハンドラを指定したい場合には、このデフォルトをコード側で上書きできるようにしています。これには #[interrupt] マクロを使います。このマクロでは、関数に、その関数が処理する割り込みに関連した特定の名前を付ける必要があります。その後、cortex-m-rt crate がそのリンカスクリプトを使って、その関数のアドレスがメモリ内の正しい場所に配置されるようにします。

Rust におけるこれらの割り込みハンドラの管理方法についてさらに詳しく知りたい場合は、Embedded Rust Book の Exceptions 章および Interrupts 章を参照してください。