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

割り込み

ここまでで、MB2 上のさまざまなハードウェアに触れてきました。ボタンを読み取り、タイマーを待ち、シリアル通信を行い、I2C を使ってデバイスとやり取りしてきました。これらはいずれも、1 つ以上のペリフェラルの準備が整うまで待つ必要がありました。これまでは、その待機を「ポーリング」で行っていました。つまり、完了したかどうかをペリフェラルに繰り返し問い合わせ、完了するまでそれを続ける方法です。

このマイクロコントローラには CPU コアが 1 つしかないので、待っている間はほかのことができません。さらに、CPU コアがペリフェラルを継続的にポーリングすると電力を無駄にしますし、多くのアプリケーションではそれは許容できません。もっと良い方法はあるでしょうか?

幸い、それは可能です! この小さなマイクロコントローラは処理を並列には実行できませんが、実行中に異なるタスクを簡単に切り替え、外界からのイベントに応答できます。この切り替えは「割り込み」と呼ばれる機能で実現されます。

割り込みという名前はまさにその通りで、ペリフェラルがコアのプログラム実行を実際にいつでも中断できるようにします。MB2 の nRF52833 では、ペリフェラルはコアの Nested Vectored Interrupt Controller(NVIC)に接続されています。NVIC は CPU をその場で停止させ、別の処理を行うよう指示し、それが終われば割り込まれる前にしていた作業へ CPU を戻せます。割り込みコントローラの Nested と Vectored の部分については後で扱います。まずは、コアがどのようにタスクを切り替えるかに注目しましょう。

割り込みの処理

NRF52833 が使う計算モデルは、ほとんどすべての現代的な CPU で使われているものです。CPU の内部には、「CPU レジスタ」と呼ばれる作業用の記憶領域があります。(やや紛らわしいですが、この CPU レジスタは、前の Registers 章で説明した「デバイスレジスタ」とは別物です。)計算を実行するために、CPU は通常、メモリから CPU レジスタへ値を読み込み、レジスタ内の値を使って計算を行い、その結果をメモリへ書き戻します。(これは「ロードストアアーキテクチャ」として知られています。)

CPU が現在実行している計算に関する情報はすべて、CPU レジスタに保存されています。コアがタスクを切り替えるなら、新しいタスクがレジスタを自分の作業領域として使えるように、CPU レジスタの内容をどこかに保存しなければなりません。新しいタスクが完了すると、CPU はレジスタの値を復元して元の計算を再開できます。実際、それこそがコアが割り込み要求に応答して最初に行うことです。すぐに現在の処理を停止し、CPU レジスタの内容をスタックに保存します。

次の段階では、割り込みに応答して実行すべきコードへ実際にジャンプします。Interrupt Service Routine(ISR)は、しばしば割り込み「ハンドラ」とも呼ばれ、割り込みに応答してコアから呼び出される、アプリケーションコード内の特別な関数です。メモリ上の「割り込みテーブル」には、起こり得るすべての割り込みに対応する「割り込みベクタ」が含まれています。割り込みベクタは、特定の割り込みを受信したときにどの ISR を呼び出すかを示します。ISR のベクタリングの詳細は NVIC and Interrupt Priority セクションで説明します。

ISR 関数は、特別な割り込み復帰(return-from-interrupt)機械命令を使って「リターン」します。この命令により、CPU は CPU レジスタを復元し、ISR が呼び出される前の位置へジャンプして戻ります。

MB2 をつつく

ISR を定義し、Button A が押されたときに MB2 を「つつく」割り込みを設定してみましょう (examples/poke.rs)。ボードは「ouch」と言ってパニックを起こします。

#![no_main]
#![no_std]

use cortex_m::asm;
use cortex_m_rt::entry;
use panic_rtt_target as _;
use rtt_target::{rprintln, rtt_init_print};

use microbit::{
    hal::{
        gpiote,
        pac::{self, interrupt},
    },
    Board,
};

/// This "function" will be called when an interrupt is received. For now, just
/// report and panic.
#[interrupt]
fn GPIOTE() {
    rprintln!("ouch");
    panic!();
}

#[entry]
fn main() -> ! {
    rtt_init_print!();
    let board = Board::take().unwrap();
    let button_a = board.buttons.button_a.into_floating_input();

    // Set up the GPIOTE to generate an interrupt when Button A is pressed (GPIO
    // wire goes low).
    let gpiote = gpiote::Gpiote::new(board.GPIOTE);
    let channel = gpiote.channel0();
    channel
        .input_pin(&button_a.degrade())
        .hi_to_lo()
        .enable_interrupt();
    channel.reset_events();

    // Set up the NVIC to handle GPIO interrupts.
    unsafe { pac::NVIC::unmask(pac::Interrupt::GPIOTE) };
    pac::NVIC::unpend(pac::Interrupt::GPIOTE);

    loop {
        // "wait for interrupt": CPU goes to sleep until an interrupt.
        asm::wfi();
    }
}

ISR ハンドラ関数は「特別」です。ここでは名前を GPIOTE にする必要があり、これは この ISR が割り込みテーブル内の GPIOTE 割り込み用エントリに格納されるべきことを示しています。

#[interrupt] デコレーションは、コンパイル時に関数を ISR として特別扱いする印を付けるために 使われます。(これは「proc macro」です。必要なら Rust book で詳しく読めます。)

要するに、「proc macro」はソースコードを別のソースコードへ変換します。特定のマクロの使用がどのようなコードに変換されるのか気になるなら、 そのマクロ呼び出しを展開できます。Rust Playground の Tools か、IDE の「rust-analyzer: Expand macro」コマンドを使えばできます。

#[interrupt] を関数に付けると、その関数にはいくつかの特別な性質が生じます。

  • コンパイラは、その関数が引数を取らず、戻り値も返さないこと(あるいは決して戻らないこと)を検査します。CPU には ISR に渡す 引数がなく、ISR からの戻り値を置く場所もありません。これは、割り込みハンドラが独自のコールスタックを持つからです(少なくとも 概念上は、実際には常にそうとは限りません)。

  • この関数を指すベクタ(つまり関数ポインタ)が、その関数名に対応する割り込みテーブル内の位置に 配置されます。

  • コンパイラは、通常のコードから ISR を直接呼び出すことを防ぎます。

割り込みを設定するには 2 つの手順があります。まず、Button A につながったピンの電圧が high から low に変化したときに割り込みを生成するよう、 GPIOTE を設定しなければなりません。次に、その割り込みを許可するよう NVIC を設定する必要があります。順序は少し重要で、「間違った」順番で行うと、 処理する準備ができる前に割り込みが発生することがあります。

ほとんどのマイクロコントローラと同様に、GPIOTE がいつ割り込みを生成するかには大きな柔軟性があります。割り込みは、ピンが low から high に遷移したとき、high から low に遷移したとき(ここで使っているもの)、任意の変化(「エッジ」)、low のとき、または high のときに生成できます。nRF52833 では、割り込みはイベントを生成し、そのイベントは同じ割り込みに対して ISR が 2 回目に呼び出されないよう、ISR 内で手動でクリアしなければなりません。ほかのマイクロコントローラでは少し動作が異なることがあります。別のボードでの詳細を理解するには、Rust crate とマイクロコントローラのドキュメントを読むべきです。

A ボタンを押すと、「ouch」というメッセージが表示され、その後パニックになります。なぜ割り込み ハンドラは panic!() を呼ぶのでしょうか? panic!() の呼び出しをコメントアウトして、ボタンを押したときに何が起こるか見てみてください。画面には 「ouch」というメッセージが流れ続けるはずです。NVIC は割り込みが発行されたことを記録します。その「イベント」は、実行中のプログラムが明示的にクリアするまで保持されます。panic!() がないと、 割り込みハンドラが戻ったときに NVIC は(この場合)割り込みを再び有効化し、まだ保留中の割り込みイベントがあることに気付いて、ハンドラを再度実行します。これは 永遠に続きます。割り込みハンドラが戻るたびに、また呼び出されます。すぐに見るように、割り込みの通知は割り込みハンドラの内側から reset_event() ペリフェラルメソッドを使ってクリアできます。

I2C の準備ができたとき、タイマーが期限に達したときなど、さまざまな割り込み要因に対して ISR を定義できます。ISR の中ではほぼ何でもできますが、 割り込みハンドラは短く素早く保つのがよい実践です。

通常、ISR が完了すると、メインプログラムは割り込みが起きなかったかのようにそのまま実行を続けます。しかし、ここには少し問題があります。ISR が実行されて何らかの処理を行ったことを、アプリケーションはどうやって知ればよいのでしょうか。ISR には入力引数も結果もないので、ISR のコードはどのようにアプリケーションコードとやり取りできるのでしょうか?