割り込みとISR
「なぜか反応が遅い」「たまに変数が壊れる」の正体がわかる。割り込みベクタからISR、優先度とネスト、レイテンシ、前半後半の分離、割り込み禁止までを原理から押さえ、確実に動く割り込み設計の勘所が身につく。
- 1.割り込みは外部イベントを合図に、CPUが実行中の処理を中断してベクタテーブル経由でISRへ分岐する仕組み。ポーリングと違い、待たずに即応でき、CPU時間を無駄にしない。
- 2.ISRは短く保つのが鉄則。時間のかかる処理は「後半処理」へ回し、割り込み応答(レイテンシ)とネスト時の優先度逆転を抑える。
- 3.ISRとメイン処理で共有する変数はクリティカルセクション(割り込み禁止 or アトミック操作)で守らないと、更新の途中で割り込まれてデータが壊れる。
「待つ」のではなく「呼ばれる」
センサーの値が変わった、タイマが満了した、通信が1バイト届いた——こうした外部イベントを検知する素朴な方法は、フラグを繰り返し読みに行くポーリングです。しかしポーリングはCPUを回し続けて電力を食い、しかも読みに行った瞬間しか反応できません。割り込み(interrupt) は発想が逆で、イベントが起きたらハードウェアがCPUに割り込み信号を送り、CPUが実行中の処理を中断して専用の処理ルーチンへ飛びます。CPUは普段は別の仕事(あるいは低電力の待機)に専念でき、必要な瞬間だけ「呼ばれる」わけです。
この呼び出しを受け取るのが ISR(Interrupt Service Routine、割り込みサービスルーチン) です。本稿では、割り込みが発生してからISRが動き、元の処理へ戻るまでの内部動作を、レイテンシ・優先度・データ保護の観点から追います。CPUの実行モデルそのものは /os/、電気信号としての割り込み線は /hardware-components/ も参照してください。
割り込みベクタとISRへの分岐
割り込み要求が受理されると、CPUは「今どの割り込みが起きたか」を番号(割り込み番号/例外番号)として得ます。この番号を索引に、割り込みベクタテーブル——各割り込みに対応するISRの先頭アドレスを並べた表——を引き、対応するISRへジャンプします。テーブルはメモリの決まった位置(多くのマイコンではリセット直後は0番地付近、Cortex-MならVTORレジスタで再配置可能)に置かれます。
分岐の前後でCPUが自動的に行うのが コンテキストの退避と復帰 です。ISRから戻ったとき、中断された処理が何事もなかったように続けられなければならないので、少なくともプログラムカウンタとステータスレジスタは保存されます。
[割り込み受理からISR実行までの流れ]
外部イベント → 割り込みコントローラが要求をラッチ
│
▼
現命令の完了を待つ ─── CPUが要求を受理
│
▼
PC・ステータスレジスタ等を自動退避(スタックへ)
│
▼
ベクタテーブル[割り込み番号] を読み、ISR先頭へ分岐
│
▼
ISR本体を実行(必要なら追加レジスタを手動退避)
│
▼
割り込み復帰命令(RETI / BX LR 等)→ 退避内容を復元し元へ戻る
Arm Cortex-Mでは、R0-R3・R12・LR・PC・xPSRの8ワードをハードウェアが自動でスタックに積む(スタッキング)ため、ISRを普通のC関数として書けるのが特徴です。一方、8ビットマイコンではISRの先頭で使うレジスタを手動退避するコードが必要になることもあります。どこまで自動化されるかはアーキテクチャ依存で、ここを取り違えるとレジスタ破壊のバグになります。
割り込み優先度とネスト
複数の割り込み源があるとき、同時期に要求が競合したらどれを先に処理するかを決めるのが 割り込み優先度 です。優先度が高い割り込みは、優先度の低いISRの実行中であっても、それを中断して割り込めます。これが ネスト(多重割り込み、nesting) です。逆に、実行中のISRと同じか低い優先度の要求は、現ISRが終わるまで保留(ペンディング)されます。
| 観点 | ネストを許す(多重割り込み) | ネストを禁じる(逐次処理) |
|---|---|---|
| 高優先度の応答性 | 低優先度ISR実行中でも即応できる | 先行ISRの完了まで待たされる |
| 最悪レイテンシ | 高優先度は短く保たれる | 最長ISRの実行時間に律速される |
| スタック消費 | ネスト段数ぶん深くなりうる | 浅く見積もりやすい |
| 実装の複雑さ | 再入・共有データの考慮が増える | 単純で解析しやすい |
低優先度のISRが共有資源をロックしたまま長く走ると、その資源を待つ高優先度の処理が進めなくなる「優先度逆転」が起きます。割り込み文脈では、ISR内で長時間割り込みを禁止したり重いロックを取ったりすると、より高い優先度の割り込みまで巻き添えで遅延します。ISRを短く保つこと、そしてOS利用時は優先度継承つきのミューテックスを使うことが対策になります。
優先度の設定はハードウェア(Cortex-MのNVIC、各種割り込みコントローラ)が担い、番号が小さいほど高優先度、といった規約はチップごとに異なります。設計時は「この割り込みは何を中断してよいか/何に中断されうるか」を優先度表として明示しておくのが安全です。
割り込みレイテンシ
割り込みレイテンシ とは、割り込み要求が発生してからISRの最初の命令が実行されるまでの遅延です。リアルタイム性の要となる指標で、次の要素の和で決まります。
- 現命令の完了待ち:CPUは通常、実行中の命令を区切りのよいところまで進めてから割り込みを受理します。除算など長い命令があると待ちが伸びます。
- コンテキスト退避の時間:レジスタをスタックに積む固定コスト(Cortex-Mなら数十サイクル程度)。
- より高優先度の割り込み処理:自分より上位の割り込みが先に処理されるぶん、開始が後ろへずれます。
- 割り込み禁止区間:後述のクリティカルセクションで割り込みを止めている間は、要求が来ても受理されません。最長の割り込み禁止区間が最悪レイテンシを直接押し上げる ため、禁止区間は可能な限り短くします。
レイテンシは平均値ではなく最悪値が問題になります。モータ制御やセンサのサンプリングのように「必ず何マイクロ秒以内に応答」が要件なら、最長の割り込み禁止区間・最も重い上位ISR・最長命令を積み上げた合計が締め切りを破らないかを検証します。平均は速くても、まれな最悪ケースで1回落ちれば制御が破綻します。
前半処理と後半処理の分離
ISRの中で通信フレーム全体を解析したり、ログをフラッシュしたりといった重い処理を行うと、その間ずっと同等以下の優先度の割り込みが待たされ、レイテンシが悪化します。そこで定石が 前半/後半(top half / bottom half)の分離 です。
- 前半(ISR本体):割り込み文脈で走る最小限の処理。ハードウェアの割り込み要因フラグをクリアし、届いたデータをバッファへ退避し、「後で処理すべし」という通知だけを立ててすぐ抜けます。
- 後半(遅延処理):割り込みを許可した通常文脈で、時間のかかる本処理を行います。実装はベアメタルなら「メインループがフラグを見て処理」、RTOS利用時は割り込みからタスクを起こす(セマフォ通知やイベントフラグ)形が典型です。Linuxカーネルのソフト割り込み/tasklet/workqueue、あるいは割り込みハンドラを分割するスレッド化割り込みも同じ考え方です。
[UART受信を前半/後半に分ける例]
ISR(前半, 割り込み文脈, 短い):
byte = UART_DATA # ハード要因を読み、フラグを自動/手動クリア
ring_buf_push(byte) # リングバッファへ退避するだけ
notify_task() # 後半へ「データあり」を通知して即 return
受信タスク(後半, 通常文脈, 割り込み許可下):
wait_notify() # 通知を待つ
while ring_buf_has_data():
frame = parse(ring_buf_pop()) # 重いフレーム解析はここで
handle(frame)
この分離により、割り込み禁止に近い状態で走る時間を最小化でき、システム全体の応答性とスループットが両立します。
クリティカルセクションと割り込み禁止
最後が最も事故の多い領域、共有データの保護 です。ISRとメイン処理(あるいは別のISR)が同じ変数を触ると、片方が更新している途中でもう片方に割り込まれ、中途半端な状態を読んでしまう 競合状態(レースコンディション) が生じます。とくに、16ビットマイコンで32ビット変数を更新する場合のように、1つの更新が複数命令に分かれる(非アトミックな)ケースが危険です。
古典的な例が、ISRでインクリメントするカウンタです。counter = counter + 1 は「読み出し→加算→書き戻し」の3手順で、読み出しと書き戻しの間で割り込まれると、増加が失われます。この不可分に実行したい一連の処理を クリティカルセクション と呼び、実行中は割り込みを一時的に禁止して守ります。
[クリティカルセクションで共有カウンタを守る]
save = disable_interrupts() # 現在の割り込み状態を保存し禁止
shared_counter += 1 # ここは割り込まれない(不可分)
restore_interrupts(save) # 元の状態へ復元(無条件に許可しない点が重要)
クリティカルセクションを抜けるとき、割り込みを問答無用で「許可」に戻すと、ネストした外側がまだ禁止したかったのに勝手に許可されてしまい、バグの温床になります。入る前の状態を保存し、抜けるときはその値へ「復元」するのが鉄則です。またvolatile修飾を付け忘れると、コンパイラが共有変数をレジスタにキャッシュしてISRの更新を見落とすため、ISRと共有する変数にはvolatileが必要です(ただしvolatileはアトミック性を保証しない点に注意)。
割り込み禁止は最も強力な保護ですが、その間は全割り込みが止まりレイテンシを悪化させるため、囲む範囲は数命令に絞る のが原則です。単一ワードの読み書きで済むならアトミック命令やCAS(compare-and-swap)を使う、リングバッファのように片方が生産・片方が消費するだけの構造にして禁止区間そのものを無くす、といった設計で禁止時間を削れます。
「ISRを短くする理由」は『割り込みレイテンシと優先度逆転を抑え、他の割り込みの取りこぼしを防ぐため』。「共有変数が壊れる原因」は『非アトミックな更新の途中で割り込まれる競合状態』で、対策は『クリティカルセクション(割り込み禁止の復元/アトミック操作)とvolatile』。前半後半の分離は『重い処理を割り込み文脈から通常文脈へ逃がす手法』と押さえます。
まとめ
割り込みは、ポーリングの浪費を避けて外部イベントへ即応するための、組み込みの背骨です。ベクタテーブルでISRへ分岐し、優先度とネストで緊急度を捌き、レイテンシという時間予算の中で応答する——この一連を「最悪ケースで締め切りを守れるか」という視点で設計するのがリアルタイム制御の要諦です。実務では、ISRは短く、重い処理は後半へ、共有データはクリティカルセクションで、という3原則を守るだけで、原因不明の遅延やデータ破壊の大半は防げます。周期割り込みを使ったタイマ駆動や制御ループの詳細は /dsp-control/ も併せて理解を深めてください。
組込み・IoT Article
割り込みとISRを実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
組み込み
比較で見る軸
難易度: advanced / カテゴリ: 組込み・IoT / タグ数: 5
導入後に効く点
ISRは短く保つのが鉄則。時間のかかる処理は「後半処理」へ回し、割り込み応答(レイテンシ)とネスト時の優先度逆転を抑える。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- 組込み・IoT
- タグ数
- 5
判断チェックリスト
- 自社の用途が「組み込み / 割り込み」に近いか確認する。
- 強みである「割り込みは外部イベントを合図に、CPUが実行中の処理を中断してベクタテーブル経由でISRへ分岐する仕組み。ポーリングと違い、待たずに即応でき、CPU時間を無駄にしない。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。