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
  1. 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.

  2. 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() { ... }`

  3. 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.

  4. 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.

  5. 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.

Ähnliche Themen