割り込み優先度の設定とネスティング

「通信データをたまに取りこぼす」「先に処理してほしい割り込み(IRQ)が後回しになる」。こうした不可解な症状の原因としてよくあるのが、割り込み優先度の設定ミスです。本記事では、一般的な32ビットマイコンの割り込みコントローラであるNVIC(Nested Vectored Interrupt Controller)を題材に、優先度の基本ルールから、プリエンプト優先度とサブ優先度の違い、CMSISでの設定手順、RTOS併用時の注意点までを解説します。

割り込み優先度の基本ルール

Cortex-Mの割り込み優先度には、最初に覚えるべきルールが3つあります。

  • 数値が小さいほど優先度が高いとする(0を最高優先度とする)。
  • 実行中の割り込みより高い優先度の割り込みが発生すると、処理を中断してプリエンプト(Preempt)する。これを割り込みのネスティング(多重割り込み)とする。
  • 同じ優先度、または低い優先度の割り込みは保留(ペンディング)し、現在の処理が終わってから実行する。

特に1つ目は誤設定が起きやすく、注意が必要な点です。多くの人の直感や、他のアーキテクチャの常識とは逆向きのため、「優先度を上げたつもりが下げていた」というミスが起こりがちです。また3つ目から導かれる重要な帰結として、同一優先度の割り込み同士は決して互いにネスティングしません。「UART受信中にタイマ割り込みで処理したい」のであれば、両者に異なるプリエンプト優先度を与える必要があります。

※同一優先度の割り込みが複数ペンディングした場合は、例外番号(Exception Number)の小さい例外が優先されます(IRQ0は例外番号16、IRQ1は例外番号17に相当します)。

優先度レベルの段数は「実装ビット数」で決まる

Cortex-Mのアーキテクチャ上、優先度は8ビット(0〜255)のフィールドとして定義されていますが、実際に使えるビット数はマイコンベンダの実装によって異なります。多くのCortex-M3/M4/M7搭載マイコンでは上位3〜4ビットのみが実装されており、4ビット実装なら16段階、3ビット実装なら8段階です。Cortex-M0/M0+では一般に2ビット(4段階)ですが、実装ビット数はマイコンベンダに依存するため、必ずデータシートで確認してください。実装ビット数は、デバイスヘッダの __NVIC_PRIO_BITS を見るか、優先度レジスタに 0xFF を書いて読み戻し、有効なビットを調べる方法でも確認できます。

有効ビットは上位側に配置されるため、下位ビットにだけ値を書いても有効な優先度としては反映されない(結果として意図せず0相当になることがある)。そのためCMSISの NVIC_SetPriority() を使う。CMSISの標準関数 NVIC_SetPriority() は、デバイスヘッダに定義された実装ビット数(__NVIC_PRIO_BITS)を考慮して自動的にシフトしてくれるため、レジスタ直書きではなくCMSIS関数を使うのが安全です。

プリエンプト優先度とサブ優先度の違い

優先度フィールドは、上位側の「プリエンプト優先度(グループ優先度)」と、下位側の「サブ優先度」の2階層に分割できます。両者の役割は明確に異なります。

  • プリエンプト優先度:実行中の割り込みをプリエンプション(横取り)し、ネスティング(多重割り込み)を発生させられるかどうかを決める。
  • サブ優先度:複数の割り込みが同時に保留されているとき、どれから実行するかの順番だけを決める。ネスティングの可否には一切影響しない。

分割位置は、AIRCRレジスタのPRIGROUPフィールド(CMSISでは NVIC_SetPriorityGrouping())で設定します。「サブ優先度を細かく分けたのにネスティングしない」という相談はよくある設定ミスで、原因はこの役割の取り違えです。迷ったら全ビットをプリエンプト優先度に割り当てる設定(サブ優先度なし)にしておくと、挙動が直感と一致しやすくなります。

なお、Cortex-M0/M0+では優先度グルーピング(プリエンプト優先度とサブ優先度への分割)の仕組みが利用できないため、優先度は実質すべてプリエンプト優先度として扱われます。

CMSISでの設定手順

標準提供されるライブラリ関数を使った基本的な設定の流れは次のとおりです。優先度を設定してから割り込みを許可する、という順番も含めて定石として覚えておくことをお勧めします。

/* 1. 優先度グループ設定の例:サブ優先度を使わない(=プリエンプト優先度を最大化する)ように設定する。設定値は __NVIC_PRIO_BITS に依存するため、ベンダの推奨値(HALのPriorityGroup定義など)に従う */
NVIC_SetPriorityGrouping(0U);
/* 優先度値をNVIC_EncodePriority()で組み立てる例(グループ設定, プリエンプト優先度, サブ優先度) */
/* uint32_t priority = NVIC_EncodePriority(0U, 1U, 0U); */
/* 2. 各割り込みの優先度を設定(小さいほど高優先度) */
NVIC_SetPriority(UART_IRQn, 1U);   /* 通信:高め      */
NVIC_SetPriority(TIMER_IRQn, 3U);   /* 周期処理:低め  */
/* 3. 優先度を決めてから割り込みを許可する */
NVIC_EnableIRQ(UART_IRQn);
NVIC_EnableIRQ(TIMER_IRQn);

ベンダ提供のHAL関数も、内部ではこれらのCMSIS関数を呼んでいます。HALを使う場合も「数値が小さいほど高優先」「プリエンプト優先度とサブ優先度は別物」というルールはそのまま当てはまります。

ネスティング時の動きとスタック消費

優先度レベル1(高優先度)のUART割り込みを処理中に、優先度レベル3(低優先度)のタイマ割り込みが発生したケースを考えます。タイマ側は優先度が低いためペンディングされ、UARTの処理完了後に実行されます。このとき、いったんメイン処理へ戻らず連続して次の割り込みへ移る「テールチェイン」という仕組みが働き、レジスタ退避・復帰のオーバーヘッドが省略されます。逆にタイマ処理中にUART割り込みが発生した場合は、即座にプリエンプトしてUARTを処理し、終了後にタイマ処理へ復帰します。

見落としがちなのがスタック消費です。ネスティングが1段深くなるごとに、退避レジスタとハンドラのローカル変数がスタックに積まれます。スタックサイズは「最悪ケースのネスティング段数」を想定して設計する必要があります。優先度の段数をむやみに増やすと、設計次第では同時にネスティングし得る割り込みの組み合わせが増え、最悪ケースのスタック見積もりが厳しくなる可能性があります。

また、FPUを使う構成では例外エントリ時の退避量が増える場合があります。スタックサイズは最悪ケースで見積もってください。

RTOS併用時の鉄則

RTOSをCortex-Mで使う場合、割り込み優先度には追加のルールが課されます。割り込みハンドラから呼ぶ専用API(多くのRTOSがISR専用として用意している「FromISR」系のAPI)を使う割り込みは、その優先度を、RTOSが定めるシステムコール割り込み優先度の上限値(代表的なオープンソースRTOSでは、この上限値を指定する設定マクロが用意されています)と同じか、それより低い優先度(数値としては同じか大きい値)に設定する必要があります。

カーネルはクリティカルセクションでBASEPRIレジスタ(Cortex-M3/M4/M7に搭載。Cortex-M0/M0+にはBASEPRIがなく、PRIMASKで全割り込みを一律にマスクします)を使い、上限値と数値が同じか、それより大きい値(優先度が低い)の割り込みをマスクします。数値が小さい高優先度の割り込みはマスクされません。整理すると、RTOSのAPI(FromISR系を含む)を呼ぶISRは、上限値以下の優先度(数値としては同じか大きい値)に置きます。上限値より高い優先度(数値が小さい値)のISRは、カーネルにマスクされないかわりにRTOSのAPI呼び出しが禁止されます。このルールを破ると、カーネルのデータ構造が壊れて「ごくまれにクラッシュする」最悪のタイプのバグになります。RTOSが備える表明検査(configASSERT() などの名前で提供されます)を有効にしておくと違反を検出できるので、開発中は必ず有効化しておくことが大切です。

なお、移植層によっては、ライブラリ表記用の別のマクロを用いて、ライブラリ表記の優先度とレジスタに書き込む値を分ける流儀があります。いずれの場合も「数値が小さいほど高優先度」が前提で、APIを呼ぶISRは数値が同じか大きい側に置きます。

ありがちなトラブルのチェックリスト

  • 全割り込みをデフォルト優先度0のまま使っている → すべての割り込みが最高優先度で同列となり、意図したネスティングが一切起きない(確認:起動時に全割り込みの優先度設定値を一覧でダンプする)。
  • 「数値が大きい=高優先」と勘違いして設定している(確認:優先度の設定値と意図した高低を並べた表にして突き合わせる)。
  • レジスタ直書きで下位ビットに値を書き、切り捨てられている → CMSIS関数を使う(確認:__NVIC_PRIO_BITS の値と、優先度レジスタから読み戻した値を比べる)。
  • 高優先度の割り込みハンドラ内で、SysTickに依存する待ち処理(マイコンベンダ提供の待機関数など)を呼んでいる → SysTickがプリエンプトできず無限待ちになる(確認:割り込みハンドラから呼ぶ関数に待ち処理が含まれていないか洗い出す)。
  • RTOSのFromISR系APIを、上限値より高優先度の割り込みから呼んでいる(確認:ISRの優先度と上限値を並べた表を作る)。

まとめ

割り込み優先度は「数値が小さいほど高い」「ネスティングを決めるのはプリエンプト優先度だけ」「実装ビット数を確認する」の3点を押さえれば、トラブルの大半は防げます。設計時には、システム内の全割り込みを一覧化し、プリエンプト関係を表として明文化しておくことが推奨されます。

組み込みソフトの世界 トップへ戻る