Grundlagen · 13 Min. Lesezeit

Arduino Interrupt: attachInterrupt, ISR-Regeln und Entprellen

Arduino Interrupts erklärt: attachInterrupt, Pins 2 und 3 am Uno, RISING/FALLING/CHANGE, volatile, Entprellen, Encoder- und Drehzahl-Beispiele, ESP32 und Pico.

Ein Arduino Interrupt sorgt dafür, dass der Prozessor in dem Moment, in dem an einem Pin die erwartete Änderung eintritt (oder ein Timer-Ereignis auslöst), seine aktuelle Arbeit unterbricht und eine spezielle Funktion ausführt, die du geschrieben hast. Kurz gesagt: Statt in loop() ständig zu fragen „Wurde der Taster gedrückt?“, sagst du dem Arduino „Sag mir Bescheid, wenn er gedrückt wird“. Das erledigst du mit dem Befehl attachInterrupt(). Am Arduino Uno sind die Interrupt-Pins D2 und D3, beim ESP32 und beim Raspberry Pi Pico kann dagegen fast jeder GPIO einen Interrupt auslösen.

Interrupts sind unverzichtbar, um sehr schnelle Impulse lückenlos zu zählen (Encoder, Drehzahlsensoren, Durchflusssensoren), ein kurzes Ereignis mitten in einer langen Aufgabe zu erfassen und ein Board aus dem Schlaf zu wecken. In dieser Anleitung siehst du Schritt für Schritt, wie ein Interrupt in der Hardware abläuft, welche Pins du an welchem Board nutzen kannst, worin sich die Modi RISING, FALLING und CHANGE unterscheiden, welche Regeln für eine Interrupt-Service-Routine (ISR) gelten, wie du einen Taster entprellst und wie Beispiele für Taster, Encoder und Drehzahlmessung aussehen. Zum Schluss lernst du auch IRAM_ATTR am ESP32 und irq mit MicroPython am Pico kennen.

Arduino Uno Interrupt-Pins D2 (INT0) und D3 (INT1)
Der Arduino Uno hat nur zwei Pins für externe Interrupts: D2 (INT0) und D3 (INT1).

Was ist ein Interrupt und wie funktioniert er?

Denk an eine Türklingel: Ohne sie müsstest du jede Minute zur Tür gehen, um nachzusehen, ob jemand da ist. Das nennt man Polling (Abfragen), und genau das machst du, wenn du in loop() einen Pin mit digitalRead() liest. Die Klingel ist ein Interrupt: Sie läutet, während du mit etwas anderem beschäftigt bist, du unterbrichst, schaust zur Tür und machst dann dort weiter, wo du aufgehört hast. Ein Mikrocontroller arbeitet genauso. Tritt am Pin die gewählte Änderung ein, setzt die Hardware ein Interrupt-Flag; der Prozessor beendet den aktuellen Befehl, sichert seine Register, springt über den Interrupt-Vektor in deine ISR (Interrupt Service Routine) und macht nach deren Ende in loop() genau an der unterbrochenen Stelle weiter. Beim Uno dauert dieser Wechsel nur wenige Mikrosekunden.

EigenschaftPollingInterrupt
Funktionsweiseloop() liest den Pin immer wiederDie Hardware erkennt die Änderung, die ISR läuft
ReaktionszeitJe langsamer loop(), desto größer die VerzögerungWenige Mikrosekunden
Mit delay() im CodeEreignisse können verloren gehenEreignisse werden trotzdem erfasst
Sehr schnelle ImpulseWerden verpasstWerden einzeln gezählt
Aufwand im CodeEinfachvolatile und atomares Lesen nötig
Ideal fürTaster, langsame SensorenEncoder, Drehzahl- und Durchflusssensoren, Aufwachen aus dem Schlaf

Welche Pins können Interrupts? Arduino, ESP32 und Pico

BoardExterne Interrupt-PinsHinweis
Arduino Uno R3, Nano, Pro MiniD2, D3D2 = INT0, D3 = INT1
Arduino Uno R4 (Minima / WiFi)D2, D3Die in der offiziellen Arduino-Referenz genannten Pins
Arduino Mega 25602, 3, 18, 19, 20, 2120 und 21 sind nicht nutzbar, solange sie für I2C verwendet werden
Arduino Leonardo, Micro0, 1, 2, 3, 70 und 1 sind zugleich die Pins der seriellen Schnittstelle
Arduino Nano EveryAlle digitalen PinsProzessor ATmega4809
ESP32Alle GPIOsGPIO 34–39 sind reine Eingänge ohne internen Pull-up
ESP8266 (NodeMCU)Alle außer GPIO 16 (D0)ISRs müssen mit IRAM_ATTR markiert werden
Raspberry Pi Pico / Pico WAlle von GP0–GP28Die Pins arbeiten mit 3,3 V, niemals 5 V anlegen

Gib im Code nicht direkt die Pin-Nummer an, sondern wandle sie mit digitalPinToInterrupt(pin) in die Interrupt-Nummer um, denn diese unterscheidet sich von Board zu Board: Beim Uno ist D2 Interrupt 0 und D3 Interrupt 1, beim Leonardo gehört Interrupt 0 dagegen zu D3 und Interrupt 1 zu D2. Brauchst du am Uno mehr als zwei Interrupts, funktionieren Pin-Change-Interrupts (PCINT) an fast allen Pins; sie melden allerdings nur „hat sich geändert“, und alle Pins eines Ports teilen sich eine ISR. Die Bibliotheken PinChangeInterrupt und EnableInterrupt machen den Einsatz leichter. Die vollständige Pinbelegung deines Boards findest du im Tool Pinout, die Funktionen der Uno-Pins im Artikel Arduino Uno Pinbelegung.

Der häufigste Fehler ist attachInterrupt(2, funktion, FALLING). Die 2 wird hier als Interrupt-Nummer und nicht als Pin-Nummer gelesen; einen Interrupt 2 gibt es am Uno nicht, also passiert gar nichts, und eine Fehlermeldung bekommst du auch nicht. Richtig ist attachInterrupt(digitalPinToInterrupt(2), funktion, FALLING).

attachInterrupt verwenden und die Interrupt-Modi

Die Syntax lautet attachInterrupt(digitalPinToInterrupt(pin), ISR, modus). Der zweite Parameter ist der Name der Funktion, die beim Interrupt ausgeführt wird; sie erhält keine Parameter und gibt nichts zurück (void). Der dritte Parameter legt fest, bei welchem Ereignis der Interrupt auslöst. Mit detachInterrupt(digitalPinToInterrupt(pin)) schaltest du einen Interrupt ab, mit noInterrupts() hältst du alle Interrupts kurz an und mit interrupts() schaltest du sie wieder ein.

ModusWann löst er aus?Typischer Einsatz
LOWImmer wieder, solange der Pin LOW istAufwecken aus dem Schlaf
CHANGEBei jeder Änderung (beide Flanken)Encoder, Impulsdauer messen, Entprellen
RISINGBeim Wechsel von LOW nach HIGH (steigende Flanke)Beginn eines Sensorimpulses, Taster mit Pull-down
FALLINGBeim Wechsel von HIGH nach LOW (fallende Flanke)Tastendruck mit Pull-up, Sensoren mit Open-Collector-Ausgang
HIGHSolange der Pin HIGH istNur bei Due, Zero und MKR1000; beim ESP32 ONHIGH

Wähle den Modus passend zu deiner Schaltung. Hast du den Taster mit INPUT_PULLUP angeschlossen, ist der Pin normalerweise HIGH und fällt beim Drücken auf LOW; den Tastendruck erfasst also FALLING. Mit dem Modus LOW solltest du vorsichtig sein: Solange der Pin LOW bleibt, läuft die ISR immer wieder, und loop() kommt kaum noch voran. Deshalb nutzt man ihn meist nur zum Aufwecken; um den Uno aus dem tiefsten Schlafmodus (Power-down) zu wecken, empfiehlt das Datenblatt einen LOW-Pegel-Interrupt oder einen Pin-Change-Interrupt. Beim ESP32 funktionieren die Konstanten LOW und HIGH als Interrupt-Modus nicht wie erwartet: LOW schaltet den Interrupt gar nicht ein, und HIGH verhält sich wie eine steigende Flanke. Für Pegel-Interrupts schreibst du dort ONLOW und ONHIGH.

Erstes Beispiel: Tastendrücke per Interrupt zählen

  1. Verbinde ein Bein des Tasters mit D2 und das andere mit GND. Ein externer Widerstand ist nicht nötig, wir nutzen den internen Pull-up.
  2. Lade den Code unten hoch und öffne den seriellen Monitor mit 9600 Baud.
  3. Drück den Taster ein paar Mal und beobachte, wie der Zähler steigt. Trotz delay(500) in loop() geht kein Tastendruck verloren.
  4. Wundere dich nicht, wenn die Zahl manchmal um 2 oder 3 auf einmal springt: Das ist das Prellen der Kontakte, und das lösen wir gleich.
C++ (Arduino IDE)
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}

In diesem Code stecken drei wichtige Punkte. Erstens erhöht onPress() nur den Zähler um eins und ist sofort fertig; die Aufgabe einer ISR ist, das Ereignis zu notieren, nicht die eigentliche Arbeit zu erledigen. Zweitens ist der Zähler als volatile deklariert, damit der Compiler ihn nicht in einem Register zwischenspeichert und einen veralteten Wert liest, sondern jedes Mal den aktuellen Wert aus dem Speicher holt. Drittens schaltet loop() die Interrupts beim Lesen kurz ab. Ein unsigned int belegt am Uno 2 Byte, und der 8-Bit-Prozessor liest ihn in zwei Schritten. Springt der Zähler von 255 auf 256, also von 0x00FF auf 0x0100, liest loop() zuerst das niedrige Byte (0xFF); kommt genau dann der Interrupt und erhöht den Zähler, liest loop() danach das hohe Byte (0x01), und das Ergebnis ist 0x01FF, also 511. Weil dieser Fehler sehr selten auftritt, ist er auch schwer zu finden. Bei AVR-Boards bietet das Makro ATOMIC_BLOCK(ATOMIC_RESTORESTATE) aus util/atomic.h denselben Schutz; Ein-Byte-Variablen wie bool und byte haben dieses Problem nicht.

ISR-Regeln: kurz halten, volatile nutzen, kein delay

  • Kurz und schnell halten: Während eine ISR läuft, warten alle anderen Interrupts. Am Uno arbeiten auch der millis()-Zähler und die serielle Schnittstelle mit Interrupts, daher bringt eine lange ISR das Timing durcheinander und kann eingehende serielle Daten verloren gehen lassen. Eine ideale ISR ist in wenigen Mikrosekunden fertig.
  • Kein delay(): delay() misst die Zeit mit Zählern, die der Timer0-Interrupt aktualisiert; in einer ISR sind die Interrupts aus, also funktioniert es nicht. millis() kannst du lesen, es zählt während der ISR aber nicht weiter, und micros() liefert nach 1–2 ms falsche Werte. Brauchst du unbedingt eine sehr kurze Wartezeit, funktioniert delayMicroseconds().
  • Kein Serial.print(): Auch das serielle Senden hängt an Interrupts; es verlängert die ISR und blockiert, wenn der Puffer voll ist. Überlass die Ausgabe loop().
  • Gemeinsame Variablen volatile machen: Jede Variable, die sowohl in der ISR als auch in loop() verwendet wird, muss volatile deklariert sein.
  • Mehrbyte-Variablen atomar lesen: Kopiere int-, long-, float- und unsigned-long-Variablen in loop() zwischen noInterrupts() und interrupts().
  • Keine Parameter, kein Rückgabewert: Eine ISR muss void sein und darf keine Parameter haben; Daten tauschst du über globale Variablen aus.
  • Keine schwere Arbeit in der ISR: Auf ein I2C-Display schreiben, auf eine SD-Karte speichern oder Bibliotheken wie Wire aufrufen, die selbst Interrupts brauchen, führt in einer ISR zum Hängenbleiben.
Das robusteste Muster lautet „Flag setzen, Arbeit in loop erledigen“: Die ISR setzt nur eine volatile bool-Variable auf true oder erhöht einen Zähler; Display aktualisieren, Motor ansteuern oder rechnen übernimmt loop(), sobald es das Flag sieht. Schreibst du loop() mit millis() statt delay, bleibt das ganze System schnell und zuverlässig.

Taster per Interrupt lesen und entprellen

Wenn du einen mechanischen Taster drückst, schließen die Metallkontakte nicht sofort sauber; sie öffnen und schließen sich etwa 0,1 bis 10 Millisekunden lang mehrmals. loop() ist dafür vielleicht zu langsam, ein Interrupt erfasst aber jede Flanke, daher kann ein einziger Druck 3–5 Interrupts erzeugen. Außerdem prellt ein Taster nicht nur beim Drücken, sondern auch beim Loslassen; Code, der nur FALLING mit einer Zeitsperre nutzt, kann deshalb doppelt zählen, wenn du den Taster länger hältst und dann loslässt. Der folgende Code nutzt den Modus CHANGE und akzeptiert einen Tastendruck nur, wenn der Pin LOW ist und die Leitung davor mindestens 50 ms ruhig war. So wird das Prellen beim Drücken und beim Loslassen herausgefiltert.

C++ (Arduino IDE)
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}

Neben der Software kannst du das Prellen auch in Hardware unterdrücken: Setzt du einen 100-nF-Kondensator parallel zum Taster und einen 10-kΩ-Pull-up-Widerstand an den Pin, ergibt sich die Zeitkonstante τ = R × C = 10 kΩ × 100 nF = 1 ms, die kurze Preller glättet; der Schmitt-Trigger an den Eingängen des Uno säubert die Flanke zusätzlich. Andere Werte berechnest du mit dem Tool RC-Zeitkonstante. Ehrlich gesagt brauchst du für Taster, die von Hand gedrückt werden, oft gar keinen Interrupt; ist loop() schnell, reicht Polling. Pull-up, Pull-down und Entprellen mit millis() erklärt der Artikel Taster am Arduino anschließen. Interrupts lohnen sich vor allem bei schnellen, kurzen Signalen.

Drehencoder per Interrupt auslesen

Ein Drehencoder (zum Beispiel der KY-040) erzeugt beim Drehen an den beiden Pins CLK und DT gegeneinander verschobene Rechtecksignale. Drehst du schnell, werden die Impulse kürzer als eine Millisekunde, und beim Polling gehen leicht Schritte verloren. Am einfachsten verbindest du CLK mit einem Interrupt-Pin und prüfst im Moment der fallenden CLK-Flanke den Pegel von DT: Ist DT HIGH, wurde in die eine Richtung gedreht, ist er LOW, in die andere. Diese Version zählt einen Schritt pro Rastung; ist die Richtung vertauscht, tausch die Zeilen mit ++ und --. Auch billige Encoder prellen, deshalb verbessert je ein 10–100-nF-Kondensator von CLK und DT nach GND das Ergebnis deutlich. Eine Version, die beide Pins per Interrupt liest und mit einer Zustandstabelle keinen Schritt verpasst, findest du in der Anleitung zum Drehencoder KY-040.

C++ (Arduino IDE)
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}

Umdrehungen zählen: Motordrehzahl (U/min) messen

Die Drehzahl eines Motors oder Rads zu messen, ist der Klassiker unter den Interrupt-Anwendungen. Als Sensor dient entweder ein optischer Gabellichtschranken-Drehzahlsensor mit LM393 und einer Encoderscheibe mit 20 Schlitzen oder ein Hall-Sensor wie der A3144 mit einem Magneten auf der Welle. Jeder Schlitz bzw. jeder Magnetdurchgang erzeugt einen Impuls. Aus der Zahl der Impulse pro Sekunde ergibt sich die Drehzahl: U/min = Impulse pro Sekunde × 60 / Impulse pro Umdrehung. Liefert eine Scheibe mit 20 Schlitzen zum Beispiel 100 Impulse pro Sekunde, dreht der Motor mit 100 × 60 / 20 = 300 U/min.

C++ (Arduino IDE)
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}

Hier kopiert loop() den Zähler jede Sekunde und setzt ihn zurück; beides geschieht im selben noInterrupts()-Block, damit kein Impuls verloren geht, falls zwischen den beiden Schritten ein Interrupt kommt. Mit 20 Schlitzen beträgt die Auflösung 3 U/min. Willst du bei niedrigen Drehzahlen genauere Werte, zähl keine Impulse, sondern miss in der ISR mit micros() die Zeit zwischen zwei Impulsen und rechne U/min = 60.000.000 / (Periode_µs × Impulse pro Umdrehung). Das gleiche Prinzip gilt für den Durchflusssensor YF-S201, die Geschwindigkeitsmessung mit einem Hall-Sensor und die Raddrehzahl eines DC-Getriebemotors.

Kurz zu Timer-Interrupts

Interrupts kommen nicht nur von Pins. Auch die Timer im Mikrocontroller können in festen Abständen Interrupts erzeugen, sodass eine Funktion exakt alle 1 ms oder alle 500 ms läuft, egal was loop() gerade tut. Der Uno hat drei Timer, und einige davon nutzt Arduino bereits für eigene Funktionen:

TimerBreiteGenutzt vonPWM-Pins
Timer08 Bitmillis(), delay(), micros()5, 6
Timer116 BitServo-Bibliothek, TimerOne9, 10
Timer28 Bittone(), IRremote3, 11

Am Uno ist die Bibliothek TimerOne der einfachste Weg: Timer1.initialize(500000) legt die Periode in Mikrosekunden fest, und Timer1.attachInterrupt(funktion) hängt die ISR an, die alle 500 ms läuft. Weil sie Timer1 belegt, kollidiert sie mit der Servo-Bibliothek und mit analogWrite() an den Pins 9 und 10. Beim ESP32 mit Arduino-Core 3.x richtest du einen Timer-Interrupt mit timerBegin(1000000), timerAttachInterrupt() und timerAlarm() ein; timerAlarmWrite() und timerAlarmEnable() aus 2.x wurden entfernt, deshalb scheitern alte Beispiele mit dem Fehler „was not declared in this scope“. Am Pico richtet MicroPython mit Timer(period=500, mode=Timer.PERIODIC, callback=funktion) in einer Zeile einen periodischen Interrupt ein. Wie Timer und PWM zusammenhängen, erfährst du im Artikel Was ist PWM?

Interrupts am ESP32: IRAM_ATTR und portMUX

Beim ESP32 musst du keinen besonderen Pin suchen, jeder GPIO kann einen Interrupt auslösen. Nur GPIO 34–39 haben keinen internen Pull-up, ein Taster braucht dort einen externen 10-kΩ-Pull-up; außerdem vermerkt die Errata-Liste von Espressif, dass GPIO 36 und 39 bei aktivem ADC falsche Interrupts erzeugen können. Der eigentliche Unterschied beim ESP32 ist das Attribut IRAM_ATTR vor der ISR. Es legt die Funktion statt in den Flash in den schnellen internen RAM (IRAM). Beim Schreiben in den Flash (Preferences, SPIFFS, OTA) wird der Cache abgeschaltet; je nach Konfiguration kann ein Interrupt, der in diesem Moment auf Code im Flash zugreift, das Board mit „Guru Meditation Error: Core 1 panic'ed (Cache disabled but cached memory region accessed)“ neu starten lassen. Der ESP8266 ist noch strenger: Eine ISR ohne IRAM_ATTR stoppt das Board schon beim Aufruf von attachInterrupt mit der Meldung „ISR not in IRAM!“.

C++ (Arduino IDE)
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}

Weil auf dem ESP32 FreeRTOS läuft und er zwei Kerne hat, schützt man gemeinsame Variablen statt mit noInterrupts() wie beim Uno mit einer portMUX_TYPE-Sperre: in der ISR mit portENTER_CRITICAL_ISR(), in loop() mit portENTER_CRITICAL(). Eine 32-Bit-Variable zu lesen ist beim ESP32 ohnehin ein einziger Schritt, aber beim Lesen und Zurücksetzen oder beim gemeinsamen Ändern mehrerer Variablen ist die Sperre Pflicht. Willst du dieselbe Funktion an mehrere Pins hängen, kannst du mit attachInterruptArg() der ISR einen Parameter wie die Pin-Nummer übergeben. Grundlagen findest du in Was ist der ESP32?, Tasks und Kerne in ESP32 FreeRTOS und Dual-Core.

Interrupts am Raspberry Pi Pico: MicroPython irq

Am Raspberry Pi Pico kann jeder Pin von GP0 bis GP28 einen Interrupt auslösen. In MicroPython richtest du ihn mit der Methode irq() des Pin-Objekts ein: Für trigger übergibst du Pin.IRQ_FALLING, Pin.IRQ_RISING oder beides kombiniert, und handler ist die Funktion, die beim Interrupt läuft; sie erhält den auslösenden Pin als Parameter. Python kennt kein volatile; es reicht, die in der ISR geänderten Variablen als global zu deklarieren. Zum Messen der Zeit nutzt du time.ticks_ms() und das überlaufsichere time.ticks_diff(). Der folgende Code überträgt die Ruhezeit-Logik aus dem Arduino-Beispiel auf den Pico:

MicroPython
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)

Am Pico läuft irq standardmäßig als Soft-Interrupt: MicroPython reiht die Funktion ein und führt sie so schnell wie möglich aus, daher macht print oder das Anlegen einer Liste darin keine Probleme, die Latenz steigt aber etwas. Mit hard=True sinkt die Latenz auf Mikrosekunden; dafür darfst du in der ISR keinen Speicher anfordern, sodass schon eine neue Liste, ein String oder sogar eine float-Berechnung einen „MemoryError“ auslösen kann. Programmierst du den Pico mit der Arduino IDE (Core von Earle Philhower), funktioniert attachInterrupt() an jedem GPIO genauso. Zur Einrichtung lies Was ist der Raspberry Pi Pico?, zu den Grundbefehlen MicroPython-Grundlagen.

Projektideen mit Interrupts

  • Reaktionstester: Miss mit micros() in der ISR auf Bruchteile einer Millisekunde genau, wie schnell du nach dem Aufleuchten einer LED den Taster drückst.
  • Fahrradtacho: Bring einen Magneten am Rad und einen Hall-Sensor am Rahmen an und berechne aus der Zeit pro Umdrehung Geschwindigkeit und Strecke.
  • Wasserzähler: Zähl die Impulse eines Durchflusssensors YF-S201 und zeig die verbrauchten Liter an.
  • Geradeaus fahrendes Roboterauto: Miss mit Radencodern die Drehzahl beider Motoren und gleiche sie an.
  • Menüsteuerung: Navigiere mit einem Drehencoder durch ein Display-Menü und ändere Einstellungen.
  • Batteriebetriebene Alarmanlage: Lass den Ausgang eines PIR-Sensors das schlafende Board wecken; für den ESP32 sieh dir den Artikel zum Deep Sleep an.

Häufige Fehler und ihre Lösungen

SymptomUrsacheLösung
Der Interrupt löst nie ausattachInterrupt(2, ...) wurde eine Pin-Nummer übergeben oder ein Pin ohne Interrupt-Unterstützung gewähltdigitalPinToInterrupt(pin) verwenden; am Uno D2 oder D3 nutzen
Ein Druck erhöht den Zähler um 2–5KontaktprellenIn Software entprellen und/oder einen 100-nF-Kondensator ergänzen
Die Variable ändert sich in loop() nievolatile vergessenDie gemeinsame Variable volatile deklarieren
Ab und zu absurd große WerteMehrbyte-Variable nicht atomar gelesenZwischen noInterrupts() und interrupts() kopieren
Das Programm hängt oder millis() läuft wegdelay(), Serial oder lange Aufgaben in der ISRIn der ISR nur ein Flag oder einen Zähler ändern
Interrupts ohne Berührung des TastersPin hängt offen (floating), kein Pull-upINPUT_PULLUP oder einen externen 10-kΩ-Widerstand verwenden
Die ISR läuft nach erneutem attachInterrupt sofort einmalEine Flanke bei abgeschaltetem Interrupt hat das Flag gesetzt (AVR)Am Uno vor attachInterrupt EIFR = bit(INTF0); schreiben (INTF1 für D3)
Der ESP32 startet zufällig neuDie ISR liegt nicht im IRAM oder macht schwere ArbeitIRAM_ATTR ergänzen und die ISR kürzen

Interrupts sind ein mächtiges Werkzeug, mit dem du jedes Ereignis erfasst, selbst wenn loop() gerade mit anderem beschäftigt ist; du musst dir nur drei Regeln merken: ISR kurz halten, gemeinsame Variablen volatile machen und sie in loop() atomar lesen. In der BlueGrays-App findest du Tasterzähler, einen Reaktionstest, ESP32-Tasterprojekte und ein Pico-Projekt, das Entprellen per Interrupt (IRQ) und per Software-Polling vergleicht, jeweils mit Schaltplan und Code Schritt für Schritt.

Häufige Fragen

Welche Pins unterstützen Interrupts am Arduino Uno?

Arduino Uno R3, Nano und Pro Mini haben nur zwei Pins für externe Interrupts: D2 (INT0) und D3 (INT1). Beim Mega 2560 sind es 2, 3, 18, 19, 20 und 21, beim Leonardo 0, 1, 2, 3 und 7. An den übrigen Pins stehen nur Pin-Change-Interrupts (PCINT) zur Verfügung, die lediglich eine Änderung melden.

Warum funktioniert delay() in einer ISR nicht?

delay() misst die Zeit mit Zählern, die vom Timer0-Interrupt aktualisiert werden. Während eine ISR läuft, warten die anderen Interrupts, deshalb zählen diese Zähler nicht weiter und delay() arbeitet nicht korrekt. Muss etwas warten, setz in der ISR nur ein Flag und erledige das Warten in loop() mit millis().

Wofür ist volatile da und wann brauche ich es?

volatile sagt dem Compiler, dass sich die Variable jederzeit außerhalb des normalen Programmablaufs ändern kann, etwa in einer ISR; ihr Wert wird daher nicht in einem Register zwischengespeichert, sondern jedes Mal aus dem Speicher gelesen. Jede Variable, die in einer ISR und in loop() genutzt wird, sollte volatile sein. Mehrbyte-Zugriffe macht volatile aber nicht atomar, dafür brauchst du noInterrupts().

Kann ich am Arduino Uno mehr als 2 Interrupts nutzen?

Ja. Dank der Pin-Change-Interrupts (PCINT) kann fast jeder Pin des Uno einen Interrupt auslösen. Sie melden nur eine Änderung, und die Pins eines Ports (D0–D7, D8–D13, A0–A5) teilen sich eine ISR. Mit den Bibliotheken PinChangeInterrupt oder EnableInterrupt funktioniert das fast wie mit attachInterrupt.

Interrupt oder Polling: Was soll ich verwenden?

Für Taster, die von Hand gedrückt werden, und langsam veränderliche Sensoren reicht Polling in loop() meist aus und ist einfacher. Interrupts solltest du bei schnellen, kurzen Impulsen wie von Encodern, Drehzahl- und Durchflusssensoren verwenden oder um das Board aus dem Schlaf zu wecken. Stecken in loop() lange delay()-Aufrufe, löst es oft schon viele Probleme, sie durch millis() zu ersetzen.

Was passiert, wenn ich beim ESP32 IRAM_ATTR weglasse?

Der Code kompiliert meist und scheint zu funktionieren, aber die ISR bleibt im Flash, startet langsamer und kann je nach Konfiguration beim Schreiben in den Flash mit „Cache disabled but cached memory region accessed“ abstürzen. Beim ESP8266 bleibt das Board ohne IRAM_ATTR sofort mit „ISR not in IRAM!“ stehen. Schreib deshalb bei ESP-Boards vor jede ISR IRAM_ATTR.

Alle Projekte ansehen
LED mit Taster ein- und ausschalten
#10Anfänger

LED mit Taster ein- und ausschalten

Einmal drücken und das Licht geht an, noch einmal drücken und es geht aus, genau wie der Lichtschalter in deinem Zimmer. Der Arduino bemerkt jeden Druck und schaltet die LED in den jeweils anderen Zus

4.8(100)
167
Raspberry Pi Pico Taster Debounce: IRQ vs Software Polling
#159Mittel
Premium

Raspberry Pi Pico Taster Debounce: IRQ vs Software Polling

Einmal Taster gedrückt, Code zählt 3-4? Das ist Contact Bounce! Vergleiche 2 Zählmethoden in Serial: RAW Polling (fängt Bounce, zählt falsch) vs IRQ + Software Debounce (genau). Lerne Contact Bounce Physik, Software Debounce, Pin.irq() Interrupt, IRQ_FALLING Trigger, ISR Handler, global Variablen ISR-Sicherheit.

0.0(0)
12
ESP32 LED Steuerung mit Taster (Toggle)
#105Anfänger

ESP32 LED Steuerung mit Taster (Toggle)

Jedes Mal, wenn die Taste gedrückt wird, ändert sich der Zustand der LED: ist sie an, schaltet sie aus, ist sie aus, schaltet sie ein. Klassisches Toggle-Muster. Mit der Entprellung wird das mechanische Geräusch des Tasters beseitigt, um ein einmaliges Umschalten mit einem einzigen Druck zu gewährleisten.

4.6(39)
57
Raspberry Pi Pico Taster Toggle LED
#156Anfänger

Raspberry Pi Pico Taster Toggle LED

Taster + LED — nächste Stufe! Anders als beim einfachen Taster-Projekt musst du den Taster nicht halten. Drücken und loslassen → LED an. Nochmal drücken → LED aus (toggle). Gleiche Schaltung, aber großer Codeunterschied. Lerne Edge Detection, State Tracking und Debounce.

0.0(0)
9
Zähler mit 7-Segment-Anzeige und Tastern
#43Fortgeschritten
Premium

Zähler mit 7-Segment-Anzeige und Tastern

Ein einfacher Zähler mit drei Knöpfen: Mit dem einen zählt die Ziffer auf der Anzeige um eins hoch, mit dem anderen um eins runter, und der dritte setzt sie auf null zurück. Die einstellige 7-Segment-

4.6(35)
47
Arduino Reaktionstest mit Tonsignal: Miss deine Reflexe in Millisekunden (LCD + Buzzer + 3 LEDs)
#97Mittel
Premium

Arduino Reaktionstest mit Tonsignal: Miss deine Reflexe in Millisekunden (LCD + Buzzer + 3 LEDs)

Wie schnell sind deine Reflexe? Drück den Taster: Die rote LED heißt Auf die Plätze, die gelbe Fertig, dann beginnt eine zufällige Wartezeit von 1,5 bis 3,5 Sekunden. Sobald die grüne LED leuchtet und der Buzzer piept, drückst du erneut; deine Reaktionszeit erscheint in Millisekunden auf dem I2C-LCD, darunter Sitzungsrekord und Durchschnitt. Wer vor Grün drückt, sieht TOO EARLY! und hört einen Verliererton. Gedrückt halten startet einen neuen Spieler mit derselben Wartefolge.

4.6(33)
50
ESP32-WebSocket-Zähler (2 Tasten + LCD + Web-Echtzeit)
#130Fortgeschritten
Premium

ESP32-WebSocket-Zähler (2 Tasten + LCD + Web-Echtzeit)

In diesem Projekt ändern zwei am ESP32 angeschlossene Taster (GPIO 5 zählt hoch, GPIO 4 zählt herunter) einen Zähler. Der Wert wird sofort auf einem 16x2-I2C-LCD und über die WebSocket-Unterstützung von ESPAsyncWebServer in allen verbundenen Browsern aktualisiert, ohne die Seite neu zu laden. Statt HTTP-Polling wird eine einzige dauerhafte Verbindung genutzt; der ESP32 sendet den neuen Wert mit ws.textAll() an alle.

4.5(14)
23
Arduino Gedächtnisspiel (4 LEDs + 4 Taster + Summer)
#93Fortgeschritten
Premium

Arduino Gedächtnisspiel (4 LEDs + 4 Taster + Summer)

Vertraust du deinem Gedächtnis? Vier bunte LEDs leuchten nacheinander auf, jede mit ihrem eigenen Ton, und du wiederholst die Reihenfolge mit den Tastern. Jede richtige Runde fügt der Folge eine neue

4.8(46)
69