Das Problem mit delay() lösen und den Sketch mit millis() beschleunigen

Die delay()-Funktion ist schnell und einfach in einfachen Programmen anzuwenden. Wenn jedoch die Komplexität des Sketches zunimmt, kann sie zu Problemen führen und dich einschränken. Insbesondere wirst du schnell an funktionalen Grenzen stoßen, wenn du gleichzeitig mehrere Aufgaben erledigen möchtest.

Stell dir folgendes Szenario vor: Du hast eine LED programmiert, die regelmäßig mit delay() blinkt. Jetzt möchtest du zusätzlich einen Schalter einbauen, der eine zweite LED ein- und ausschaltet. Wie du bereits weißt, führt die Verwendung der delay()-Funktion dazu, dass der Sketch anhält, bis die in der Klammer angegebene Zeit abgelaufen ist.

Der Aufbau

Der Sketch

Schaue dir folgenden Sketch genauer an und probiere ihn an deinem Arduino aus. Wie reagiert die zweite LED, wenn du auf den Button drückst? Funktioniert es jedesmal?

int ledPin = 13; 
int buttonPin = 3; 
int ledCheck = 4;
int valueButton;

void setup()
{
  pinMode(ledPin, OUTPUT);
  pinMode(ledCheck, OUTPUT);
  pinMode(buttonPin, INPUT);
}

void loop()
{
  // LED blinken lassen
  digitalWrite(ledPin, HIGH); // Rote LED auf High-Pegel (5V) 
  delay(2000); // Eine Sekunde warten 
  digitalWrite(ledPin, LOW); // Rote LED auf LOW-Pegel (0V) 
  delay(2000); // Eine Sekunde warten
  
  // Abfrage des Taster-Status
  valueButton = digitalRead(buttonPin);
  
  if(valueButton == HIGH)
  {
    digitalWrite(ledCheck, HIGH);
  }
  else
  {
    digitalWrite(ledCheck, LOW);
  }
}

Der Sketch verhält sich anders als erwartet, obwohl der Code korrekt zu sein scheint. Wenn der Taster mehrmals betätigt wird, reagiert die zweite LED (ledCheck) nicht sofort. Das liegt daran, dass die delay()-Funktion den Programmfluss blockiert, wenn sie aufgerufen wird. Wenn der Taster in diesem Moment gedrückt wird, kann der Status nicht gelesen werden und die if-Kondition erhält keinen Eingang. Erst nachdem die delay()-Funktionen abgelaufen sind und das Programm den Taster erreicht hat, kann der Status abgefragt werden (und nur an dieser Stelle). Um dieses Problem zu lösen, müssen einige Änderungen vorgenommen werden.

Um das Problem besser zu verstehen, betrachten wir die Signale an den Pins 13, 3 und 4 übereinander. Das blaue Impulsdiagramm zeigt das gleichmäßige Blinken am Pin 13. Das Signal wechselt kontinuierlich zwischen LOW und HIGH. Wenn wir den Taster und die ledCheck mit diesem blauen Signal vergleichen und in Beziehung setzen, sehen wir, dass die ledCheck nicht immer auf den Taster reagiert. Sie reagiert nur an den mit dem Buchstaben X markierten Bereichen. Warum nur an diesen Stellen? Das liegt daran, dass der Taster erst 2000 ms Zeit hat, um den Status einzulesen, wenn beide delay()-Funktionen abgearbeitet sind (nach dem LOW-Pegel), bevor die erste delay()-Funktion (HIGH-Pegel) erneut aufgerufen wird.

Probleme im Code:

  • Das Hauptproblem im Code ist die Verwendung von delay(), was zu einer blockierenden Ausführung führt. Dadurch kann der Taster nicht sofort erkannt werden, wenn er gedrückt wird, da das Programm während der Verzögerung feststeckt.

  • Das Blinken der roten LED kann nicht gleichzeitig mit der Überwachung des Tasters erfolgen. Der Code verwendet eine einfache Sequenz von delay(), was bedeutet, dass die Ausführung für eine festgelegte Zeit anhält, ohne andere Aufgaben erledigen zu können.

  • Wenn der Taster schnell hintereinander gedrückt wird, kann es zu inkonsistenten oder unerwarteten Reaktionen der zweiten LED kommen, da die Verzögerungen dazu führen, dass das Programm den Tasterstatus nicht sofort liest.

Um diese Probleme zu beheben, müsste der Code umstrukturiert werden, um die Verwendung von delay() zu vermeiden und stattdessen das Konzept der Zustandsmaschine oder der nicht blockierenden Verzögerungen mit millis() einzuführen. Dadurch kann das Programm gleichzeitig den Taster überwachen und die LEDs steuern, ohne den Programmfluss zu blockieren.

int ledPinBlink = 13; // Rote Blink-LED-Pin 13

int ledPinTaster = 10;  // Gelbe Taster-LED-Pin 10
int tasterPin = 8; // Taster-Pin 8

int tasterStatus;  // Variable zur Aufname des Tasterstatus 

int interval = 2000; // Intervalzeit (2 Sekunden)

unsigned long prev; // Zeit-Variable
int ledStatus = LOW; // Statusvariable für die Blink-LED


void setup()
{
  pinMode(ledPinBlink, OUTPUT); // Blink-LED-Pin als Ausgang 
  pinMode(ledPinTaster, OUTPUT); // Taster-LED-Pin als Ausgang 
  pinMode(tasterPin, INPUT); // Taster-Pin als Eingang
  prev = millis(); // jetzigen Zeitstempel merken
}


void loop()
{
  // Blink-LED über Intervalsteuerung blinken lassen 
  if((millis() - prev) > interval)
  {
  prev = millis();
  ledStatus = !ledStatus; // Toggeln des LED-Status 
  digitalWrite(ledPinBlink, ledStatus); // Toggeln der roten LED
  }

// Abfrage des Taster-Status
tasterStatus = digitalRead(tasterPin); 

if(tasterStatus == HIGH)
digitalWrite(ledPinTaster, HIGH); // Gelbe LED auf High-Pegel (5V) 
else
digitalWrite(ledPinTaster, LOW); // Gelbe LED auf High-Pegel (0V) 
}

Ich denke, dass ich mit der Intervalsteuerung beginne, denn sie ist hier das Wichtigste. Das folgende Diagramm zeigt uns einen zeitli- chen Verlauf mit bestimmten markanten Zeitwerten. Zuvor muss ich aber noch einige Dinge im Quellcode erklären. Da ist zum einen die neue millis-Funktion, die die Zeit seit dem Starten des aktuellen Sketches in Millisekunden zurück liefert. Dabei ist auf etwas Wich- tiges zu achten. Der Rückgabedatentyp ist unsigned long, also ein vorzeichenloser 32-Bit Ganzzahltyp, dessen Wertebereich sich von 0 bis 4.294.967.295 (232-1) erstreckt. Dieser Wertebereich ist so groß, weil er über einen längeren Zeitraum (max. 49.71 Tage) in der Lage sein soll, die Daten aufzunehmen, bevor es zu einem Über- lauf kommt.

Ein Überlauf bedeutet bei Variablen übrigens, dass der maximal abbildbare Wertebereich für einen bestimmten Datentyp über- schritten wurde und anschließend wieder bei 0 begonnen wird. Für den Datentyp byte, der eine Datenbreite von 8 Bits aufweist und demnach 28 = 256 Zustände (0 bis 255) speichern kann, tritt ein Überlauf bei der Aktion 255 + 1 auf. Den Wert 256 ist der Datentyp byte nicht mehr in der Lage zu verarbeiten.

Es wurden von mir drei weitere Variablen eingefügt, die folgende Aufgabe haben:

  • interval (nimmt die Zeit im ms auf, die für das Blinkinterval zuständig ist)

  • prev (nimmt die aktuell verstrichene Zeit in ms auf. Prev kommt von previous und bedeutet übersetzt: vorher)

  • ledStatus (In Abhängigkeit des Status von HIGH oder LOW der Variablen, wird die Blink-LED angesteuert)

Während des ganzen Ablaufes wurde an keiner Stelle im Quellcode ein Halt in Form einer Pause eingelegt, so dass das Abfragen des digitalen Pins 8 zur Steuerung der Taster-LED in keinster Weise beeinträchtigt wurde. Ein Druck auf den Taster wird fast unmittel- bar ausgewertet und zur Anzeige gebracht. Der einzige neue Befehl, bei dem es sich ja um eine Funktion handelt, die einen Wert zurück liefert, lautet millis.

Du siehst, dass er keine Argumente entgegen nimmt und deswegen ein leeres Klammernpaar hat. Sein Rückgabewert besitzt den Datentyp unsigned long.

Eine Zeile bereitet mir aber noch ein wenig Kopfschmerzen. Was bedeutet denn ledStatus = !ledStatus ? Und was heisst toggeln?

Du bist ja wieder schneller als ich, denn so weit war ich doch noch gar nicht. Aber ok, wenn Du’s schon mal ansprichst, dann will ich auch sofort darauf eingehen. In der Variablen ledStatus wird der Pegel gespeichert, der die rote LED ansteuert bzw. für das Blinken zuständig ist (HIGH bedeutet leuchten und LOW bedeutet dun- kel). Über die nachfolgende Zeile

digitalWrite(ledPinBlink, ledStatus);

wird die LED dann angesteuert. Das Blinken wird ja gerade dadurch erreicht, dass du zwischen den beiden Zuständen HIGH bzw. LOW hin- und herschaltest. Das wird auch Toggeln genannt. Ich werde die Zeile etwas umformulieren, denn dann wird der Sinn vielleicht etwas deutlicher.

if(ledStatus == LOW) ledStatus = HIGH;

else

ledStatus = LOW;

In der ersten Zeile wird abgefragt, ob der Inhalt der Variablen led- Status gleich LOW ist. Falls ja, setze ihn auf HIGH, andernfalls auf LOW. Das bedeutet ebenfalls ein Toggeln des Status. Viel kürzer geht es mit der folgenden einzeiligen Variante, die ich ja schon ver- wendet habe.

ledStatus = !ledStatus; // Toggeln des LED-Status

Ich verwende dabei den logischen Not-Operator, der durch das Ausrufezeichen repräsentiert wird. Er wird häufig bei booleschen Variablen verwendet, die nur die Wahrheitswerte true bzw. false annehmen können. Der Not-Operator ermittelt ein Ergebnis, das einen entgegengesetzten Wahrheitswert aufweist, wie der Operand. Es funktioniert aber auch bei den beiden Pegeln HIGH bzw. LOW.

Am Schluss wird noch ganz normal und ohne Verzögerung der Tas- ter an Port 8 abgefragt.

tasterStatus = digitalRead(tasterPin); if(tasterStatus == HIGH)

digitalWrite(ledPinTaster, HIGH); else

digitalWrite(ledPinTaster, LOW);

Ich zeige dir das Verhalten wieder an einem Impulsdiagramm, bei dem die drei relevanten Signale wieder Blink-LED (Pin 13), Taster (Pin 8) und Taster-LED (Pin 10), wie auch schon eben, untereinan- der dargestellt sind:

Wir erkennen, dass das blaue Signal die Blink-LED an Pin 13 dar- stellt. Wenn ich jetzt in unregelmäßigen Abständen den Taster an Pin 8 betätige - dargestellt durch das gelbe Signal - reagiert unmit- telbar das rote Signal der Taster-LED an Pin 10. Es ist keine Zeit- verzögerung bzw. Unterbrechung zu erkennen. Das Verhalten der Schaltung ist genau das, was wir erreichen wollten.