Arduino Interrupts verstehen: attachInterrupt, ISR & Entprellen – nie wieder verpasste Signale
Dein Arduino soll auf einen Tastendruck reagieren – aber genau in dem Moment steckt er in einem delay() oder liest gerade einen Sensor aus, und der Druck verpufft ungehört. Die Lösung heißt Interrupts: Der Mikrocontroller unterbricht seine aktuelle Arbeit in Mikrosekunden, führt eine kurze Reaktions-Funktion aus und macht danach nahtlos weiter. In diesem Tutorial lernst du attachInterrupt() von Grund auf: Was eine ISR ist, warum volatile kein optionales Schmuckwort ist, wie du prellende Signale sauber entprellst und wie du Touch-Sensor, PIR-Bewegungsmelder und Rotary Encoder interrupt-gesteuert auswertest – auf dem Arduino UNO genauso wie auf dem ESP32.
Warum Polling nicht reicht – und Interrupts die Rettung sind
Der klassische Anfänger-Ansatz prüft Eingänge im loop(): if (digitalRead(PIN) == HIGH) ... – das nennt man Polling. Das funktioniert, solange dein loop() schnell durchläuft. Sobald aber ein delay(1000), eine langsame Sensor-Abfrage oder eine Display-Aktualisierung dazwischenfunkt, ist dein Programm für diese Zeit blind: Ein kurzer Impuls von 50 Millisekunden geht schlicht verloren.
Ein Interrupt dreht das Prinzip um: Die Hardware selbst überwacht den Pin. Ändert sich der Pegel, unterbricht der Controller sofort das laufende Programm, springt in deine ISR (Interrupt Service Routine), arbeitet sie ab und kehrt exakt an die unterbrochene Stelle zurück. Das Ganze dauert nur wenige Mikrosekunden.
- Nichts geht verloren: Auch kürzeste Impulse werden erfasst – egal was der loop() gerade treibt
- Sofortige Reaktion: Reaktionszeit im Mikrosekunden-Bereich statt „irgendwann im nächsten loop-Durchlauf“
- Sauberer Code: Ereignis-Logik wandert aus dem loop() in eine eigene kleine Funktion
- Strom sparen: Interrupts können den Controller sogar aus Schlafmodi aufwecken – die Basis von ESP32 Deep Sleep
ℹ️ Interrupts ersetzen millis() nicht: Für zeitgesteuerte Abläufe bleibt das millis()-Muster das Mittel der Wahl. Interrupts sind für externe Ereignisse da, die du nicht verpassen darfst. Die Kombination aus beidem macht deine Sketches wirklich reaktionsschnell.
Das brauchst du
Welche Pins können Interrupts?
Nicht jeder Pin eignet sich auf jedem Board für externe Interrupts – das ist der häufigste Stolperstein überhaupt:
| Board | Interrupt-fähige Pins | Anzahl |
|---|---|---|
| Arduino UNO R3 | D2, D3 | 2 |
| Arduino Nano V3 | D2, D3 | 2 |
| ESP32 | praktisch alle GPIOs | ~30 |
| ESP8266 (Wemos D1 Mini) | alle außer GPIO 16 | ~8 |
Damit dein Sketch auf allen Boards läuft, übersetzt du die Pin-Nummer immer mit digitalPinToInterrupt(pin) – niemals die rohe Interrupt-Nummer eintragen. Als Modus stehen zur Wahl:
-
RISING– löst bei steigender Flanke aus (LOW → HIGH) -
FALLING– löst bei fallender Flanke aus (HIGH → LOW) -
CHANGE– löst bei jeder Pegeländerung aus -
LOW– löst dauerhaft aus, solange der Pin LOW ist (selten sinnvoll)
1Dein erster Interrupt: Touch-Zähler mit dem TTP223
Der TTP223 Touch-Sensor ist der perfekte Einstieg, weil er ein digital sauberes Signal liefert – kein Prellen, kein Rauschen. Verkabelung: VCC an 5V, GND an GND, Signal (I/O) an D2. Der Sketch zählt Berührungen, während der loop() demonstrativ mit einem trägen delay() beschäftigt ist:
const int TOUCH_PIN = 2; // D2 = interrupt-faehig am UNO
volatile unsigned int beruehrungen = 0; // volatile ist PFLICHT!
void beruehrungErkannt() {
beruehrungen++; // ISR: so kurz wie moeglich
}
void setup() {
Serial.begin(115200);
pinMode(TOUCH_PIN, INPUT);
attachInterrupt(digitalPinToInterrupt(TOUCH_PIN),
beruehrungErkannt, RISING);
Serial.println("Touch-Zaehler bereit - beruehre den Sensor!");
}
void loop() {
delay(2000); // absichtlich traege - Interrupt kommt trotzdem durch
Serial.print("Beruehrungen bisher: ");
Serial.println(beruehrungen);
}
Obwohl der loop() zwei Sekunden schläft, wird keine einzige Berührung verpasst – genau das ist die Magie der Interrupts.
⚠️ Warum volatile? Der Compiler optimiert Variablen gern in CPU-Register, wenn er glaubt, dass sie sich „von allein“ nicht ändern. Eine ISR ändert die Variable aber außerhalb des normalen Programmflusses. volatile zwingt den Compiler, die Variable bei jedem Zugriff frisch aus dem RAM zu lesen. Ohne volatile zeigt dein loop() unter Umständen ewig den alten Wert an – ein Fehler, der beim Testen oft zufällig nicht auftritt und später umso mysteriöser zuschlägt.
2Die goldenen Regeln für ISRs
Eine ISR unterbricht dein Programm mitten im Befehl. Damit dabei nichts schiefgeht, gelten eiserne Regeln:
- Kurz halten: Nur Flags setzen, Zähler erhöhen, Zeitstempel merken. Die eigentliche Arbeit (Display, Berechnungen) erledigt der loop()
- Kein Serial.print() in der ISR: Serial nutzt selbst Interrupts – im schlimmsten Fall friert dein Sketch ein
- Kein delay() und kein millis()-Warten: delay() steht in der ISR komplett still; millis() zählt während einer ISR nicht weiter
-
Geteilte Variablen als volatile deklarieren – und bei Variablen größer als 1 Byte (int, long, float) den Zugriff im loop() mit
noInterrupts()/interrupts()schützen
So sieht der sichere Zugriff auf einen mehrbyteigen Zähler aus:
volatile unsigned long impulse = 0;
void loop() {
unsigned long kopie;
noInterrupts(); // Interrupts ganz kurz anhalten ...
kopie = impulse; // ... Wert atomar kopieren ...
interrupts(); // ... und sofort wieder freigeben
Serial.println(kopie); // mit der Kopie arbeiten
delay(1000);
}
ℹ️ Hintergrund: Ein 8-Bit-AVR wie der ATmega328P liest einen unsigned long in vier einzelnen Byte-Zugriffen. Funkt die ISR genau dazwischen, bekommst du eine wilde Mischung aus altem und neuem Wert – die berüchtigte Race Condition. Die zwei Zeilen noInterrupts()/interrupts() kosten praktisch nichts und ersparen dir stundenlange Fehlersuche.
3Entprellen: Wenn ein Tastendruck zehnmal zählt
Mechanische Taster und Schalter prellen: Beim Schließen federn die Kontakte einige Millisekunden lang, und aus einem Druck werden fünf bis zwanzig Flanken – dein Interrupt-Zähler explodiert. Die Lösung ist eine Zeitsperre direkt in der ISR: Auslösungen, die weniger als ~200 ms nach der letzten kommen, werden ignoriert:
const int TASTER_PIN = 3; // zweiter Interrupt-Pin am UNO
const unsigned long PRELL_SPERRE = 200; // ms - je nach Taster 50-250
volatile unsigned int druecke = 0;
volatile unsigned long letzteAusloesung = 0;
void tasterGedrueckt() {
unsigned long jetzt = millis(); // millis LESEN ist in ISR ok
if (jetzt - letzteAusloesung > PRELL_SPERRE) {
druecke++;
letzteAusloesung = jetzt;
}
}
void setup() {
Serial.begin(115200);
pinMode(TASTER_PIN, INPUT_PULLUP); // interner Pull-up: Taster gegen GND
attachInterrupt(digitalPinToInterrupt(TASTER_PIN),
tasterGedrueckt, FALLING);
}
void loop() {
static unsigned int letzterStand = 0;
unsigned int kopie;
noInterrupts();
kopie = druecke;
interrupts();
if (kopie != letzterStand) {
letzterStand = kopie;
Serial.print("Tastendruck Nr. ");
Serial.println(kopie);
}
}
Dank INPUT_PULLUP brauchst du keinen externen Widerstand: Der Pin liegt in Ruhe auf HIGH, der Taster zieht ihn beim Drücken auf GND – deshalb lauscht der Interrupt auf FALLING.
4Praxis: PIR-Alarm und Rotary Encoder per Interrupt
Bewegungsmelder ohne Polling
Der HC-SR501 hält seinen Ausgang bei Bewegung für einige Sekunden auf HIGH – perfekt für RISING. Die ISR setzt nur ein Flag, der loop() erledigt die Reaktion:
const int PIR_PIN = 2;
const int LED_PIN = 13;
volatile bool bewegungErkannt = false;
void pirAusgeloest() {
bewegungErkannt = true; // nur das Flag setzen - mehr nicht!
}
void setup() {
Serial.begin(115200);
pinMode(PIR_PIN, INPUT);
pinMode(LED_PIN, OUTPUT);
attachInterrupt(digitalPinToInterrupt(PIR_PIN), pirAusgeloest, RISING);
Serial.println("PIR-Waechter aktiv (60 s Aufwaermzeit beachten)");
}
void loop() {
if (bewegungErkannt) {
bewegungErkannt = false; // Flag zuruecksetzen
digitalWrite(LED_PIN, HIGH);
Serial.println("Bewegung erkannt!");
// hier: Buzzer, Nachricht, Foto ausloesen ...
}
if (digitalRead(PIR_PIN) == LOW) {
digitalWrite(LED_PIN, LOW); // PIR wieder in Ruhe -> LED aus
}
}
Wie du daraus eine komplette Alarmanlage mit ESP32 baust, zeigt das PIR-Alarmanlagen-Tutorial.
Rotary Encoder: das Interrupt-Paradebeispiel
Beim schnellen Drehen eines Rotary Encoders feuern die Impulse im Millisekunden-Takt – mit Polling verlierst du Schritte, die Drehrichtung springt. Mit einem Interrupt auf CLK sitzt jeder Schritt (CLK an D2, DT an D3, VCC an 5V, GND an GND):
const int CLK_PIN = 2; // Interrupt-Pin
const int DT_PIN = 3;
volatile int position = 0;
void encoderDreht() {
// Drehrichtung: Zustand von DT im Moment der CLK-Flanke
if (digitalRead(DT_PIN) != digitalRead(CLK_PIN)) {
position++; // im Uhrzeigersinn
} else {
position--; // gegen den Uhrzeigersinn
}
}
void setup() {
Serial.begin(115200);
pinMode(CLK_PIN, INPUT_PULLUP);
pinMode(DT_PIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(CLK_PIN), encoderDreht, CHANGE);
}
void loop() {
static int letztePosition = 0;
int kopie;
noInterrupts();
kopie = position;
interrupts();
if (kopie != letztePosition) {
letztePosition = kopie;
Serial.print("Position: ");
Serial.println(kopie);
}
}
Auf dieser Basis baust du komfortable Bedienoberflächen – etwa das OLED-Menüsystem mit Rotary Encoder. Alle Details zum Encoder selbst stehen im KY-040 Grundlagen-Tutorial.
💡 ESP32-Besonderheit: Auf dem ESP32 markierst du ISRs zusätzlich mit void IRAM_ATTR meineISR() { ... }. Das legt die Funktion ins schnelle interne RAM statt in den Flash – sonst drohen sporadische Abstürze, wenn der Flash gerade durch WLAN-Aktivität blockiert ist. Ansonsten funktioniert attachInterrupt() identisch, nur eben auf fast jedem GPIO.
Troubleshooting
⚠️ attachInterrupt tut gar nichts: Falscher Pin! Am UNO/Nano funktionieren nur D2 und D3. Und immer digitalPinToInterrupt(pin) verwenden – wer die Pin-Nummer direkt einträgt, erwischt am UNO zufällig die richtige Nummer und wundert sich dann auf jedem anderen Board.
⚠️ Ein Tastendruck zählt mehrfach: Klassisches Prellen. Zeitsperre wie in Schritt 3 einbauen oder gleich einen prellfreien TTP223 Touch-Sensor verwenden.
⚠️ Sketch friert ein oder stürzt ab: Serial.print(), delay() oder lange Berechnungen in der ISR. Alles außer Flag setzen und Zähler erhöhen gehört in den loop().
⚠️ Werte im loop() sind Unsinn: volatile vergessen – oder bei int/long den Kopier-Trick mit noInterrupts()/interrupts() ausgelassen.
⚠️ ESP32 stürzt mit „Guru Meditation Error“ ab: Das IRAM_ATTR vor der ISR fehlt, oder in der ISR wird auf Flash-Funktionen zugegriffen.
⚠️ PIR löst ständig aus: Der HC-SR501 braucht nach dem Einschalten ~60 Sekunden Aufwärmzeit, und WLAN-Boards direkt daneben können ihn stören – etwas Abstand schafft Ruhe.
🚀 Dein Interrupt-Experimentierpaket
Alle Bauteile für dieses Tutorial bekommst du im Shop:
- UNO R3 Entwicklungsboard (USB-C) – zwei Interrupt-Pins für deine ersten Versuche
- TTP223 Touch-Sensor im 10er-Pack – prellfreie Signale für saubere Interrupts
- HC-SR501 PIR Bewegungsmelder – Bewegung erkennen ganz ohne Polling
- Rotary Encoder EC11 Modul – kein verlorener Drehimpuls mehr
- ESP32 Entwicklungsboard (USB-C) – Interrupts auf fast jedem GPIO
- Breadboard 400 Kontakte – für den fliegenden Aufbau
Fazit
Interrupts sind der Schritt vom „Sketch, der zufällig reagiert“ zum „Programm, das garantiert reagiert“. Du weißt jetzt, welche Pins am UNO interrupt-fähig sind, wie attachInterrupt() mit RISING, FALLING und CHANGE arbeitet, warum volatile und der atomare Kopier-Trick keine Kür, sondern Pflicht sind, und wie du prellende Taster mit einer simplen Zeitsperre zähmst.
Die Merkregel für jede ISR: rein, Flag setzen, raus. Alles Weitere – Displays, Berechnungen, Nachrichten – erledigt der loop() in Ruhe. Mit diesem Muster laufen Touch-Bedienung, Bewegungsmelder und Rotary Encoder parallel und verlustfrei, während dein Hauptprogramm sich um die eigentliche Aufgabe kümmert. Viel Spaß beim Unterbrechen!