Arduinoの割り込み:attachInterrupt、ISRのルール、チャタリング対策
Arduinoの割り込みの使い方:attachInterrupt、Unoのピン2と3、RISING/FALLING/CHANGE、volatile、チャタリング対策、エンコーダーと回転数の例、さらにESP32とPicoも解説します。
Arduinoの割り込みは、待っている変化がピンで起きた瞬間(またはタイマーのイベントが発生した瞬間)に、プロセッサに実行中の処理を中断させ、自分で書いた特別な関数を実行させるしくみです。つまり、loop()の中で「ボタンは押された?」と絶えず確認する代わりに、Arduinoに「押されたら知らせて」と頼むわけです。これにはattachInterrupt()関数を使います。Arduino Unoの割り込みピンはD2とD3ですが、ESP32とRaspberry Pi Picoではほぼすべてのピン(GPIO)で割り込みを発生させられます。
割り込みは、とても速いパルスを1つも逃さずに数える(エンコーダー、速度センサー、水流センサー)、長い処理の途中で短いイベントを捉える、スリープ中のボードを起こす、といった場面に欠かせません。このガイドでは、割り込みがハードウェアでどう動くか、各ボードで使えるピン、RISING、FALLING、CHANGEの各モードの違い、割り込みサービスルーチン(ISR)を書くときのルール、ボタンのチャタリング対策、そしてボタン、エンコーダー、回転数(RPM)カウンターの例をステップごとに見ていきます。最後に、ESP32のIRAM_ATTRと、PicoのMicroPythonでのirqの使い方も学びます。

割り込みとは?そのしくみ
ドアベルを思い浮かべてください。ドアベルがなければ、誰か来ていないか確かめるために毎分ドアまで歩いて行かなければなりません。これをポーリングと呼び、loop()の中でdigitalRead()を使ってピンを読むのはまさにこれです。ドアベルは割り込みにあたります。ほかのことをしている最中に鳴り、手を止めてドアを確認し、またやっていたことに戻ります。マイコンも同じように動きます。選んだ変化がピンで起きると、ハードウェアが割り込みフラグを立てます。プロセッサは実行中の命令を終え、レジスタを退避し、割り込みベクタを経由して自分で書いたISR(割り込みサービスルーチン)にジャンプし、ISRが終わるとloop()の中断した場所からそのまま処理を続けます。Unoでは、この切り替えにかかる時間はわずか数マイクロ秒です。
| 項目 | ポーリング | 割り込み |
|---|---|---|
| しくみ | loop()がピンを繰り返し読む | ハードウェアが変化を捉えてISRを実行する |
| 応答時間 | loop()が遅いほど遅延が大きくなる | 数マイクロ秒 |
| コードにdelay()がある場合 | イベントを見逃すことがある | それでもイベントを捉えられる |
| 非常に速いパルス | 見逃す | 1つずつ数えられる |
| コードの複雑さ | シンプル | volatileとアトミックな読み取りが必要 |
| 向いている用途 | ボタン、変化の遅いセンサー | エンコーダー、速度センサーや水流センサー、スリープからの復帰 |
割り込みに対応したピンは?Arduino、ESP32、Pico
| ボード | 外部割り込みピン | 備考 |
|---|---|---|
| Arduino Uno R3、Nano、Pro Mini | D2, D3 | D2 = INT0, D3 = INT1 |
| Arduino Uno R4(Minima / WiFi) | D2, D3 | Arduino公式リファレンスに記載されているピン |
| Arduino Mega 2560 | 2, 3, 18, 19, 20, 21 | 20と21は、I2Cとして使っている間は割り込みに使えない |
| Arduino Leonardo、Micro | 0, 1, 2, 3, 7 | 0と1はシリアルポートのピンも兼ねる |
| Arduino Nano Every | すべてのデジタルピン | ATmega4809プロセッサ |
| ESP32 | すべてのGPIO | GPIO 34〜39は入力専用で、内部プルアップがない |
| ESP8266(NodeMCU) | GPIO 16(D0)以外のすべて | ISRにはIRAM_ATTRを付ける必要がある |
| Raspberry Pi Pico / Pico W | GP0〜GP28のすべて | ピンは3.3 V、5 Vは絶対にかけない |
コードではピン番号を直接渡さず、digitalPinToInterrupt(pin)で割り込み番号に変換してください。割り込み番号はボードによって変わるからです。UnoではD2が割り込み0、D3が割り込み1ですが、Leonardoでは割り込み0がD3、割り込み1がD2です。Unoで3つ以上の割り込みが必要な場合は、ピン変化割り込み(PCINT)がほぼすべてのピンで使えます。ただし、わかるのは「何かが変化した」ことだけで、同じポートのピンはすべて1つのISRを共有します。PinChangeInterruptライブラリやEnableInterruptライブラリを使うと、扱いやすくなります。ボードの完全なピンマップはピン配置ツールで、Unoのピンの機能はArduino Unoのピン配置の記事で確認できます。
attachInterruptの使い方と割り込みモード
構文はattachInterrupt(digitalPinToInterrupt(pin), ISR, mode)です。2番目の引数は、割り込みが発生したときに実行される関数の名前です。この関数は引数を取らず、何も返しません(void)。3番目の引数は、どのイベントで割り込みを発生させるかを決めます。割り込みを無効にするにはdetachInterrupt(digitalPinToInterrupt(pin))を、すべての割り込みを一時的に止めるにはnoInterrupts()を、再び有効にするにはinterrupts()を使います。
| モード | いつ発生する? | 主な用途 |
|---|---|---|
| LOW | ピンがLOWの間、何度も繰り返し | スリープからの復帰 |
| CHANGE | 変化するたび(両方のエッジ) | エンコーダー、パルス幅の測定、チャタリング対策 |
| RISING | LOWからHIGHに変わるとき(立ち上がりエッジ) | センサーのパルスの始まり、プルダウンのボタン |
| FALLING | HIGHからLOWに変わるとき(立ち下がりエッジ) | プルアップのボタンを押したとき、オープンコレクタのセンサー |
| HIGH | ピンがHIGHの間ずっと | Due、Zero、MKR1000のみ。ESP32ではONHIGH |
回路に合わせてモードを選びましょう。ボタンをINPUT_PULLUPで配線した場合、ピンは普段HIGHで、押すとLOWに下がるので、FALLINGで押した瞬間を捉えられます。LOWモードは注意して使ってください。ピンがLOWのままの間はISRが何度も繰り返し実行され、loop()はほとんど進みません。そのため、通常はボードをスリープから起こす用途にしか使われません。Unoを最も深いスリープモード(パワーダウン)から起こすには、データシートではLOWレベル割り込みかピン変化割り込みが推奨されています。ESP32では、定数LOWとHIGHは割り込みモードとして期待どおりには動きません。LOWでは割り込みがまったく有効にならず、HIGHは立ち上がりエッジのように動作します。ESP32でレベル割り込みを使うにはONLOWとONHIGHを書きます。
最初の例:割り込みでボタンを押した回数を数える
- ボタンの一方の足をD2に、もう一方をGNDにつなぎます。外付けの抵抗は不要です。内部プルアップを使います。
- 下のコードを書き込み、シリアルモニタを9600ボーで開きます。
- ボタンを何回か押して、カウンターが増えていくのを確認します。loop()の中にdelay(500)があっても、押した回数を1回も見逃しません。
- 数が一度に2や3増えることがあっても驚かないでください。これは接点のチャタリングで、このあとすぐに対策します。
1const byte BUTTON_PIN = 2; // kesme pini D2 (INT0) / interrupt pin D2 (INT0)
2volatile unsigned int pressCount = 0; // ISR ile paylaşılan sayaç / counter shared with the ISR
3
4void onPress() { // ISR: kısa ve hızlı / ISR: short and fast
5 pressCount++;
6}
7
8void setup() {
9 Serial.begin(9600);
10 pinMode(BUTTON_PIN, INPUT_PULLUP); // dahili pull-up, buton GND'ye / internal pull-up, button to GND
11 attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), onPress, FALLING);
12}
13
14void loop() {
15 noInterrupts(); // 2 baytlık değeri güvenle kopyala / copy the 2-byte value safely
16 unsigned int count = pressCount;
17 interrupts();
18
19 Serial.print(F("Basis / Presses: "));
20 Serial.println(count);
21 delay(500); // loop yavaş olsa da basış kaçmaz / presses are not missed even if loop is slow
22}このコードには重要なポイントが3つあります。1つ目に、onPress()はカウンターに1を足すだけで、すぐに終わります。ISRの役目はイベントを記録することで、処理そのものを行うことではありません。2つ目に、カウンターはvolatileとして宣言されているので、コンパイラは値をレジスタに保持して古い値を読むことはせず、毎回メモリから最新の値を読み出します。3つ目に、loop()はカウンターを読む間だけ割り込みを一時的に止めます。Unoではunsigned intは2バイトで、8ビットのプロセッサはこれを2回に分けて読みます。カウンターが255から256、つまり0x00FFから0x0100に変わるとき、loop()はまず下位バイト(0xFF)を読みます。ちょうどそのときに割り込みが発生してカウンターを増やすと、次に上位バイト(0x01)を読み、結果は0x01FF、つまり511になります。このバグはごくまれにしか現れないため、原因を突き止めるのも困難です。AVRボードでは、util/atomic.hのATOMIC_BLOCK(ATOMIC_RESTORESTATE)マクロでも同じ保護が得られます。boolやbyteのような1バイトの変数には、この問題はありません。
ISRのルール:短く、volatileを使い、delayは使わない
- 短く速く:ISRの実行中は、ほかの割り込みは待たされます。Unoではmillis()のカウンターやシリアルポートも割り込みを使っているので、ISRが長いとタイミングが狂い、受信したシリアルデータを取りこぼすことがあります。理想的なISRは数マイクロ秒で終わります。
- delay()を使わない:delay()は、Timer0の割り込みで更新されるカウンターを使って時間を計ります。ISRの中では割り込みが止まっているので、delay()は動きません。millis()は読めますが、ISRの実行中は進まず、micros()は1〜2 msを過ぎると正しくない値を返すようになります。どうしてもごく短い待ち時間が必要な場合は、delayMicroseconds()なら使えます。
- Serial.print()を使わない:シリアルデータの送信も割り込みに依存しています。ISRが長くなるうえ、バッファがいっぱいになると処理が止まってしまいます。表示はloop()に任せましょう。
- 共有する変数はvolatileにする:ISRとloop()の両方で使う変数は、すべてvolatileとして宣言する必要があります。
- 複数バイトの変数はアトミックに読む:loop()では、int、long、float、unsigned longの変数をnoInterrupts()とinterrupts()の間でコピーします。
- 引数なし、戻り値なし:ISRはvoidで、引数を取ってはいけません。データのやり取りはグローバル変数で行います。
- 重い処理はISRの外で:I2Cディスプレイへの書き込み、SDカードへの保存、Wireのようにそれ自体が割り込みを必要とするライブラリの呼び出しは、ISRの中ではハングします。
割り込みでボタンを読み、チャタリングを防ぐ
機械式のボタンを押すと、金属の接点はすぐには落ち着かず、およそ0.1〜10ミリ秒の間に何度も開いたり閉じたりします。loop()では遅すぎて気づかないこともありますが、割り込みはすべてのエッジを捉えるので、1回押しただけで3〜5回の割り込みが発生することがあります。さらに、チャタリングは押したときだけでなく離したときにも起こるため、FALLINGだけを時間によるロックアウトと組み合わせて使うコードでは、ボタンをしばらく押し続けてから離したときに二重にカウントすることがあります。下のコードはCHANGEモードを使い、ピンがLOWで、かつその直前の少なくとも50 msの間ラインが静かだった場合にだけ、押されたと判断します。これで、押したときと離したときの両方のチャタリングを取り除けます。
1const byte BUTTON_PIN = 2; // kesme pini / interrupt pin
2const byte LED_PIN = 13; // dahili LED / onboard LED
3const unsigned long QUIET_MS = 50; // gereken sessizlik süresi / required quiet time
4
5volatile bool ledState = false; // ISR ile paylaşılır / shared with the ISR
6
7void onButtonChange() { // iki kenarda da çalışır / runs on both edges
8 static unsigned long lastEdgeMs = 0;
9 unsigned long now = millis(); // ISR'de okunur ama artmaz / readable in an ISR but does not advance
10 bool pressed = (digitalRead(BUTTON_PIN) == LOW);
11
12 if (pressed && now - lastEdgeMs > QUIET_MS) { // hat sessizdi: gerçek basış / line was quiet: real press
13 ledState = !ledState;
14 }
15 lastEdgeMs = now; // her kenar süreyi yeniden başlatır / every edge restarts the timer
16}
17
18void setup() {
19 pinMode(LED_PIN, OUTPUT);
20 pinMode(BUTTON_PIN, INPUT_PULLUP);
21 attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), onButtonChange, CHANGE);
22}
23
24void loop() {
25 digitalWrite(LED_PIN, ledState ? HIGH : LOW); // bool tek bayt, okuması atomik / bool is one byte, atomic read
26 // burada uzun işler olsa da basış kaçmaz / long tasks here will not make you miss a press
27}ソフトウェアだけでなく、ハードウェアでもチャタリングを抑えられます。ボタンと並列に100 nFのコンデンサを入れ、ピンに10 kΩのプルアップ抵抗を付けると、時定数はτ = R × C = 10 kΩ × 100 nF = 1 msとなり、短いチャタリングをならしてくれます。Unoの入力にあるシュミットトリガもエッジを整えます。ほかの値はRC時定数ツールで計算できます。正直に言うと、手で押すボタンには割り込みがまったく必要ないことも多く、loop()が速ければポーリングで十分です。プルアップ、プルダウン、millis()によるチャタリング対策についてはArduinoに押しボタンをつなぐ方法をご覧ください。割り込みは、速くて短い信号のためにとっておきましょう。
割り込みでロータリーエンコーダーを読む
ロータリーエンコーダー(KY-040など)を回すと、CLKとDTという2本のピンに、互いにずれた矩形波が出ます。速く回すとパルスは1ミリ秒より短くなり、ポーリングでは簡単にステップを取りこぼします。最も簡単な方法は、CLKを割り込みピンにつなぎ、CLKが立ち下がった瞬間のDTのレベルを調べることです。DTがHIGHなら一方の向き、LOWならもう一方の向きに回ったことになります。このバージョンは1クリックにつき1ステップを数えます。向きが逆になる場合は、++と--の行を入れ替えてください。安いエンコーダーもチャタリングを起こすので、CLKとGNDの間、DTとGNDの間に10〜100 nFのコンデンサを入れると、はっきりと違いが出ます。両方のピンを割り込みにつなぎ、状態テーブルを使って1ステップも取りこぼさないバージョンは、ロータリーエンコーダー(KY-040)のガイドをご覧ください。
1const byte PIN_CLK = 2; // kesme pini / interrupt pin
2const byte PIN_DT = 4; // normal dijital giriş / regular digital input
3
4volatile long position = 0; // 4 baytlık konum / 4-byte position
5
6void onClkFall() {
7 if (digitalRead(PIN_DT) == HIGH) { // yön DT seviyesinden / direction from the DT level
8 position++;
9 } else {
10 position--;
11 }
12}
13
14void setup() {
15 Serial.begin(115200);
16 pinMode(PIN_CLK, INPUT_PULLUP);
17 pinMode(PIN_DT, INPUT_PULLUP);
18 attachInterrupt(digitalPinToInterrupt(PIN_CLK), onClkFall, FALLING);
19}
20
21void loop() {
22 static long lastPos = 0;
23
24 noInterrupts(); // atomik kopya / atomic copy
25 long pos = position;
26 interrupts();
27
28 if (pos != lastPos) {
29 Serial.println(pos);
30 lastPos = pos;
31 }
32}回転を数える:モーターの回転数(RPM)を測る
モーターや車輪の速度を測るのは、割り込みの代表的な使い方です。センサーには、LM393を使ったスロット型の光学式速度センサーと20スロットのエンコーダーディスクの組み合わせか、軸に磁石を貼り付けたA3144のようなホールセンサーを使います。スロット1つごと、または磁石が1回通過するごとに、1つのパルスが出ます。1秒間に数えたパルス数から、速度はRPM = 1秒あたりのパルス数 × 60 / 1回転あたりのパルス数で求められます。例えば、20スロットのディスクで1秒あたり100パルスなら、モーターは100 × 60 / 20 = 300 RPMで回っています。
1const byte SENSOR_PIN = 3; // kesme pini D3 (INT1) / interrupt pin D3 (INT1)
2const byte PULSES_PER_REV = 20; // diskteki yarık sayısı / slots on the encoder disc
3
4volatile unsigned long pulseCount = 0;
5
6void onPulse() {
7 pulseCount++;
8}
9
10void setup() {
11 Serial.begin(9600);
12 pinMode(SENSOR_PIN, INPUT_PULLUP);
13 attachInterrupt(digitalPinToInterrupt(SENSOR_PIN), onPulse, FALLING);
14}
15
16void loop() {
17 static unsigned long lastMs = 0;
18
19 if (millis() - lastMs >= 1000) { // her saniye hesapla / compute once per second
20 lastMs += 1000;
21
22 noInterrupts(); // kopyala ve sıfırla / copy and reset
23 unsigned long pulses = pulseCount;
24 pulseCount = 0;
25 interrupts();
26
27 float rpm = pulses * 60.0 / PULSES_PER_REV;
28 Serial.print(F("RPM: "));
29 Serial.println(rpm, 1);
30 }
31}ここでは、loop()が1秒ごとにカウンターをコピーしてリセットします。その2つの操作の間に割り込みが来てもパルスを失わないように、どちらも同じnoInterrupts()ブロックの中で行います。20スロットのディスクでは、分解能は3 RPMです。低速でより正確な結果が欲しい場合は、パルスを数える代わりに、ISRの中でmicros()を使って2つのパルスの間隔を測り、RPM = 60,000,000 / (周期(µs) × 1回転あたりのパルス数)を使います。同じ方法は、YF-S201水流センサー、ホールセンサーによる速度測定、ギア付きDCモーターの車輪の速度にも使えます。
タイマー割り込みについて少しだけ
割り込みはピンから来るものだけではありません。マイコン内部のタイマーも一定の間隔で割り込みを発生させられるので、loop()が何をしていても、関数をきっちり1 msごとや500 msごとに実行できます。Unoには3つのタイマーがあり、そのいくつかはArduino自身の関数ですでに使われています。
| タイマー | ビット数 | 使用している機能 | PWMピン |
|---|---|---|---|
| Timer0 | 8ビット | millis()、delay()、micros() | 5, 6 |
| Timer1 | 16ビット | Servoライブラリ、TimerOne | 9, 10 |
| Timer2 | 8ビット | tone()、IRremote | 3, 11 |
Unoで最も簡単なのはTimerOneライブラリを使う方法です。Timer1.initialize(500000)で周期をマイクロ秒単位で設定し、Timer1.attachInterrupt(function)で500 msごとに実行されるISRを登録します。Timer1を使うので、Servoライブラリや、ピン9と10のanalogWrite()と競合します。Arduinoコア3.xのESP32では、timerBegin(1000000)、timerAttachInterrupt()、timerAlarm()でタイマー割り込みを設定します。2.xのtimerAlarmWrite()とtimerAlarmEnable()は削除されたため、古い例は「was not declared in this scope」(宣言されていない)というエラーで失敗します。Picoでは、MicroPythonのTimer(period=500, mode=Timer.PERIODIC, callback=function)の1行で周期的な割り込みを設定できます。タイマーとPWMの関係についてはPWMとは?をご覧ください。
ESP32の割り込み:IRAM_ATTRとportMUX
ESP32では特別なピンを探す必要はなく、すべてのGPIOで割り込みを発生させられます。ただしGPIO 34〜39には内部プルアップがないので、そこにつなぐボタンには外付けの10 kΩプルアップ抵抗が必要です。また、Espressifのエラッタには、ADCの使用中にGPIO 36と39が誤った割り込みを発生させることがあると記載されています。ESP32での本当の違いは、ISRの前に書くIRAM_ATTR属性です。これは関数をフラッシュではなく高速な内部RAM(IRAM)に配置します。フラッシュへの書き込み中(Preferences、SPIFFS、OTA)はキャッシュが無効になるため、構成によっては、その瞬間にフラッシュ上のコードにアクセスする割り込みが「Guru Meditation Error: Core 1 panic'ed (Cache disabled but cached memory region accessed)」(キャッシュが無効なのにキャッシュ領域にアクセスした)というエラーでボードをリセットさせることがあります。ESP8266はさらに厳しく、IRAM_ATTRのないISRは、attachInterruptを呼んだ時点で「ISR not in IRAM!」というメッセージとともにボードを停止させます。
1const int BUTTON_PIN = 27; // her GPIO kesme verebilir / any GPIO can interrupt
2const int LED_PIN = 2; // çoğu DevKit'te dahili LED / onboard LED on most DevKits
3const uint32_t QUIET_MS = 50; // gereken sessizlik süresi / required quiet time
4
5volatile uint32_t pressCount = 0;
6portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED; // paylaşılan veri kilidi / lock for shared data
7
8void IRAM_ATTR onButtonChange() { // ISR IRAM'de çalışır / ISR runs from IRAM
9 static uint32_t lastEdgeMs = 0;
10 uint32_t now = millis();
11 bool pressed = (digitalRead(BUTTON_PIN) == LOW);
12
13 if (pressed && now - lastEdgeMs > QUIET_MS) {
14 portENTER_CRITICAL_ISR(&mux);
15 pressCount++;
16 portEXIT_CRITICAL_ISR(&mux);
17 }
18 lastEdgeMs = now;
19}
20
21void setup() {
22 Serial.begin(115200);
23 pinMode(LED_PIN, OUTPUT);
24 pinMode(BUTTON_PIN, INPUT_PULLUP);
25 attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), onButtonChange, CHANGE);
26}
27
28void loop() {
29 static uint32_t lastCount = 0;
30
31 portENTER_CRITICAL(&mux); // loop tarafında kilit / lock on the loop side
32 uint32_t count = pressCount;
33 portEXIT_CRITICAL(&mux);
34
35 if (count != lastCount) {
36 lastCount = count;
37 digitalWrite(LED_PIN, (count % 2) ? HIGH : LOW); // her basışta LED değişir / LED toggles on every press
38 Serial.printf("Basis / Press: %lu\n", (unsigned long)count);
39 }
40}ESP32はFreeRTOSで動き、コアが2つあるため、共有する変数はUnoのnoInterrupts()ではなくportMUX_TYPEのロックで保護します。ISRの中ではportENTER_CRITICAL_ISR()を、loop()ではportENTER_CRITICAL()を使います。ESP32では32ビットの変数の読み取りはもともと1回で済みますが、値を読んでリセットする、複数の変数をまとめて更新するといった場合にはロックが欠かせません。同じ関数を複数のピンに割り当てたい場合は、attachInterruptArg()を使うと、ピン番号などの引数をISRに渡せます。ESP32の基本はESP32とは?を、タスクとコアについてはESP32のFreeRTOSとデュアルコアをご覧ください。
Raspberry Pi Picoの割り込み:MicroPythonのirq
Raspberry Pi Picoでは、GP0からGP28までのすべてのピンで割り込みを発生させられます。MicroPythonでは、ピンオブジェクトのirq()メソッドで割り込みを設定します。triggerにはPin.IRQ_FALLING、Pin.IRQ_RISING、またはその両方を組み合わせたものを渡し、handlerには割り込みが発生したときに実行される関数を指定します。この関数は、割り込みを発生させたピンを引数として受け取ります。Pythonにはvolatileがないので、ISRで変更する変数をglobalとして宣言するだけで十分です。時間を測るには、time.ticks_ms()と、オーバーフローしても正しく計算できるtime.ticks_diff()を使います。下のコードは、Arduinoの例で使った「ラインが静かな時間」を見るロジックをPicoに移したものです。
1from machine import Pin
2import time
3
4led = Pin("LED", Pin.OUT) # dahili LED (Pico ve Pico W) / onboard LED (Pico and Pico W)
5button = Pin(14, Pin.IN, Pin.PULL_UP) # buton GP14 ile GND arasında / button between GP14 and GND
6
7QUIET_MS = 50 # gereken sessizlik süresi / required quiet time
8presses = 0
9last_edge = time.ticks_ms()
10
11def on_change(pin): # handler tetikleyen pini alır / handler receives the pin
12 global presses, last_edge
13 now = time.ticks_ms()
14 if pin.value() == 0 and time.ticks_diff(now, last_edge) > QUIET_MS:
15 presses += 1
16 led.toggle()
17 last_edge = now
18
19button.irq(trigger=Pin.IRQ_FALLING | Pin.IRQ_RISING, handler=on_change)
20
21while True:
22 print("Basis / Presses:", presses) # ana döngü uyurken bile basışlar sayılır / presses are counted while the loop sleeps
23 time.sleep(1)Picoでは、irqはデフォルトでソフト割り込みとして動作します。MicroPythonが関数をキューに入れてできるだけ早く実行するので、その中でprintを使ったりリストを作ったりしても問題ありませんが、遅延が少し大きくなります。hard=Trueを渡すと遅延はマイクロ秒単位まで小さくなりますが、その代わりISRの中でメモリを確保できなくなるため、新しいリスト、文字列、さらにはfloatの計算でも「MemoryError」が発生することがあります。Arduino IDE(Earle Philhowerのコア)からPicoをプログラムする場合、attachInterrupt()はどのGPIOでも同じように動作します。セットアップについてはRaspberry Pi Picoとは?を、基本的なコマンドについてはMicroPythonの基本をご覧ください。
割り込みを使うプロジェクトアイデア
- 反応時間タイマー:LEDが点灯してからボタンを押すまでの時間を、ISRの中でmicros()を使って1ミリ秒未満の精度で測ります。
- 自転車のスピードメーター:車輪に磁石を、フレームにホールセンサーを取り付け、1回転にかかる時間から速度と走行距離を計算します。
- 水道メーター:YF-S201流量センサーのパルスを数え、使った水の量をリットルで表示します。
- まっすぐ走るロボットカー:車輪のエンコーダーで両方のモーターの速度を測り、速度をそろえます。
- メニュー操作:ロータリーエンコーダーで画面のメニューを切り替え、設定を変更します。
- 電池で動くアラーム:スリープ中のボードをPIRセンサーの出力で起こします。ESP32についてはディープスリープの記事をご覧ください。
よくある間違いと対処法
| 症状 | 原因 | 対処法 |
|---|---|---|
| 割り込みがまったく発生しない | attachInterrupt(2, ...)にピン番号を渡した、または割り込みに対応していないピンを選んだ | digitalPinToInterrupt(pin)を使う。UnoではD2かD3につなぐ |
| 1回押すとカウンターが2〜5増える | 接点のチャタリング | ソフトウェアでチャタリング対策をする、または100 nFのコンデンサを追加する(両方でも可) |
| loop()で変数がまったく変わらない | volatileを付け忘れた | 共有する変数をvolatileとして宣言する |
| ときどき異常に大きな値が出る | 複数バイトの変数をアトミックに読んでいない | noInterrupts()とinterrupts()の間でコピーする |
| プログラムが固まる、またはmillis()がずれる | ISRの中のdelay()、Serial、長い処理 | ISRではフラグかカウンターの更新だけを行う |
| ボタンに触れていないのに割り込みが発生する | ピンがフローティング状態で、プルアップがない | INPUT_PULLUPか外付けの10 kΩ抵抗を使う |
| 再登録した直後にISRが1回実行される | 割り込みが無効の間にエッジが来てフラグが立った(AVR) | Unoでは、attachInterruptの前にEIFR = bit(INTF0);と書く(D3ならINTF1) |
| ESP32がランダムにリセットする | ISRがIRAMにない、または重い処理をしている | IRAM_ATTRを付け、ISRを短くする |
割り込みは、loop()がほかの処理で忙しいときでもすべてのイベントを捉えられる強力な道具です。覚えておくべきルールは3つだけです。ISRは短くすること、共有する変数はvolatileにすること、そしてloop()ではそれをアトミックに読むことです。BlueGraysアプリには、ボタンカウンター、反応テスト、ESP32のボタンプロジェクト、そして割り込み(IRQ)とソフトウェアのポーリングによるチャタリング対策を比較するPicoのプロジェクトがあり、どれもステップごとの回路図とコードが付いています。
よくある質問
Arduino Unoで割り込みに対応しているピンはどれですか?
Arduino Uno R3、Nano、Pro Miniの外部割り込みピンは、D2(INT0)とD3(INT1)の2本だけです。Mega 2560では2、3、18、19、20、21を、Leonardoでは0、1、2、3、7を使えます。それ以外のピンで使えるのはピン変化割り込み(PCINT)だけで、これはピンが変化したことを知らせるだけです。
割り込み(ISR)の中でdelay()が動かないのはなぜですか?
delay()は、Timer0の割り込みで更新されるカウンターを使って時間を計ります。ISRの実行中はほかの割り込みが待たされるので、そのカウンターが進まず、delay()は正しく動作できません。待つ処理が必要な場合は、ISRではフラグを立てるだけにして、待つ処理はloop()でmillis()を使って行いましょう。
volatileは何をするもので、いつ使うべきですか?
volatileは、その変数が通常のプログラムの流れの外、例えばISRの中で、いつでも変わる可能性があることをコンパイラに伝えます。そのため値はレジスタにキャッシュされず、毎回メモリから読み出されます。ISRとloop()の両方で使う変数は、すべてvolatileにしてください。ただし、volatileは複数バイトの読み取りをアトミックにはしません。それにはnoInterrupts()を使います。
Arduino Unoで2つを超える割り込みを使えますか?
はい。ピン変化割り込み(PCINT)のおかげで、Unoのほぼすべてのピンで割り込みを発生させられます。わかるのは変化があったことだけで、同じポートのピン(D0〜D7、D8〜D13、A0〜A5)は1つのISRを共有します。PinChangeInterruptライブラリやEnableInterruptライブラリを使えば、attachInterruptとほとんど同じ感覚で使えます。
割り込みとポーリングのどちらを使うべきですか?
手で押すボタンやゆっくり変化するセンサーなら、通常はloop()でのポーリングで十分で、そのほうがシンプルです。割り込みは、エンコーダー、速度センサー、流量センサーなどの速くて短いパルスや、ボードをスリープから起こす場合に使いましょう。loop()が長いdelay()だらけなら、millis()に置き換えるだけでもほとんどの問題が解決します。
ESP32でIRAM_ATTRを省略するとどうなりますか?
コードは通常コンパイルでき、一見動いているように見えますが、ISRはフラッシュに置かれたままで起動が遅くなり、構成によってはフラッシュへの書き込み中に「Cache disabled but cached memory region accessed」(キャッシュが無効なのにキャッシュ領域にアクセスした)というエラーでクラッシュすることがあります。ESP8266では、IRAM_ATTRがないとボードは「ISR not in IRAM!」ですぐに停止します。そのため、ESPボードではすべてのISRの前にIRAM_ATTRを付けてください。
この記事を読んだ後に作れるプロジェクト
全プロジェクトを見る
ボタンでLEDの点灯・消灯を切り替え
部屋の照明スイッチのように、ボタンを1回押すと点灯し、もう1回押すと消灯します。Arduinoはボタンが押されるたびにLEDの状態を反転させます。ボタン内部の小さな接点の振動で誤動作しないよう、短い待ち時間も設けます。1つのボタンでオン・オフを切り替える、シンプルな仕組みです。

Raspberry Pi Picoのボタンのチャタリング対策:IRQとソフトウェアポーリングの比較
ボタンを1回押しただけで、コードでは3~4回と数えられますか?原因はチャタリングです。金属接点がマイクロ秒単位で振動します!シリアルモニタで、2つのカウント方法を並べて比較します。未処理のポーリングは振動も検出して多く数え、IRQとソフトウェアによるチャタリング対策は正確に数えます。接点が振動する仕組み、ソフトウェアによる対策(ticks_diff() > 50ms)、Pin.irq()による割り込みの登録、IRQ_FALLINGトリガー、ISRハンドラー、globalとISRの安全な扱い方を学びます。IRQでLEDを切り替え、動作を目で確認できます。シリアルモニタには押すたびに両方のカウントが表示されます。

ESP32でボタンによるLED制御(トグル)
ボタンを押すたびにLEDの状態が切り替わり、点灯中なら消灯し、消灯中なら点灯します。基本的なトグル動作です。チャタリング対策でボタンの機械的なノイズを除去し、1回の操作で1回だけ切り替わるようにします。

Raspberry Pi PicoでボタンによるLEDの点灯・消灯切り替え
ボタンとLEDを使って、さらに一歩進みます!基本のボタンプロジェクトと違い、ボタンを押し続ける必要はありません。1回押して離すとLEDが点灯し、もう1回押して離すと消灯します(トグル動作)。回路は同じ(LEDはGP14、ボタンはGP2)ですが、コードは大きく変わります。エッジ検出(1→0に変わる瞬間の検出)、状態の記録、チャタリング対策(機械的な接点の振動を除去)を学びます。MicroPythonのled.toggle()なら1行で状態を切り替えられます。モード切り替え、IRQ、ダブルクリックなど、ボタン操作を使うあらゆるプロジェクトの基礎になります。

7セグメントディスプレイとボタンで作るカウンター
3つのボタンを使うシンプルなカウンターです。1つ目のボタンを押すと表示の数字が1増え、2つ目を押すと1減り、3つ目を押すと0に戻ります。1桁の7セグメントディスプレイで0から9まで数え、範囲の端に達すると隅の小さな点が0.5秒間点灯して知らせます。スコアボードや順番表示器などのプロジェクトの基礎になります。

Arduino 音で知らせる反応速度テスト:反射神経をミリ秒で測ろう(LCD+ブザー+LED 3個)
反射神経はどれくらい速いでしょうか。ボタンを押すと、赤LEDが位置について、黄LEDが用意を知らせ、1.5〜3.5秒のランダムな待ち時間が始まります。緑LEDが光ってブザーが鳴った瞬間にもう一度押すと、反応時間がI2C LCDにミリ秒で表示され、下の行にその回の最高記録と平均が出ます。緑より先に押すと画面にTOO EARLY!と表示され、残念な音が鳴ります。ボタンを長押しすると新しいプレイヤーになり、全員が同じ待ち時間の順番で勝負できます。

ESP32 WebSocketカウンター(2つのボタン+LCD+Webリアルタイム表示)
このプロジェクトでは、ESP32に接続した2つのボタンでカウンターの値を変更します(GPIO 5で増加、GPIO 4で減少)。値は16x2 I2C LCDに即座に反映され、ESPAsyncWebServerのWebSocket機能により、接続中のすべてのブラウザーでもページを再読み込みせずに更新されます。HTTPポーリングの代わりに1つの接続を維持し、ESP32がws.textAll()で新しい値を全員に送信します。

Arduino記憶ゲーム(LED4個+ボタン4個+ブザー)
記憶力に自信はありますか?4色のLEDが順番に点灯し、それぞれの音を鳴らします。同じ順番でボタンを押して再現してください。正解するたびに新しい色が1つ追加され、短い勝利のメロディーが流れて、LCDディスプレイにスコアが表示されます。1回でも押し間違えると、悲しい音とともにゲームが終了し、最初からやり直しになります。
ほかの記事
BlueGraysアプリ
この回路をスマホでステップごとに作る
回路図、コード、部品リストを1つの画面で確認できます。Arduino、ESP32、Picoのプロジェクトを200件以上用意しています。

