はじめに
初心者さん割り込みって、“すぐ処理できる便利な場所”ですよね?



初心者の頃はみんなそう思う。でも実際は、一番事故が起きやすい場所なんだ
前回の記事では、割り込みの基本とポーリングとの違いを整理しました。
割り込みの基本についてはこちら


今回はさらに一歩踏み込みます。
割り込みは便利な仕組みですが、
扱いを間違えると、
- 再現しないバグ
- タイミング依存不具合
- 謎のフリーズ
などの原因になります。
この記事では、
実務でよく見る
👉 「割り込み処理でやってはいけない設計」
を整理していきます。
割り込み処理解説シリーズ
本記事は「割り込み処理解説」シリーズの1つです。
こちらのまとめ記事で全体像を整理しています。


割り込み記事シリーズ一覧です。
- 割り込みとは何か?ポーリングとの違いから理解する【組み込み入門】
- 割り込み処理でやってはいけないこと5選|組み込み設計の落とし穴(この記事)
- 割り込みは「処理を書く場所」ではない|組み込み設計の本質
- 割り込み×状態遷移設計|イベント駆動型組み込みの基本パターン
- 割り込み優先度設計の考え方|リアルタイム性を壊さないために
- 割り込みとRTOSなし構成の違いとは?設計思想の本質を整理する
なぜ割り込み処理は危険なのか?
割り込み処理(ISR: Interrupt Service Routine)は、
👉 CPUの通常処理を“強制的に中断”して実行されます。
つまりISRは、
- いつ実行されるか分からない
- 他処理を止める
- スタックを消費する
- 並行実行状態になる
という非常に特殊な環境です。
通常の関数感覚で書くと、
後でかなり危険なことになります。
割り込み発生時にCPUで起きていること
割り込み発生時、
CPU内部では以下のような処理が行われます。


つまりISRは、
👉 「CPUの実行を途中で切り替えている」
ということです。
そのため、
- ISRが長い
- ネストが深い
- スタック不足
などが発生すると、
システム全体へ影響が出ます。
① 割り込み内で重い処理を書く
これは最も多いミスです。
例:
void UART_IRQHandler(void) {
printf("received\n");
heavy_calculation();
}一見問題なさそうに見えます。
しかし実際には非常に危険です。
なぜ危険なのか?
ISR実行中は、
他の処理が止められている可能性があります。
そのためISRが長いと、
- タイマ割り込み遅延
- UART受信取りこぼし
- センサ応答遅延
- リアルタイム性崩壊
などにつながります。
特に組み込みでは、
👉 「ISR実行時間」
がシステム品質へ直結します。
原則
割り込みは、
👉 「処理を書く場所」ではなく、
👉 「イベント通知する場所」
です。
ISRでは、
最低限の処理だけ行うのが基本です。
② ISR内で printf() を使う
デバッグ中、
かなりやりがちです。
void UART_IRQHandler(void) {
printf("IRQ\n");
}しかしこれは、
組み込みではかなり危険です。
なぜ printf は危険なのか?
printf() は見た目以上に重い処理です。
内部では例えば:
- UART送信待ち
- バッファ処理
- ロック処理
- 文字列変換
などを行っています。
さらにライブラリによっては、
内部で割り込みを使うこともあります。
つまり、
👉 「軽そうに見えて、実はかなり重い」
処理です。
実務でよくあること
実際の現場では、
- 「printfを消したら動いた」
- 「デバッグ版だけ不安定」
- 「ログ出力すると壊れる」
などはかなりよくあります。
特にISR内printfは、
真っ先に疑われます。
③ 共有変数を雑に扱う
例えば以下です。
int flag;
void ISR(void) {
flag = 1;
}
int main(void) {
while(1) {
if(flag == 1) {
flag = 0;
process();
}
}
}一見問題なさそうです。
しかし実際には危険です。
なぜ危険なのか?
コンパイラ最適化によって、
👉 flag を毎回読み直さない
可能性があります。
すると、
ISR側で flag = 1 しても、
メインループが永遠に終了しないことがあります。
必須になる知識
割り込みと共有変数では、
- volatile
- クリティカルセクション
- アトミック性
などの理解が必要になります。
volatile の重要性
例えば:
volatile int flag = 0;とすることで、
👉 「毎回メモリから読み直す」
ことをコンパイラへ指示できます。
割り込みと volatile は非常に重要な組み合わせです。
volatileについては、
こちらの記事でも詳しく解説しています。


④ ISR内で malloc() する
これもかなり危険です。
例えば:
void ISR(void) {
char* buf = malloc(128);
}リアルタイム設計では、
基本的に避けます。
なぜ malloc が危険なのか?
mallocは内部で、
- 空き領域探索
- 管理テーブル更新
- メモリ管理処理
などを行います。
つまり:
👉 実行時間が一定ではありません
リアルタイム制御では、
「いつ終わるか分からない処理」は危険です。
さらに、
- フラグメンテーション
- メモリ不足
- ヒープ破壊
などの原因にもなります。
実務ではどうする?
組み込みでは一般的に、
- 静的確保
- 固定長バッファ
- リングバッファ
などを使います。
組み込み開発でmallocが嫌われる理由については、こちらの記事でも解説しています。


⑤ 割り込みネストを考慮しない
割り込みには優先度があります。
例えば:
Timer ISR 実行中
↓
UART ISR 割り込み
↓
さらに別ISR
というように、
ISR中にさらにISRが発生することがあります。
これを「割り込みネスト」と呼びます。
ネストで起きる問題
ネストが深くなると、
- スタック消費増大
- RAM不足
- 再入問題
- HardFault
などにつながります。
特にスタック不足は、
再現性が低く非常に危険です。
スタック領域との関係
割り込み発生時には、
CPU状態がスタックへ退避されます。
つまりネストが増えるほど、
スタック使用量も増加します。
スタック領域については、
こちらの記事で詳しく解説しています。


このように割り込みが連続する状況では、割り込み優先度設計が重要です。
割り込み優先度設計はこちらの記事で解説しています。


理想的なISR設計
ISRの理想は、
👉 「イベント通知だけ行う」
ことです。
例えば:
volatile uint8_t uart_rx_event = 0;
void UART_IRQHandler(void) {
uart_rx_event = 1;
}ISRではフラグONだけ行います。
実処理はメインループ側
while(1) {
if(uart_rx_event) {
uart_rx_event = 0;
process_uart();
}
}実際の重い処理は、
メインループ側で行います。
なぜこの設計が重要なのか?
この構造にすることで、
- ISR短縮
- リアルタイム性向上
- デバッグ性向上
- ネスト影響軽減
などのメリットがあります。
状態遷移設計との関係
実際の組み込み開発では、以下の「イベント駆動型設計」が良く使われます。


このような「イベント駆動型設計」は、
組み込みソフトで非常によく使われる基本パターンです。
状態遷移設計については、
こちらの記事で詳しく解説しています。


なぜ割り込みは設計力が問われるのか?
割り込みは、
- 実行タイミングが予測できない
- 並行動作になる
- スタックを消費する
- 優先度管理が必要
など、かなり設計難易度が高い仕組みです。
つまり、
👉 「並行処理設計そのもの」
です。
ここを軽く扱うと、
- 再現しないバグ
- タイミング依存不具合
- 長期間追えない障害
につながります。
まとめ
割り込み処理でやってはいけないこと:
- 重い処理を書く
- printfする
- 共有変数を雑に扱う
- mallocする
- ネストを無視する
ISRは便利な処理場所ではありません。
👉 「最も慎重に扱うべき設計ポイント」
です。
そして実際の組み込みでは、
👉 ISRはイベント通知だけ行う
という設計が非常に重要になります。
次回予告
次回は、
「割り込みは“処理を書く場所ではない”」
という設計思想について、
さらに深掘りしていきます。


この記事が参考になった方へ
割り込み処理については、
こちらのまとめ記事で全体像を整理しています。


また当サイトでは、
割り込み理解に必要なC言語文法やメモリ構造についても実務目線で解説しています。




エンジニアとして技術を学ぶことは重要ですが、
キャリアや副業についても同時に考える必要があります。
副業の現実や市場価値、今後のキャリア戦略については、
こちらの記事でまとめています。


技術に関するご相談・開発・自動化ツール作成・記事執筆などのご依頼も承っています。
小さなご相談からでもお気軽にご連絡ください。


コメント