Die Arduino-Plattform hat unzähligen Bastlern, Studenten und Ingenieuren die Türen zur Welt der Mikrocontroller geöffnet. Eines der grundlegendsten, aber oft missverstandenen Konzepte ist die Interaktion mit physischen Komponenten wie Tastern. Viele Anfänger stoßen schnell an ihre Grenzen, wenn sie versuchen, einen Taster-Zustand abzufragen, ohne dass ihr gesamtes Programm zum Stillstand kommt. Das Stichwort hier ist „non-blocking” – und genau darum geht es in diesem umfassenden Artikel. Wir zeigen Ihnen, wie Sie Taster effektiv und elegant in Ihre Arduino-Projekte integrieren, sodass Ihre void loop()
reibungslos weiterläuft und Ihr Projekt responsive bleibt.
Das Problem mit der Blockade: Warum delay()
Ihre Projekte lähmt
Stellen Sie sich vor, Sie bauen ein Smart-Home-System mit einem Arduino. Sie möchten, dass ein Licht auf Knopfdruck angeht, aber gleichzeitig soll ein Temperatursensor alle paar Sekunden Werte senden und ein Display aktualisiert werden. Wenn Sie nun den Taster wie folgt abfragen:
void loop() {
if (digitalRead(tasterPin) == HIGH) {
// Taster gedrückt
digitalWrite(ledPin, HIGH);
delay(1000); // Warten, damit der Taster nicht prellt und die LED kurz an bleibt
digitalWrite(ledPin, LOW);
}
// Hier soll eigentlich der Temperatursensor ausgelesen werden, aber das Programm ist blockiert!
}
Was passiert? Während die Funktion delay(1000)
ausgeführt wird, tut Ihr Arduino – nichts. Er wartet einfach. In dieser Sekunde können keine anderen Aufgaben erledigt werden: Der Temperatursensor wird nicht ausgelesen, das Display wird nicht aktualisiert, und weitere Taster-Eingaben werden ignoriert. Dieses Verhalten ist in vielen komplexeren Projekten unerwünscht und führt zu einer schlechten Benutzererfahrung. Das Problem wird noch größer, wenn Sie mehrere Taster oder komplexere Interaktionen haben, die auf unterschiedliche Timings angewiesen sind.
Der Übeltäter ist die Funktion delay()
. Sie pausiert die Ausführung des gesamten Programms für die angegebene Anzahl von Millisekunden. Für einfache Blinker mag das in Ordnung sein, aber sobald Ihr Arduino mehr als eine Aufgabe gleichzeitig erledigen soll, wird delay()
zu einem Hindernis.
Die Lösung: Zeitmanagement mit millis()
und Zustandsmaschinen
Der Schlüssel zu non-blocking Code liegt darin, dem Arduino mitzuteilen, dass er nicht warten, sondern stattdessen regelmäßig überprüfen soll, ob eine bestimmte Zeit vergangen ist oder ob sich ein Zustand geändert hat. Hier kommt die Funktion millis()
ins Spiel.
Was ist millis()
?
millis()
ist eine in die Arduino-Sprache integrierte Funktion, die die Anzahl der Millisekunden zurückgibt, seit der Arduino gestartet oder zuletzt zurückgesetzt wurde. Dieser Zähler läuft ununterbrochen weiter und ist nicht von delay()
betroffen. Er ist quasi die innere Uhr Ihres Arduinos. Nach etwa 49 Tagen wird der Zähler überlaufen (d.h., er springt wieder auf Null), aber für die meisten Projekte ist das kein Problem.
Das Prinzip der Zustandsmaschine
Statt einer strikten Abfolge von A -> Warte -> B, arbeiten wir mit Zustandsmaschinen. Das bedeutet, Ihr Programm befindet sich immer in einem bestimmten Zustand (z.B. „Taster nicht gedrückt”, „Taster gedrückt”, „LED an”, „LED aus”) und wechselt basierend auf bestimmten Bedingungen (z.B. „Taster wurde gedrückt”, „1 Sekunde ist vergangen”) von einem Zustand in den nächsten. Die void loop()
-Funktion wird dabei in rasantem Tempo durchlaufen und prüft immer wieder alle Bedingungen.
Grundlagen: Taster-Prellen (Debouncing) verstehen und beherrschen
Bevor wir zum non-blocking Code kommen, müssen wir noch ein wichtiges Phänomen verstehen: das Taster-Prellen. Wenn Sie einen mechanischen Taster drücken, stellen die internen Kontakte nicht sofort eine saubere Verbindung her. Stattdessen prallen sie für kurze Zeit (oft 10-50 Millisekunden) ein paar Mal voneinander ab, bevor sie sich stabilisieren. Für den Arduino sieht das so aus, als würden Sie den Taster mehrfach hintereinander drücken und loslassen, obwohl Sie ihn nur einmal betätigt haben. Dies führt zu unerwünschten Mehrfachauslösungen.
Um dies zu verhindern, müssen wir das Signal „entprellen” (engl. „debounce”). Eine gängige Methode ist, nach der ersten Erkennung einer Tasterbetätigung eine kurze Zeitspanne zu warten und erst danach den Zustand endgültig zu übernehmen. Genau hierfür können wir millis()
nutzen.
Non-Blocking Taster-Abfrage mit millis()
im Detail
Wir werden nun Schritt für Schritt eine robuste, non-blocking Taster-Abfrage implementieren, die auch das Prellen berücksichtigt. Die Grundidee ist, dass wir uns merken, wann der Taster das letzte Mal seinen Zustand geändert hat, und erst wenn eine bestimmte Entprellzeit seit dieser Änderung vergangen ist, den neuen Zustand als gültig ansehen.
Benötigte Variablen
Für unsere non-blocking Taster-Abfrage benötigen wir einige globale Variablen:
const int tasterPin = 2;
: Der Pin, an den der Taster angeschlossen ist.const int ledPin = 13;
: Ein Pin für eine LED, um die Funktion zu testen.int tasterZustand;
: Der aktuelle (entprellte) Zustand des Tasters (HIGH oder LOW).int letzterTasterZustand = LOW;
: Der zuletzt gelesene, unentprellte Zustand des Tasters. Dies dient zur Erkennung von Zustandsänderungen.unsigned long letzteEntprellZeit = 0;
: Eine Variable vom Typunsigned long
, um den Zeitpunkt der letzten Zustandsänderung des Tasters zu speichern. Dies ist wichtig, damillis()
auch einunsigned long
zurückgibt.const unsigned long entprellIntervall = 50;
: Die Dauer, für die wir warten, um das Prellen zu ignorieren (z.B. 50 Millisekunden).
Die setup()
-Funktion
In setup()
initialisieren wir die Pins:
void setup() {
pinMode(tasterPin, INPUT_PULLUP); // Taster mit internem Pull-up Widerstand
pinMode(ledPin, OUTPUT);
Serial.begin(9600); // Für Debug-Ausgaben
// Initialen Taster-Zustand setzen (Pull-up bedeutet HIGH, wenn nicht gedrückt)
tasterZustand = digitalRead(tasterPin);
}
Hinweis: Wir verwenden INPUT_PULLUP
, was bedeutet, dass der Pin intern auf HIGH gezogen wird, wenn der Taster nicht gedrückt ist. Wird der Taster gedrückt, wird der Pin auf LOW gezogen. Dies vereinfacht die Verdrahtung, da kein externer Widerstand benötigt wird.
Die loop()
-Funktion – Das Herzstück
Nun zum Kern der Sache: Die loop()
-Funktion. Sie wird kontinuierlich und sehr schnell ausgeführt. Darin werden wir den Taster-Zustand immer wieder prüfen, aber ohne jegliche Blockade.
void loop() {
// 1. Aktuellen, rohen Zustand des Tasters lesen
int lesung = digitalRead(tasterPin);
// 2. Prüfen, ob sich der rohe Zustand geändert hat
if (lesung != letzterTasterZustand) {
// Wenn ja, merken wir uns den aktuellen Zeitpunkt.
// Dies startet den Entprell-Timer.
letzteEntprellZeit = millis();
}
// 3. Nach Ablauf des Entprell-Intervalls den Zustand endgültig übernehmen
if ((millis() - letzteEntprellZeit) > entprellIntervall) {
// Wenn die Entprellzeit abgelaufen ist UND der gelesene Zustand
// immer noch anders ist als der zuletzt angenommene (entprellte) Zustand...
if (lesung != tasterZustand) {
tasterZustand = lesung; // Dann ist dies unser neuer, gültiger Taster-Zustand
// Hier können wir auf den Taster-Zustand reagieren
if (tasterZustand == LOW) { // LOW bedeutet gedrückt, wenn INPUT_PULLUP verwendet wird
Serial.println("Taster wurde gedrückt!");
digitalWrite(ledPin, HIGH); // LED einschalten
} else {
Serial.println("Taster wurde losgelassen!");
digitalWrite(ledPin, LOW); // LED ausschalten
}
}
}
// 4. Den gelesenen Zustand für den nächsten Durchlauf merken
letzterTasterZustand = lesung;
// Hier können andere Aufgaben ausgeführt werden, ohne blockiert zu werden!
// Beispiel: Sensordaten lesen, Display aktualisieren, weitere Taster abfragen etc.
// delay(10); // KEIN delay() hier!
// Serial.println("Loop läuft weiter..."); // Zur Demonstration
}
Erklärung des Codes: Schritt für Schritt
int lesung = digitalRead(tasterPin);
: Wir lesen den aktuellen, rohen Zustand des Tasters. Dieser kann „prellen”.if (lesung != letzterTasterZustand)
: Wir vergleichen die aktuelle Lesung mit dem Zustand des Tasters aus dem *vorherigen* Loop-Durchlauf (letzterTasterZustand
). Wenn sich diese unterscheiden, wissen wir, dass sich der Taster gerade ändert (oder prellt).letzteEntprellZeit = millis();
: Wenn eine Änderung erkannt wird, speichern wir den aktuellen Zeitpunkt inletzteEntprellZeit
. Dies ist der Startpunkt für unseren Entprell-Timer.if ((millis() - letzteEntprellZeit) > entprellIntervall)
: Diese Bedingung prüft, ob seit der letzten Zustandsänderung (oder dem Beginn des Prellens) genügend Zeit vergangen ist (hier 50 ms). Wenn ja, ist der Taster wahrscheinlich stabil.if (lesung != tasterZustand)
: *Innerhalb* der Entprell-Bedingung prüfen wir nun, ob sich der *aktuelle, stabile* Zustand (lesung
) vom *letzten gültigen entprellten Zustand* (tasterZustand
) unterscheidet. Wenn ja, haben wir eine echte, stabile Zustandsänderung.tasterZustand = lesung;
: Jetzt aktualisieren wir unseren gültigentasterZustand
.- Reaktion auf Taster-Zustand: Hier können Sie Ihren Code platzieren, der auf das Drücken oder Loslassen des Tasters reagiert. Im Beispiel wird eine LED geschaltet und eine Nachricht über die serielle Schnittstelle gesendet.
letzterTasterZustand = lesung;
: Ganz am Ende des Loops speichern wir die aktuelle, rohe Lesung inletzterTasterZustand
, damit sie im nächsten Loop-Durchlauf mit der dann aktuellen Lesung verglichen werden kann.
Der entscheidende Punkt ist: Die loop()
-Funktion wird in jedem Millisekundenbruchteil durchlaufen, ohne angehalten zu werden. Sie prüft lediglich die Bedingungen. Wenn die Zeitbedingungen nicht erfüllt sind, wird der Code einfach übersprungen, und die nächste Aufgabe in der Schleife wird ausgeführt. So bleibt Ihr Arduino stets reaktionsschnell.
Erkennung von Taster-Ereignissen: Gedrückt oder Losgelassen
Oft möchten wir nicht nur wissen, ob der Taster gerade gedrückt ist, sondern ob er *gerade* gedrückt *wurde* (einmaliger Trigger) oder *gerade* losgelassen *wurde*. Dies ist besonders nützlich für das Umschalten von Zuständen oder das Auslösen einer Aktion pro Knopfdruck, nicht pro Dauer des Gedrücktseins.
Dafür benötigen wir eine zusätzliche Variable, die den Zustand des Tasters im vorherigen entprellten Loop-Durchlauf speichert. Nennen wir sie vorherigerTasterZustand
.
Erweitertes Beispiel: LED umschalten
// Globale Variablen (wie oben), erweitert um:
int vorherigerTasterZustand = HIGH; // Wenn INPUT_PULLUP, dann HIGH im Ruhezustand
bool ledAn = false; // Zustand der LED
void setup() {
pinMode(tasterPin, INPUT_PULLUP);
pinMode(ledPin, OUTPUT);
Serial.begin(9600);
digitalWrite(ledPin, ledAn);
tasterZustand = digitalRead(tasterPin);
}
void loop() {
int lesung = digitalRead(tasterPin);
if (lesung != letzterTasterZustand) {
letzteEntprellZeit = millis();
}
if ((millis() - letzteEntprellZeit) > entprellIntervall) {
if (lesung != tasterZustand) {
vorherigerTasterZustand = tasterZustand; // Alten Zustand speichern
tasterZustand = lesung; // Neuen, gültigen Zustand übernehmen
// Hier die Ereigniserkennung:
if (tasterZustand == LOW && vorherigerTasterZustand == HIGH) {
// Taster wurde GEdrückt (Wechsel von HIGH nach LOW)
Serial.println("Taster wurde gedrückt! LED-Zustand umschalten.");
ledAn = !ledAn; // LED-Zustand toggeln
digitalWrite(ledPin, ledAn);
} else if (tasterZustand == HIGH && vorherigerTasterZustand == LOW) {
// Taster wurde LOSgelassen (Wechsel von LOW nach HIGH)
Serial.println("Taster wurde losgelassen!");
}
}
}
letzterTasterZustand = lesung;
// Hier können weiterhin andere Aufgaben ausgeführt werden
}
Mit vorherigerTasterZustand
können wir genau erkennen, wann ein Taster von „nicht gedrückt” auf „gedrückt” wechselt (HIGH
zu LOW
bei INPUT_PULLUP
) oder umgekehrt. Dies ist die Grundlage für jede Art von Schalterfunktion, bei der eine Aktion genau einmal pro Tastendruck ausgelöst werden soll.
Mehrere Taster ohne Blockieren abfragen
Das Schöne an dieser Methode ist, dass sie sich hervorragend auf mehrere Taster erweitern lässt. Sie brauchen für jeden Taster lediglich seine eigenen Satz an Variablen:
int tasterZustand_1, tasterZustand_2;
int letzterTasterZustand_1, letzterTasterZustand_2;
unsigned long letzteEntprellZeit_1, letzteEntprellZeit_2;
- Und natürlich separate
tasterPin_1, tasterPin_2;
Der Code für die Abfrage jedes Tasters würde dann einfach nacheinander in der loop()
-Funktion stehen. Da keine delay()
-Funktionen verwendet werden, wird jeder Taster unabhängig voneinander und ohne gegenseitige Beeinflussung abgefragt.
// Taster 1 Variablen
const int taster1Pin = 2;
int taster1Zustand;
int letzterTaster1Zustand = LOW;
unsigned long letzteTaster1EntprellZeit = 0;
// Taster 2 Variablen
const int taster2Pin = 3;
int taster2Zustand;
int letzterTaster2Zustand = LOW;
unsigned long letzteTaster2EntprellZeit = 0;
const unsigned long entprellIntervall = 50;
void setup() {
pinMode(taster1Pin, INPUT_PULLUP);
pinMode(taster2Pin, INPUT_PULLUP);
Serial.begin(9600);
}
void loop() {
// Abfrage für Taster 1
int lesung1 = digitalRead(taster1Pin);
if (lesung1 != letzterTaster1Zustand) {
letzteTaster1EntprellZeit = millis();
}
if ((millis() - letzteTaster1EntprellZeit) > entprellIntervall) {
if (lesung1 != taster1Zustand) {
taster1Zustand = lesung1;
if (taster1Zustand == LOW) {
Serial.println("Taster 1 gedrückt!");
// Aktionen für Taster 1
}
}
}
letzterTaster1Zustand = lesung1;
// Abfrage für Taster 2
int lesung2 = digitalRead(taster2Pin);
if (lesung2 != letzterTaster2Zustand) {
letzteTaster2EntprellZeit = millis();
}
if ((millis() - letzteTaster2EntprellZeit) > entprellIntervall) {
if (lesung2 != taster2Zustand) {
taster2Zustand = lesung2;
if (taster2Zustand == LOW) {
Serial.println("Taster 2 gedrückt!");
// Aktionen für Taster 2
}
}
}
letzterTaster2Zustand = lesung2;
// Andere Aufgaben hier...
}
Für eine noch sauberere Lösung, insbesondere bei vielen Tastern, könnten Sie die Logik in eigene Funktionen auslagern oder sogar eine kleine Klasse für Taster erstellen. Es gibt auch fertige Bibliotheken wie Bounce2
oder ezButton
, die diese Logik bereits implementieren und Ihnen die Arbeit erleichtern. Das Verständnis der hier gezeigten manuellen Implementierung ist jedoch essenziell, um zu verstehen, was „unter der Haube” passiert.
Vorteile der Non-Blocking Methode
Die Verwendung von millis()
und Zustandsmaschinen für die Taster-Abfrage bietet entscheidende Vorteile:
- Responsivität: Ihr Arduino reagiert sofort auf Eingaben und kann andere Aufgaben parallel ausführen.
- Multitasking: Sie können mehrere Taster, Sensoren, Displays und Aktoren gleichzeitig steuern, ohne dass sich diese gegenseitig blockieren.
- Flexibilität: Komplexere Logiken, wie das Drücken-und-Halten, Doppelklick oder zeitgesteuerte Aktionen, lassen sich viel einfacher implementieren.
- Sauberer Code: Auch wenn es am Anfang etwas mehr Variablen und Logik erfordert, führt es langfristig zu modularerem und besser wartbarem Code.
Fazit: Beherrschen Sie die Zeit, beherrschen Sie Ihr Projekt
Die Fähigkeit, Taster-Zustände non-blocking abzufragen, ist eine fundamentale Fertigkeit in der Arduino-Programmierung. Sie befreit Sie von den Einschränkungen der delay()
-Funktion und ermöglicht es Ihnen, komplexere, interaktivere und reaktionsschnellere Projekte zu realisieren. Indem Sie die Prinzipien von millis()
, Entprellen und Zustandsmaschinen verstehen und anwenden, verwandeln Sie Ihren Arduino von einem einfachen sequenziellen Ausführer in ein leistungsfähiges Multitasking-Werkzeug.
Nehmen Sie sich die Zeit, die hier gezeigten Beispiele zu Hause auf Ihrem Arduino zu testen und zu experimentieren. Sobald Sie dieses Konzept verinnerlicht haben, werden sich Ihnen völlig neue Möglichkeiten für Ihre Projekte eröffnen. Viel Erfolg beim Coden!