Arduinoの割り込み処理(interrupt)とは――loop()を止めずに即時検知する仕組み

プログラミング・電子工作






Arduinoの割り込み処理(interrupt)とは――loop()を止めずに即時検知する仕組み。|YOFUKASI AI




Arduino / 電子工作

Arduinoの割り込み処理(interrupt)とは――loop()を止めずに即時検知する仕組み。

delay()で処理が止まっている間のボタン操作、取りこぼしていませんか。attachInterrupt()の基本構文から、volatile修飾子の必要性、実践的な実装手順までを解説します。

YOFUKASI AI編集部 | 更新日:2026年8月18日 | 読了目安:約13分

Chapter 01 — Introduction

01なぜloop()だけでは反応が遅れることがあるのか

結論から言えば、loop()の中でdigitalRead()を繰り返して状態を監視する「ポーリング」という方式には弱点がある。loop()の途中にdelay()や時間のかかる処理が入っていると、その間に発生したボタン操作やセンサーの変化を取りこぼしてしまう。割り込み(interrupt)を使えば、こうした処理の途中であっても、指定した条件が発生した瞬間に別の処理(割り込みサービスルーチン、ISR)へ即座に切り替えられる。

実例として、Arduinoの外部割り込みを使ってスイッチの状態変化を検知するプロジェクトで、割り込みを使ったにもかかわらずチャタリング(接点の跳ね返りによる誤検知)が発生し、ソフトウェア側の対策が必要になった事例がある。Arduinoの割り込み(interrupts)を使ってチャタリングが出た話【ソフトで解決】では、機械式スイッチを使う以上チャタリングは避けられない現象であり、割り込みを使う場合でも何らかの対策が必要になることが紹介されている。

編集部見解:割り込みは「反応の速さ」を手に入れられる一方で、扱いを誤ると原因の特定が難しいバグを生みやすい機能でもある。特にチャタリングや共有変数の扱いは、割り込みを初めて使う際につまずきやすいポイントだと考えられる。
― YOFUKASI AI編集部

本記事では、Arduinoにおける割り込みの基本構文から、ポーリングとの使い分け、そして緊急停止ボタンのような実践的な実装手順と注意点までを、初心者から中級者を対象に体系的に解説する。



Chapter 02 — Background

02Arduinoにおける割り込みの基礎知識

割り込み(interrupt)とは、通常のプログラムの流れを一時的に中断し、あらかじめ指定しておいた処理を実行する仕組みのことである。この際に呼び出される関数は割り込みサービスルーチン(ISR: Interrupt Service Routine)と呼ばれる。

Arduinoでピンの電圧変化をきっかけに割り込みを発生させる(外部割り込み)には、attachInterrupt()関数を使う。公式リファレンスでは、Arduino公式 Language Reference「attachInterrupt()」において、この関数が割り込み発生時に呼び出すISRを指定する働きを持つと定義されている。トリガーの条件は、ピンがLOWのときに発生するLOW、ピンの値が変化したときに発生するCHANGE、LOWからHIGHへ変化したときに発生するRISING、HIGHからLOWへ変化したときに発生するFALLINGの4種類が定義済み定数として用意されている

用語の整理

  • ISR(割り込みサービスルーチン):割り込みが発生した際に呼び出される関数。引数を取らず、戻り値も返さない(void型)と定められている。
  • 外部割り込み(external interrupt):特定のピンの電圧変化をきっかけに発生する割り込み。
  • トリガーモード:割り込みを発生させる条件。LOW・CHANGE・RISING・FALLINGの4種類。

すべてのピンが外部割り込みに対応しているわけではない。使いたいピンが対応しているかどうかは、Arduino公式 Language Reference「digitalPinToInterrupt()」で確認できる関数を使って調べることができる。Arduino UNOではデジタルピン2番・3番の2本のみが外部割り込みに対応している点に注意が必要である。

割り込みを学ぶ前に、Arduinoの基礎文法やボタン回路の基本をまだ体系的に学んでいない場合は、先に基礎を押さえておくとこの先の理解がスムーズになる。

Chapter 03 — Mechanism

03割り込みの仕組みと技術的背景

外部割り込みを使う最小構成のスケッチは、次のような形になる。

const byte ledPin = 13;
const byte interruptPin = 2;
volatile byte state = LOW;

void setup() {
  pinMode(ledPin, OUTPUT);
  pinMode(interruptPin, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(interruptPin), blink, CHANGE);
}

void loop() {
  digitalWrite(ledPin, state);
}

void blink() {
  state = !state;
}

ここで注目すべきはvolatile byte stateという宣言である。volatileは変数修飾子の一種で、コンパイラに対して「この変数はレジスタにキャッシュせず、常にRAMから読み直すように」と指示するものである。公式リファレンスでは、Arduino公式 Language Reference「volatile」において、変数の値がコードのある区画の制御が及ばない何か(並行して実行されるスレッドなど)によって変更される可能性がある場合にvolatileを宣言すべきであり、Arduinoボードにおいてこれが起こり得る唯一の場所が割り込みサービスルーチンに関連するコード区画であると説明されている

編集部想定シナリオ
volatileを付け忘れた場合、コードは一見正常にコンパイルされ、実機でも「たまたま動いているように見える」ことがある。ところがコンパイラの最適化レベルやコードの微修正をきっかけに、突然ISR内での変更がloop()側に反映されなくなる、という再現性の低いバグとして表面化しやすい。原因が分かりにくいため、割り込みで使う共有変数には最初からvolatileを付けておく習慣が推奨される。

また、ISRの実行中はプログラムの他の割り込みが一時的に無効化される。手動で割り込みを禁止・再開したい場合はnoInterrupts()interrupts()を使う。Arduino公式 Language Reference「interrupts()」では、タイミングがシビアな処理区間の前後でこれらの関数を使い、一時的に割り込みを無効化・再開する使い方が紹介されている。

タッチセンサーを使ったインタラクティブな演出のように、割り込みに近い即時反応が求められるプロジェクトの実例は、以下の記事でも扱っている。

ArduinoとPythonで実現するインタラクティブ花火演出システムの実例はこちらの記事で紹介している。タッチセンサーの入力をきっかけに演出を切り替える構成の参考になる。



Chapter 04 — Comparison

04ポーリング・外部割り込み・タイマー割り込みの比較

状態変化をどう検知するかは、用途によって最適な方式が変わる。以下に3つの選択肢を整理する。

項目 A. ポーリング(loop内でdigitalRead) B. 外部割り込み(attachInterrupt) C. タイマー割り込み(ライブラリ利用)
向いている用途 取りこぼしが許容できる一般的な入力 緊急停止など取りこぼし厳禁の入力 一定間隔での定期処理
反応速度 loop()の実行間隔に依存 ほぼ即時 設定した間隔で正確
実装の複雑さ 低い 中程度(volatile・チャタリング対策が必要) やや高い(ライブラリの知識が必要)
対応ピンの制約 制約なし ボードにより限定的(UNOは2本のみ) ピンではなくタイマー回路に依存

※各方式の適用範囲は編集部見解を含む。外部割り込みの対応ピン・トリガーモードの定義についてはArduino公式 Language Reference「attachInterrupt()」を参照。

迷った場合の目安として、「その入力を取りこぼした場合にどれだけ困るか」を基準にするとよい。取りこぼしが安全上の問題につながるなら外部割り込み、多少の取りこぼしが許容できるならポーリングのままでも実用上問題ないことが多い。

Chapter 05 — Practice

05実践:緊急停止ボタンを割り込みで実装する5ステップ

ここでは「モーターを動かしている最中でも、ボタンが押されたら即座に停止させる緊急停止ボタン」という編集部想定シナリオを題材に、割り込みを使った実装の具体的な手順を示す。

  1. STEP1. 対応ピンを確認し、プルアップ設定で配線する

    Arduino UNOの場合、外部割り込みに対応しているのはデジタルピン2番・3番のみである。ボタンの片側をこのピンに、もう片側をGNDに接続し、内部プルアップ(INPUT_PULLUP)を使う配線にする。

    実例:緊急停止ボタンをピン2に接続し、pinMode(2, INPUT_PULLUP);で内部プルアップを有効化した。ボタンを押すとピンがLOWになる配線にした。
  2. STEP2. 共有変数をvolatileで宣言する

    ISRの中で変更し、loop()側でも参照するフラグ変数をvolatile bool emergencyStop = false;のように宣言する。

    実例:volatile bool emergencyStop = false;を宣言。この変数だけをISRとloop()の橋渡しに使う設計にした。
  3. STEP3. ISRを短く、フラグを立てるだけの処理にする

    ISR内では時間のかかる処理を避け、フラグを立てるだけにとどめる。モーターの停止処理そのものはloop()側で行う。

    実例:void stopISR() { emergencyStop = true; }という1行だけのISRを定義した。モーター停止処理はloop()内の判定に任せた。
  4. STEP4. attachInterrupt()でISRを登録する

    setup()内でattachInterrupt(digitalPinToInterrupt(2), stopISR, FALLING);のように登録する。プルアップ配線の場合、ボタンを押した瞬間はHIGHからLOWへの変化になるため、トリガーモードはFALLINGを指定する。

    実例:attachInterrupt(digitalPinToInterrupt(2), stopISR, FALLING);を追加。ボタンを押した瞬間だけを検知するようにした。
  5. STEP5. loop()でフラグを判定し、チャタリング対策を加える

    loop()側でemergencyStopを判定し、trueであればモーターを停止する。あわせて、前回の割り込みからの経過時間をmillis()で確認し、短時間の再発生を無視するデバウンス処理を加える。

    実例:ISR内にif (millis() - lastInterruptTime > 50) { emergencyStop = true; } lastInterruptTime = millis();という判定を追加したところ、ボタンを1回押しただけで複数回反応していた誤検知が解消された(編集部想定シナリオ)。

ボタン回路の配線や、プルアップ・プルダウンの基礎からもう一度確認したい場合は、以下の記事も参考になる。

Arduinoとブレッドボードで学ぶボタン回路の基礎から応用まではこちらの記事で解説している

Chapter 06 — Pitfalls

06注意点・よくある誤解

!誤解1. ISR内にどんな処理を書いても問題ない

割り込みが発生している間はmillis()のカウントが止まるため、ISR内でdelay()は正しく動作しない。またシリアル通信中に割り込みが発生すると受信データが失われる可能性がある。ISR内はできるだけ短く、フラグを立てるだけの処理にとどめ、実際の処理はloop()側で行うのが基本となる。

!誤解2. 割り込みを使えばチャタリングの心配はない

機械式スイッチである以上、接点の跳ね返り(チャタリング)は割り込みを使っても発生し得る。1回のボタン操作で割り込みが複数回発生してしまうことがあるため、millis()を使ったソフトウェア的なデバウンス処理を組み合わせる必要がある。

!誤解3. どのピンでも割り込みを設定できる

外部割り込みに対応しているピンはボードによって異なり、Arduino UNOではデジタルピン2番・3番の2本のみである。対応していないピンをattachInterrupt()に指定してもエラーにならず正常にコンパイルされてしまう場合があるため、事前にdigitalPinToInterrupt()で対応状況を確認しておくことが望ましい。



Chapter 07 — FAQ

07FAQ

Q.Arduinoの割り込み(interrupt)とは何ですか。
A.割り込みとは、通常のプログラムの流れ(loop()の実行)を一時中断し、あらかじめ指定しておいた処理(割り込みサービスルーチン、ISR)を即座に実行する仕組みです。delay()で処理が止まっている間でも、割り込みは発生すればすぐに反応します。

Q.Arduino UNOで外部割り込みが使えるピンはどこですか。
A.Arduino UNOでは、デジタルピン2番(割り込み番号0)とデジタルピン3番(割り込み番号1)の2本が外部割り込みに対応しています。使いたいピンが対応しているかどうかは、digitalPinToInterrupt()関数で確認できます。

Q.割り込みで使う変数にvolatileを付ける必要があるのはなぜですか。
A.volatileを付けないと、コンパイラが最適化の過程で変数の値をレジスタにキャッシュしてしまい、割り込み内で変更された最新の値がloop()側で反映されない場合があります。割り込みサービスルーチンの中と外の両方からアクセスする変数には、volatile修飾子を付けるのが基本です。

Q.割り込みサービスルーチン(ISR)の中でdelay()やSerial.print()を使ってもよいですか。
A.推奨されません。割り込みが発生している間はmillis()のカウントが止まるため、ISR内でdelay()は正しく動作しません。また、シリアル通信中に割り込みが発生した場合、受信データが失われる可能性があるため、ISR内はできるだけ短く、フラグを立てるだけの処理にとどめるのが基本です。

Q.割り込みを使うとチャタリングの影響を受けやすくなりますか。
A.はい。機械式スイッチの接点の跳ね返り(チャタリング)により、1回のボタン操作で割り込みが複数回発生してしまうことがあります。前回の割り込みからの経過時間をmillis()で計測し、一定時間内の再発生を無視するソフトウェア的なデバウンス処理を組み合わせるのが一般的な対策です。

Q.割り込みを使わずポーリング(loop内でdigitalReadを繰り返す)だけではダメですか。
A.ポーリングでも多くの場合は対応できますが、loop()内に時間のかかる処理やdelay()が含まれていると、その間のボタン操作を取りこぼす可能性があります。緊急停止のように「取りこぼしが許されない」処理では、割り込みを使う方が確実です。逆に、多少の取りこぼしが許容できる用途では、コードがシンプルになるポーリングの方が適している場合もあります(編集部見解)。

参考文献

関連記事(内部リンク)


関連記事

特集記事

コメント

この記事へのコメントはありません。

TOP
CLOSE