Interrupts statt Polling
Hard · 40 min · Kostenlos
Ziel & Lerninhalt
Verstehe den fundamentalen Unterschied zwischen Polling (ständiges Nachfragen) und Interrupts (Benachrichtigung beim Ereignis).
- attachInterrupt() und ISR korrekt schreiben.
- Volatile-Variablen und Prellfilter in der ISR verstehen.
- Polling vs. Interrupt an einem Zähler-Experiment vergleichen.
Erklärung & Wissen
Basic
Polling ist wie ein Kind, das alle 5 Sekunden fragt: 'Sind wir schon da?' Interrupt ist die Mutter, die von selbst sagt: 'Jetzt!' – wenn es so weit ist. Beim Polling prüft der Controller ständig in der loop(), ob ein Pin HIGH oder LOW ist. Das blockiert Zeit und kann schnelle Ereignisse verpassen. Beim Interrupt reagiert die Hardware sofort, unterbricht den laufenden Code, führt eine kurze ISR (Interrupt Service Routine) aus und macht dann weiter. Ergebnis: Kein Ereignis geht verloren, die loop() bleibt frei.
Medium
In Arduino: `attachInterrupt(digitalPinToInterrupt(2), meinISR, RISING)` – registriert die Funktion meinISR, die bei steigender Flanke an Pin 2 aufgerufen wird. Interrupt-fähige Pins: Auf dem ATmega328 nur D2 (INT0) und D3 (INT1). Die ISR muss kurz sein: kein delay(), kein Serial.print(). Variablen, die in ISR verändert werden, müssen als `volatile` deklariert sein – sonst optimiert der Compiler ihren Zugriff weg. Prellfilter in der ISR: `if (millis() - letzterIRQ < 50) return;`
Expert
Interrupts unterbrechen die Hauptroutine an einer beliebigen Stelle – der aktuelle Zustand (Programmzähler, Register) wird auf den Stack gesichert (Context Save). Nach der ISR wird dieser Zustand wiederhergestellt (Context Restore). Das kostet auf ATmega ~25–70 Taktzyklen Overhead. Wichtig: ISRs teilen sich den Stack mit dem Hauptprogramm – eine zu große ISR mit vielen lokalen Variablen kann den Stack zerstören. In zeitkritischen Systemen (PID-Regler, PWM-Messung) nutzt man Interrupts für den Timer-gesteuerten Regleraufruf (Timer-Interrupt statt delay()).
Fun Fact
Der erste Computer, der Hardware-Interrupts unterstützte, war der IBM 7090 aus dem Jahr 1959. Davor liefen alle Programme rein sequenziell – Peripheriegeräte wurden ausschließlich per Polling bedient, was riesige CPU-Ressourcen verschwendete.
- ISR (Interrupt Service Routine)
- Die kurze Funktion, die automatisch beim Eintreten des Interrupts ausgeführt wird. ISR-Regeln: Kein delay(), kein Serial.print(), kein millis() (letzteres ist selbst Interrupt-basiert). So kurz wie möglich halten: nur ein Flag setzen oder Zähler inkrementieren. Die eigentliche Verarbeitung passiert dann in der loop().
- Volatile
- Schlüsselwort, das den Compiler zwingt, eine Variable immer aus dem RAM zu lesen statt aus dem Cache. Ohne `volatile`: Compiler speichert die Variable in einem CPU-Register und liest nie wieder aus RAM. Ändert die ISR die Variable im RAM, sieht die loop() trotzdem den alten Wert. Mit `volatile`: Jeder Zugriff geht direkt in den RAM – langsamer, aber korrekt.
- Flankendetektion
- Erkennen des Moments, in dem ein Signal von LOW auf HIGH (oder umgekehrt) wechselt. Arduino-Modi: RISING (LOW→HIGH), FALLING (HIGH→LOW), CHANGE (beides), LOW (dauerhaft LOW, nicht empfohlen). Bei Tastern: FALLING + INPUT_PULLUP ist oft sinnvoller als RISING, weil beim Drücken der Pin auf GND zieht.
- Prellfilter
- Software-Lösung gegen mehrfache Interrupt-Auslösung durch mechanisches Tastenprellen. Mechanische Kontakte prellen beim Schließen 5–50 ms. Ohne Filter: ein Tastendruck löst 20+ Interrupts aus. Softwarefilter in der ISR: `if (millis() - letzterIRQ < 50) return;` – Interrupts innerhalb 50 ms nach dem letzten werden ignoriert. Alternative: Hardware-Kondensator (100 nF) von Pin nach GND.
Praktischer Versuch: Ereigniszähler – Polling vs. Interrupt
Zähle Büroklammer-Tastendrücke einmal per Polling und einmal per Interrupt. Vergleiche, welche Methode schnelle Drücke besser erkennt.
- Büroklammer als Taster (an D2)
- Stück Alufolie als Kontaktfläche
Polling-Zähler bauen
Schreibe loop() mit Polling: `if (!digitalRead(2)) { zaehler++; delay(100); }`. Drücke schnell hintereinander. Beobachte, wie viele Drücke verloren gehen.
Check: Monitor zeigt 'Zähler: X' und erhöht sich bei Tastendruck – manchmal werden schnelle Drücke nicht erkannt.
Tipp: Delay(100) im loop() macht das Polling langsam – 100 ms Blindzeit zwischen Abfragen.
ISR schreiben
Schreibe außerhalb aller Funktionen: `volatile int irqZaehler = 0;` und `void IRAM_ATTR taste_ISR() { if (millis() - letzterIRQ < 50) return; irqZaehler++; letzterIRQ = millis(); }`
Check: ISR kompiliert fehlerfrei und enthält nur einen Zähler-Inkrement.
Tipp: ISR-Funktion hat keine Parameter und gibt nichts zurück: `void meinISR() { ... }`
attachInterrupt() einrichten
In setup(): `pinMode(2, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(2), taste_ISR, FALLING);` Beachte: FALLING, weil der Pin mit INPUT_PULLUP im Ruhezustand HIGH ist.
Check: Im Monitor erscheint die IRQ-Zahl nach kurzem Drücken korrekt.
Tipp: digitalPinToInterrupt(2) gibt die interne Interrupt-Nummer für Pin 2 zurück.
Vergleich: schnelle Drücke
Drücke 10 Mal schnell. Schreibe beide Zählerstände auf. Der IRQ-Zähler sollte mehr Ereignisse erfasst haben als der Polling-Zähler.
Check: Interrupt-Zähler ist höher als Polling-Zähler beim schnellen Drücken.
Tipp: Drücke mindestens 10 Mal schnell hintereinander und vergleiche beide Zählerstände.
loop() frei halten
Füge in loop() eine Servo-Pendelbewegung hinzu (langsam hin und her). Beobachte: Auch schnelles Drücken stört die Servo-Bewegung nicht mehr – Interrupt arbeitet im Hintergrund.
Check: Servo dreht weiter, obwohl schnell gedrückt wird – kein Hickup.
Tipp: Der Servo-Code in loop() wird nicht durch die ISR blockiert – das ist der Hauptvorteil.
Büroklammer-Taster an D2 (INPUT_PULLUP, FALLING-Interrupt). Servo optional an D9.
// ProtoPeaks - Interrupts vs. Polling
#include <Servo.h>
Servo servo;
const int TASTER_PIN = 2;
volatile int irqZaehler = 0;
volatile unsigned long letzterIRQ = 0;
int pollingZaehler = 0;
bool letzterStatus = HIGH;
void taste_ISR() {
if (millis() - letzterIRQ < 50) return;
irqZaehler++;
letzterIRQ = millis();
}
void setup() {
Serial.begin(9600);
pinMode(TASTER_PIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(TASTER_PIN), taste_ISR, FALLING);
servo.attach(9);
}
void loop() {
// Polling
bool aktuell = digitalRead(TASTER_PIN);
if (aktuell == LOW && letzterStatus == HIGH) {
pollingZaehler++;
}
letzterStatus = aktuell;
Serial.print("Polling="); Serial.print(pollingZaehler);
Serial.print("\tIRQ="); Serial.println(irqZaehler);
// Servo laeuft weiter - wird nicht blockiert
int pos = (millis() / 10) % 180;
servo.write(pos);
delay(10);
}- IRQ-Zähler springt sofort auf 50+ ohne Drücken. — Büroklammer prellt. Prellzeit im Filter erhöhen: von 50 ms auf 100 ms.
- Interrupt wird gar nicht ausgelöst. — Pin 2 prüfen. attachInterrupt() muss mit FALLING und INPUT_PULLUP kombiniert werden. Kontrolle: digitalRead(2) im Monitor anzeigen lassen.
Kurzzusammenfassung
Interrupts reagieren sofort auf Ereignisse ohne loop()-Blockierung. ISR kurz halten, volatile verwenden, Prellfilter einbauen. Der Vergleich Polling vs. Interrupt macht den Vorteil unmittelbar sichtbar: Interrupts verpassen nichts.