Pause muss auch mal sein - Beispiele zum nichtblockierenden Programmablauf

Es kommt häufig vor, dass der Ablauf eines Programms für kurze Zeit pausiert werden muss. In diesem Beitrag möchte ich zeigen, welche Möglichkeiten es dafür gibt und wann man welche einsetzen sollte. Ich möchte auch noch einmal darauf eingehen, was non-blocking bedeutet und wie man es programmieren könnte. Für die Demonstration habe ich Ihnen einige Arduino-Sketches erstellt. Das Finale bildet ein Programm mit Auto- und Fußgängerampel, einem Taster für den Überquerungswunsch eines Fußgängers und einer Nachtschaltung. Alles „unterbrechungsfrei“. Los geht’s.

Was wir benötigen

1

Nano V3.0 CH340 Chip unverlötet

oder

Nano V3.0 CH340 Chip fertig verlötet

oder

Mikrocontroller Board ATmega328

oder

Mega 2560 R3 Board mit ATmega2560

oder

D1 Mini NodeMcu mit ESP8266-12F

oder

mehrere

LED mit Vorwiderständen

1

alternativ das Ampelmodul einzeln oder im Sensorkit

1

Taster (oder zwei Kabel, die wie ein Taster verwendet werden)

1

Breadboard mit Kabeln

 

In vielen Getting Started Guides für Mikrocontroller wird zuerst ein Blink-Beispiel gezeigt. Das finden Sie auch in der Arduino IDE unter Beispiele -> 01.Basic -> Blink. Das ist das Pendant zum „Hello World!“ Quellcode, nur eben nicht mit Textausgabe, sondern einer blinkenden LED. Es dient wunderbar als Test, ob der Upload zum Mikrocontroller funktioniert. Einige ESP Entwicklerboards haben keine LED onboard, das sollte man beachten.

Der abstrakte Aufbau dieses Programms ist immer gleich:

  • Schalte LED ein
  • Pause
  • Schalte LED aus
  • Pause
  • Starte von vorn

In der Arduino IDE wird der Befehl „starte von vorn“ automatisch mit der loop()-Funktion umgesetzt. Programmiert man den Mikrocontroller in einer anderen Entwicklungsumgebung wie Eclipse (in C++) oder Thonny (in MicroPython), muss man sich um die Wiederholung selbst kümmern. Nicht zu vergessen ist hier der Raspberry Pi, der ebenfalls (dann mit Python) so programmiert werden kann.

Das Schalten eines Ein- oder Ausgangs ist ebenfalls abhängig von der Entwicklungsumgebung. Darauf kommt es uns hier allerdings nicht an. Es geht hauptsächlich um die Pause.

In der Arduino IDE wird für eine Verzögerung die Funktion delay() verwendet. Als Übergabeparameter setzt man dann den Verzögerungswert in Millisekunden ein, z.B. delay(1000) für eine Verzögerung von 1 Sekunde. Alternativ kann auch die Funktion delayMicroseconds() verwendet werden, die wie der Name schon sagt, um Mikrosekunden verzögert. In (Micro)Python übernimmt solch eine Aufgabe die Funktion sleep(), die allerdings Sekunden statt Millisekunden entgegennimmt.

Man kann sich diese Verzögerung auch selbst programmieren, in dem man eine while-Schleife so lange laufen lässt, bis eine bestimmte Zeit vergangen ist. In der Arduino IDE kann dafür die Funktion millis() verwendet werden. Man speichert sich die Zeit vor dem Betreten der Schleife und fragt innerhalb der Schleife zyklisch ab, ob die Zeitspanne überschritten wurde. Dann verlässt man diese Schleife wieder. Der Vorteil hier wäre, dass man innerhalb der Schleife noch eigene Befehle ausführen könnte.

Genau das dient nun zur Überleitung, warum man delay() oder sleep() in bestimmten Situationen nicht einsetzen sollte. Deren Ausführung ist nämlich blockierend (engl. blocking). Das heißt, dass während der Pause keine weiteren Befehle ausgeführt werden können, was aber in den meisten Anwendungsfällen nötig ist.

Welche Anwendungsfälle könnten das sein? Wenn Sie mehrere LEDs in unterschiedlichen Intervallen blinken lassen möchten, oder während des Blinkens einfache andere Befehle ausführen möchten. Die einfache Verwendung eines Tasters zur Nutzereingabe wäre mit Verzögerungen schon etwas hakelig. Denn wenn Sie z.B. eine LED im Sekundentakt blinken lassen würden, wäre die jeweilige Pause für EIN und AUS eine Sekunde lang (in der Summe zwei Sekunden) und für die Nutzereingabe am Taster hätte man nur wenige Millisekunden Zeit. Man müsste die Taste gedrückt halten, bis das Programm in der Schleife beim Befehl der Tasterabfrage angekommen ist und würde dann den Befehl dafür ausführen.

Ein weiteres Beispiel wäre z.B. ein Datenserver, wie man ihn mit dem ESP und dem onboard WiFi Chip umsetzen kann. Die Abfrage über einen Webbrowser wäre immer stark verzögert, wenn Sie an einer Stelle im Quellcode die blockierenden Pausen einfügen würden.

Es birgt also viele Nachteile, wenn das Programm eines Mikrocontrollers blockiert. Das Gegenstück dazu ist das nicht-blockierende Programmieren (engl. non-blocking). Zwei Möglichkeiten, so etwas umzusetzen sind entweder zyklische Zeitabfragen, oder Timerinterrupts.

Zyklische Zeitabfragen

Es gibt in der Arduino IDE unter Beispiele -> 0.2Digital den Sketch BlinkWithoutDelay. Darin wird gezeigt, wie der Zustand einer LED nach dem Ablauf eines Zeitintervalls umgeschaltet wird. Ein kleiner Tipp am Rande: Man benötigt in diesem Sketch weniger Zeilen Code, wenn man die komplette if-Abfrage für das Umschalten der LED weglässt und stattdessen ledState = !ledState; verwendet.

Die oben erwähnte Variante mit der while-Schleife, in der man weitere Befehle ausführen kann, wird hier nun ein wenig umgekrempelt. Wie funktioniert das Ganze? Der Ablauf sieht folgendermaßen aus:

  • Lies die aktuelle Zeit
  • Wenn mein gewünschtes Zeitintervall überschritten ist, ändere den Zustand der LED und speichere die neue Zeit
  • Mach etwas anderes
  • Beginn von vorn

Wie Sie sehen, wird hier nicht pausiert. Wenn die Bedingung für das Zeitintervall nicht erfüllt ist, wird das Umschalten der LED nicht durchgeführt. Das Programm läuft aber weiter.

Um herauszufinden, ob das gewünschte Zeitintervall erreicht oder überschritten ist, wird hier (wie oben schon erwähnt) die Funktion millis() verwendet. Sie gibt die Zeit in Millisekunden seit dem Start des Mikrocontrollers zurück. Subtrahiert man nun die alte gespeicherte Zeit (die einen geringeren Wert hat) von der aktuellen Zeit (die einen größeren Wert hat), ergibt das die abgelaufene Zeitspanne. Die vergleicht man nun mit dem gewünschten Intervall.

Jetzt könnte man denken, wenn das Intervall erreicht ist und das Umschalten der LED durchgeführt wird, ist der Mikrocontroller beschäftigt. Das stimmt. Jedoch sind das Ausführungszeiten von wenigen Millisekunden. Es ist also eher vertretbar, als sekundenlange Pausen.

Ich möchte Ihnen nun als Beispiel einige Sketches zeigen, in denen ich mehrere LEDs „gleichzeitig“ mit unterschiedlichen Intervallen blinken lasse.

„Gleichzeitig“ ist in Computerprogrammen oft relativ zu sehen, da selbst in der Parallelprozess- oder Threadprogrammierung die Trigger für die gleichzeitig ablaufenden Prozesse oder Threads nacheinander ausgeführt werden. Für echte parallele Prozessausführung empfehle ich einen Ausflug in die Welt der FPGA (dabei handelt es sich um integrierte Schaltkreise der Digitaltechnik, in welchen eine logische Schaltung geladen werden kann).

Später werde ich zusätzlich einen Taster einfügen, der eine weitere LED umschaltet und am Ende gebe ich Ihnen noch eine unterbrechungsfreie Ampelschaltung mit sechs LEDs und Taster mit auf den Weg.

Hinweis: wenn Sie statt eines AVR Mikrocontrollers z.B. den D1 Mini ESP8266 verwenden, müssen Sie die Pinnummern ändern. Schauen Sie dafür in das Datenblatt, das auf den jeweiligen Shopseiten verlinkt ist. Ich verwende hier den Nano V3 mit ATmega328p.

In folgendem Sketch habe ich aus dem ursprünglichen Blink-Beispiel ein Programm mit drei statt einer LED gemacht:

3 LEDs

Blink_3LEDs_delay.ino

#define LED1 4
#define LED2 5
#define LED3 6

unsigned long pauseLED1 = 500;
unsigned long pauseLED2 = 1000;
unsigned long pauseLED3 = 5000;

bool LEDstate1 = LOW;
bool LEDstate2 = LOW;
bool LEDstate3 = LOW;

void setup() {
  pinMode(LED1, OUTPUT);
  pinMode(LED2, OUTPUT);
  pinMode(LED3, OUTPUT);
}

void loop() {
  delay(pauseLED1);
  LEDstate1 = !LEDstate1;
  digitalWrite(LED1, LEDstate1);
  
  delay(pauseLED2);
  LEDstate2 = !LEDstate2;
  digitalWrite(LED2, LEDstate2);
  
  delay(pauseLED3);
  LEDstate3 = !LEDstate3;
  digitalWrite(LED3, LEDstate3); 
}

Wenn Sie dieses Programm ausführen, zeigt sich der Nachteil der delay()-Funktion. Die LEDs werden deutlich nacheinander umgeschaltet.

Im folgenden Sketch habe ich nun delay() entfernt und stattdessen die „Komm später wieder vorbei!“-Methode verwendet:

Blink_3LEDs_noDelay.ino

#define LED1 4
#define LED2 5
#define LED3 6

unsigned long pauseLED1 = 500;
unsigned long pauseLED2 = 1000;
unsigned long pauseLED3 = 5000;

unsigned long oldMillis_LED1 = 0;
unsigned long oldMillis_LED2 = 0;
unsigned long oldMillis_LED3 = 0;

bool LEDstate1 = LOW;
bool LEDstate2 = LOW;
bool LEDstate3 = LOW;

void setup() {
  pinMode(LED1, OUTPUT);
  pinMode(LED2, OUTPUT);
  pinMode(LED3, OUTPUT);
}

void loop() {

  if (millis() - oldMillis_LED1 >= pauseLED1) {
    LEDstate1 = !LEDstate1;
    digitalWrite(LED1, LEDstate1);
    oldMillis_LED1 = millis();
  }

  if (millis() - oldMillis_LED2 >= pauseLED2) {
    LEDstate2 = !LEDstate2;
    digitalWrite(LED2, LEDstate2);
    oldMillis_LED2 = millis();
  }

  if (millis() - oldMillis_LED3 >= pauseLED3) {
    LEDstate3 = !LEDstate3;
    digitalWrite(LED3, LEDstate3);
    oldMillis_LED3 = millis();
  }
}

Wenn Sie diesen Sketch ausführen, blinken die LEDs scheinbar unabhängig in ihrem Intervall. Sie können gern mal testen wie das aussieht, wenn Sie die Werte für pauseLEDx ändern.

Wir können jetzt schon sehen, wie quasiparallele Ausführung auf einem Singlecore-Prozessor aussehen kann. Richtig interessant wird es, wenn man als Nutzer dann noch Eingaben tätigen möchte. Sei es nun durch einen Taster, oder wie oben beschrieben z.B. durch Abfragen per Datenbusverbindung.

Für die Datenübertragung existieren übrigens auch verschiedene Strategien und Algorithmen. Oft wird in der C/C++-Programmierung die Funktion read() verwendet. In den meisten Fällen ist das eine blockierende Funktion, die z.B. an einer seriellen Verbindung oder einem Socket so lange wartet, bis Daten eintreffen. Wenn Ihr Programm dann aber noch andere Aufgaben durchführen soll, ist die blockierende Abfrage hinderlich. Dafür gibt es dann wieder Strategien, um die Blockade aufzuheben. Etwa durch einen Timeout, der dafür sorgt, dass nur kurz die „Datenleitung“ (eigentlich Speicherplätze bzw. Register) auf neue Daten abgefragt und dann wieder weggeschickt werden. So kann das Hauptprogramm weiter ausgeführt werden. Das nur als kurzer Exkurs.

Ich möchte nun in die oben gezeigten Programme einen Taster einfügen. In diesem Fall übernimmt er die beispielhafte Aufgabe, eine weitere LED umzuschalten. Während delay() ausgeführt wird, werden keine Dateneingänge erkannt. Welches Verhalten zeigt also das folgende Programm, wenn Sie es ausführen?

4 LEDs 1 Taster

Blink_3LEDs_delay_taster.ino

#define LED1 4
#define LED2 5
#define LED3 6
#define LED4 7
#define TASTER 8

unsigned long pauseLED1 = 500;
unsigned long pauseLED2 = 1000;
unsigned long pauseLED3 = 5000;

bool LEDstate1 = LOW;
bool LEDstate2 = LOW;
bool LEDstate3 = LOW;
bool LEDstate4 = LOW;

bool TASTERstate = HIGH;

void setup() {
  pinMode(LED1, OUTPUT);
  pinMode(LED2, OUTPUT);
  pinMode(LED3, OUTPUT);
  pinMode(LED4, OUTPUT);
  pinMode(TASTER, INPUT_PULLUP);
}

void loop() { 
  delay(pauseLED1);
  LEDstate1 = !LEDstate1;
  digitalWrite(LED1, LEDstate1);
  
  delay(pauseLED2);
  LEDstate2 = !LEDstate2;
  digitalWrite(LED2, LEDstate2);
  
  delay(pauseLED3);
  LEDstate3 = !LEDstate3;
  digitalWrite(LED3, LEDstate3);

  TASTERstate = digitalRead(TASTER);
  if (TASTERstate == LOW) {
    LEDstate4 = !LEDstate4;
    digitalWrite(LED4, LEDstate4);
  }
}

Sie müssen entweder den Taster sehr lang gedrückt halten, oder genau den richtigen Moment abpassen, damit die vierte LED umgeschaltet wird.

Kurzer Hinweis zum Sketch: der Eingang des Tasters ist als INPUT_PULLUP definiert. Dadurch wird kein externer Widerstand benötigt, aber der Eingang ist dadurch activ low. Der Normalzustand (also AUS) ist HIGH und der gedrückte Zustand des Tasters (also AN) ist LOW.

Ich setze nun den Taster und die vierte LED in den Sketch ohne delay()-Funktion ein:

Blink_3LEDs_noDelay_taster.ino

#define LED1 4
#define LED2 5
#define LED3 6
#define LED4 7
#define TASTER 8

unsigned long pauseLED1 = 500;
unsigned long pauseLED2 = 1000;
unsigned long pauseLED3 = 5000;

unsigned long oldMillis_LED1 = 0;
unsigned long oldMillis_LED2 = 0;
unsigned long oldMillis_LED3 = 0;

bool LEDstate1 = LOW;
bool LEDstate2 = LOW;
bool LEDstate3 = LOW;
bool LEDstate4 = LOW;

bool TASTERstate = HIGH;

void setup() {
  pinMode(LED1, OUTPUT);
  pinMode(LED2, OUTPUT);
  pinMode(LED3, OUTPUT);
  pinMode(LED4, OUTPUT);
  pinMode(TASTER, INPUT_PULLUP);
}

void loop() {

  if (millis() - oldMillis_LED1 >= pauseLED1) {
    LEDstate1 = !LEDstate1;
    digitalWrite(LED1, LEDstate1);
    oldMillis_LED1 = millis();
  }

  if (millis() - oldMillis_LED2 >= pauseLED2) {
    LEDstate2 = !LEDstate2;
    digitalWrite(LED2, LEDstate2);
    oldMillis_LED2 = millis();
  }

  if (millis() - oldMillis_LED3 >= pauseLED3) {
    LEDstate3 = !LEDstate3;
    digitalWrite(LED3, LEDstate3);
    oldMillis_LED3 = millis();
  }

  TASTERstate = digitalRead(TASTER);
  if (TASTERstate == LOW) {
    LEDstate4 = !LEDstate4;
    digitalWrite(LED4, LEDstate4);
  }
}

Während die LEDs blinken, können Sie den Taster betätigen und damit die vierte LED umschalten. Dabei wird Ihnen auffallen, dass das Programm noch nicht ganz perfekt funktioniert. Manchmal reagiert die LED nicht so auf den Taster, wie wir es erwarten. Das hat einen physikalischen Hintergrund und nennt sich „Prellen“ (engl. bounce). Im mikroskopischen Bereich wird der Taster nämlich nicht sofort geschlossen, sondern öffnet und schließt sich in sehr kurzen Zeitabständen mehrmals, bis er endgültig schließt. Das Ganze passiert auch beim Loslassen des Tasters. Meine eigenen Erfahrungen haben gezeigt, dass das Prellen nach ca. 50 ms bis 80 ms vorbei ist. Auch hier könnte man in einigen Situationen die delay()-Funktion verwenden. 80 ms sind nicht viel und könnten unter Umständen vertretbar sein. Man registriert die erste Änderung des Tasters und legt dann eine Pause ein, in der nicht mehr auf den Eingang reagiert werden kann.

Auch hier kann man statt delay() die zyklische Zeitabfrage verwenden. Außerdem müsste man noch den Umstand betrachten, dass man den Taster gedrückt hält. Die LED wird folglich in kurzen Abständen immer wieder umgeschaltet, was normalerweise nicht erwünscht ist. Somit registrieren wir neben der vergangenen prellZeit auch das Loslassen des Tasters.

Im folgenden Sketch habe ich das (dieses Mal ohne Gegenbeispiel mit delay()) umgesetzt. Die vierte LED leuchtet immer dann, wenn der Taster betätigt wird. Man beachte, dass währenddessen die drei ersten LEDs fleißig weiterblinken und der Taster scheinbar keinen Einfluss darauf hat. Quasi parallel.

Blink_3LEDs_noDelay_taster_debounce.ino

#define LED1 4
#define LED2 5
#define LED3 6
#define LED4 7
#define TASTER 8

unsigned long pauseLED1 = 500;
unsigned long pauseLED2 = 1000;
unsigned long pauseLED3 = 5000;

unsigned long oldMillis_LED1 = 0;
unsigned long oldMillis_LED2 = 0;
unsigned long oldMillis_LED3 = 0;

bool LEDstate1 = LOW;
bool LEDstate2 = LOW;
bool LEDstate3 = LOW;
bool LEDstate4 = LOW;

bool TASTERstate = HIGH;
bool TASTERstate_old = HIGH;

unsigned long prellZeit = 80;
unsigned long TASTERmillis_old = 0;

void setup() {
  pinMode(LED1, OUTPUT);
  pinMode(LED2, OUTPUT);
  pinMode(LED3, OUTPUT);
  pinMode(LED4, OUTPUT);
  pinMode(TASTER, INPUT_PULLUP);
}

void loop() {

  if (millis() - oldMillis_LED1 >= pauseLED1) {
    LEDstate1 = !LEDstate1;
    digitalWrite(LED1, LEDstate1);
    oldMillis_LED1 = millis();
  }

  if (millis() - oldMillis_LED2 >= pauseLED2) {
    LEDstate2 = !LEDstate2;
    digitalWrite(LED2, LEDstate2);
    oldMillis_LED2 = millis();
  }

  if (millis() - oldMillis_LED3 >= pauseLED3) {
    LEDstate3 = !LEDstate3;
    digitalWrite(LED3, LEDstate3);
    oldMillis_LED3 = millis();
  }

  TASTERstate = digitalRead(TASTER);

  if (TASTERstate != TASTERstate_old && millis() - TASTERmillis_old > prellZeit) {
    LEDstate4 = !LEDstate4;
    digitalWrite(LED4, LEDstate4);
    TASTERstate_old = TASTERstate;
    TASTERmillis_old = millis();
  }
}

Was machen wir nun, wenn der Zustand der LED beibehalten werden soll, nachdem wir den Taster losgelassen haben? Wir dürfen nicht wieder sofort auf die Änderung des Tasters reagieren, sondern müssen einmal aussetzen, nämlich beim Loslassen. Das entspricht dem neuen Zustand HIGH (AUS) und dem alten Zustand LOW (AN), also muss der Wechsel von AN nach AUS ignoriert werden.

Dafür setzen wir eine weitere if-Abfrage ein:

Blink_3LEDs_noDelay_taster_debounce_gedrueckthalten.ino

#define LED1 4
#define LED2 5
#define LED3 6
#define LED4 7
#define TASTER 8

unsigned long pauseLED1 = 500;
unsigned long pauseLED2 = 1000;
unsigned long pauseLED3 = 5000;

unsigned long oldMillis_LED1 = 0;
unsigned long oldMillis_LED2 = 0;
unsigned long oldMillis_LED3 = 0;

bool LEDstate1 = LOW;
bool LEDstate2 = LOW;
bool LEDstate3 = LOW;
bool LEDstate4 = LOW;

bool TASTERstate = HIGH;
bool TASTERstate_old = HIGH;

unsigned long prellZeit = 80;
unsigned long TASTERmillis_old = 0;

void setup() {
  pinMode(LED1, OUTPUT);
  pinMode(LED2, OUTPUT);
  pinMode(LED3, OUTPUT);
  pinMode(LED4, OUTPUT);
  pinMode(TASTER, INPUT_PULLUP);
}

void loop() {

  if (millis() - oldMillis_LED1 >= pauseLED1) {
    LEDstate1 = !LEDstate1;
    digitalWrite(LED1, LEDstate1);
    oldMillis_LED1 = millis();
  }

  if (millis() - oldMillis_LED2 >= pauseLED2) {
    LEDstate2 = !LEDstate2;
    digitalWrite(LED2, LEDstate2);
    oldMillis_LED2 = millis();
  }

  if (millis() - oldMillis_LED3 >= pauseLED3) {
    LEDstate3 = !LEDstate3;
    digitalWrite(LED3, LEDstate3);
    oldMillis_LED3 = millis();
  }

  TASTERstate = digitalRead(TASTER);

  if (TASTERstate != TASTERstate_old && millis() - TASTERmillis_old > prellZeit) {
    if (TASTERstate == LOW && TASTERstate_old == HIGH) {
      LEDstate4 = !LEDstate4;
      digitalWrite(LED4, LEDstate4);      
    }
    TASTERstate_old = TASTERstate;
    TASTERmillis_old = millis();
  }
}

Wir haben nun drei „unabhängig“ voneinander blinkende LEDs, einen davon unabhängigen entprellten Taster und eine weitere unabhängige LED, die abhängig vom Taster umschaltet. Natürlich wirkt das nur auf uns so wie Multitasking, da alles in sehr kurzen Zeitspannen durchgeführt wird.

Timer Interrupts

Eine weitere Möglichkeit, auf abgelaufene Zeitintervalle zu reagieren, sind Timerinterrupts. Jeder Mikrocontroller ist damit unterschiedlich ausgestattet. Der ATmega328 besitzt drei solcher Timer, die jeweils einen Interrupt (Unterbrechung) auslösen können. Dabei wird ein Zähler gestartet, der mit dem Takt des Prozessors läuft. Überschreitet er seinen möglichen Maximalwert, kann an dieser Stelle der Interrupt ausgelöst werden, bevor er wieder bei 0 anfängt zu zählen. Da hierbei die Geschwindigkeit vom Systemtakt abhängt, kann man einen sogenannten Pre-Scaler verwenden. Damit wird der Systemtakt „herunterdividiert“ und der daraus errechnete neue Takt an den Timer übergeben.

Es ist auch möglich, einen eigenen Wert festzulegen, wann der Interrupt ausgelöst werden soll. Der Zähler zählt dann nur bis zum vorgegebenen Wert. Um einen wiederkehrenden Interrupt umzusetzen, wird der Autoload-Modus benötigt. Der stellt den Zähler automatisch zurück. Das alles wird mit Registern umgesetzt. Genauere Details finden Sie hier.

Wenn wir nun die delay()-Funktion durch solch einen sogenannten Hardwaretimer ersetzen, passiert folgendes:

Genau wie bei der zyklischen Zeitabfrage wird hier nicht pausiert. Es wird allerdings auch nicht geprüft, ob die gewünschte Zeitspanne überschritten ist. Das Hauptprogramm läuft in der Dauerschleife. Im „Hintergrund“ zählt der Timer seinen Zähler hoch. Wurde der (eingestellte) Maximalwert erreicht, wird das Programm unterbrochen (Interrupt) und eine spezielle Funktion aufgerufen. Die Interrupt Service Routine (ISR). Darin kann dann etwas ausgeführt werden, bevor sie wieder verlassen und das Hauptprogramm an der gleichen Stelle fortgesetzt wird. Dabei sollte man die stetige Regel beachten: nur so viel Befehle ausführen wie nötig und dabei so wenig wie möglich. Funktionsaufrufe aus großen Bibliotheken sollte man also eher nicht durchführen.

Wenn wir damit die LED im Sekundentakt blinken lassen möchten, müssen wir den Timerinterrupt initialisieren, eine ISR deklarieren und sie mit dem Interrupt verbinden. Beispielecode und noch ein wenig mehr Infos finden Sie hier. Immer dann, wenn der Timer abgelaufen ist, wird die LED umgeschaltet.

Sobald eine ISR aufgerufen wird, wird die CPU (bzw. Timer) angehalten und Interrupts werden deaktiviert. Somit funktionieren auch die Funktionen delay() oder millis() nicht in einer ISR (man kann jedoch noch den letzten Wert aus millis() auslesen). Variablen, denen man in einer ISR Werte zuweist, müssen global und volatile deklariert werden, wenn man deren Inhalt im restlichen Programm behalten möchte.

Da (16Bit) Timer auch für Pulsweitenmodulation (PWM) z.B. für Servos oder gedimmte LEDs verwendet werden, sind diese ebenfalls beeinträchtigt.

Dazu mal ein Gedankenspiel. Wir halten uns an die wichtigste Regel und ändern nur eine Variable in der ISR. Für unsere Zwecke reicht eine bool Variable, die von HIGH auf LOW, oder LOW auf HIGH geändert wird, jedoch sind auch Zählvariablen möglich. Wir fragen in unserem normalen Programmablauf nun regelmäßig ab, ob sich diese Variable irgendwann geändert hat (das nennt man auch „Polling“). Das machen wir an der Stelle, an der wir bisher die zyklische Abfrage gesetzt haben, um die LEDs blinken zu lassen. Unser Programm hat in der loop()-Funktion aktuell mehrere bedingte Abfragen und Befehlsaufrufe. Sobald der Timer abgelaufen ist, springt das Programm aus der loop() in die ISR.

An welcher Stelle der Sprung stattfindet, ist vorher nicht klar und zeigt schon das erste Problem. Der Wechsel unserer Variablen findet an unterschiedlichen Punkten statt. Manchmal direkt nach der bedingten Abfrage, oder manchmal auch davor. Es ist vorher nicht klar, wann der Interrupt auslöst. Erst wenn im Programmcode der Punkt erreicht wird, an dem wir die Variable abfragen, stellt das Programm die Änderung fest. Es kann aber in der Zwischenzeit im restlichen Programm schon einiges mehr passiert sein, bis die Abfrage erreicht wurde. Um dieses Problem zu beheben, kann man die LED in der ISR umschalten. Da jedoch die Funktion digitalWrite(), die für den Ausgang der LED verwendet wird, eben ein Funktionsaufruf ist, sollte man auch das lieber vermeiden. Es ist allerdings möglich. Alternativ kann man stattdessen direkt die Portregister beschreiben und so per Portmanipulation die GPIOs umschalten.

Das nächste Problem ist, dass wir nur drei Timer auf dem ATmega328 haben. Wir können auch nur einen Timer für jede LED verwenden. Was aber, wenn wir mehr davon brauchen, um noch mehr Zeitintervalle zu messen? Da erreichen wir dann die Grenzen. Eventuell könnte man mit Berechnungen die Timer aufteilen. Auch die ISRs und das Einrichten der Timer machen den Quellcode größer und am Ende macht das Programm genau das Gleiche, wie mit zyklischen Abfragen. Es ist also ratsam, für solche Aufgaben keine Interrupts zu verwenden. Daher zeige ich auch keinen Beispielcode.

Timerinterrupts haben natürlich nicht nur Nachteile. Wenn Ihr Programm zur Laufzeit blockiert, weil es auf einen Input wartet, ist das Pollen (also das wiederholte Abfragen) nicht sinnvoll. Dafür eignet sich dann der Interrupt, da er das Warten unterbrechen kann. Auch die Pause mit delay() kann durch den Interrupt unterbrochen werden. Wenn Sie mehr daran interessiert sind, schauen Sie doch mal auf diese Beispiele.

Die nicht-blockierende Ampel

Zum Abschluss dieses Beitrages zeige ich Ihnen noch einen Sketch, der eine Ampelsteuerung darstellt. Ich nehme die ersten drei LEDs als Ampel für die Autos. Den Taster für den Fußgänger, um zu signalisieren, dass er die Straße überqueren möchte. Die vierte LED soll für den Fußgänger blinken, als Bestätigung, dass er die Taste gedrückt hat („Signal kommt“). Außerdem füge ich zwei weitere LEDs für die Fußgängerampel hinzu.

6 LEDs 1 Taster

Normalerweise ist es die Warnampel für abbiegende Autos, die blinkt und die Ampel für den Überquerungswunsch leuchtet dauerhaft, bis der Fußgänger Grün hat. Ich habe das hier mal etwas verändert, da wir keine abbiegenden Autos haben und ich dennoch die verschiedenen Blinksignale zeigen möchte. Die Ampel reagiert auch sofort auf den Taster.

Die Funktion einer solchen Ampelphasenschaltung sollte uns aus dem realen Leben bekannt sein. Die Autoampel zeigt folgendes Verhalten:

  • Grün
  • Gelb
  • Rot
  • Rot und Gelb
  • Grün
  • während die Autoampel Grün ist, ist die Fußgängerampel rot
  • schaltet die Autoampel auf Rot, dauert es einen Moment, dann schaltet die Fußgängerampel auf Grün
  • schaltet die Fußgängerampel auf Rot, dauert es einen Moment, dann schaltet die Autoampel auf Rot und Gelb
  • Beginn von vorn

Diese Abläufe kann man gut in einer sogenannten Statemachine (oder Zustandsautomaten) abbilden. Das mache ich im Sketch mit einer switch-case-Anweisung. Die Variablen habe ich umbenannt, damit man z.B. die Farben der LEDs besser zuordnen kann.

Ampel_no_delay.ino

#define A_ROT 4
#define A_GELB 5
#define A_GRUEN 6
#define F_HALT 7
#define TASTER 8
#define F_ROT 9
#define F_GRUEN 10

unsigned long dauer_A_ROTGELB = 2000;
unsigned long dauer_A_GRUEN = 6000;
unsigned long dauer_A_GELB = 2000;
unsigned long dauer_A_F = 3000;
unsigned long dauer_F_GRUEN = 6000;
unsigned long dauer_F_A = 3000;

unsigned long oldMillis_Ampel = 0;

bool F_HALT_state = LOW;
bool F_HALT_blink = false;
unsigned long F_HALT_blinkdauer = 300;
unsigned long oldMillis_F_HALT = 0;

bool TASTERstate = HIGH;
bool TASTERstate_old = HIGH;

unsigned long prellZeit = 80;
unsigned long TASTERmillis_old = 0;
unsigned int ampelState = 0; void set_A_Ampel(bool A_ampel_rot, bool A_ampel_gelb, bool A_ampel_gruen) {  digitalWrite(A_ROT, A_ampel_rot);  digitalWrite(A_GELB, A_ampel_gelb);  digitalWrite(A_GRUEN, A_ampel_gruen); } void set_F_Ampel(bool F_ampel_rot, bool F_ampel_gruen) {  digitalWrite(F_ROT, F_ampel_rot);  digitalWrite(F_GRUEN, F_ampel_gruen); } void setup() {  pinMode(A_ROT, OUTPUT);  pinMode(A_GELB, OUTPUT);  pinMode(A_GRUEN, OUTPUT);  pinMode(F_HALT, OUTPUT);  pinMode(F_ROT, OUTPUT);  pinMode(F_GRUEN, OUTPUT);    pinMode(TASTER, INPUT_PULLUP);    digitalWrite(F_HALT, LOW);  set_F_Ampel(HIGH, LOW); } void loop() {  TASTERstate = digitalRead(TASTER);  if (ampelState <= 1 && TASTERstate != TASTERstate_old && millis() - TASTERmillis_old >= prellZeit) {    if (TASTERstate == LOW && TASTERstate_old == HIGH) {      F_HALT_blink = true;      ampelState = 2;         }    TASTERstate_old = TASTERstate;    TASTERmillis_old = millis();  }  if (F_HALT_blink && millis() - oldMillis_F_HALT >= F_HALT_blinkdauer) {    F_HALT_state = !F_HALT_state;    digitalWrite(F_HALT, F_HALT_state);    oldMillis_F_HALT = millis();  }  switch(ampelState) {    // Autoampel Gruen    case 0: {      set_A_Ampel(LOW, LOW, HIGH);      ampelState = 1;      oldMillis_Ampel = millis();    }; break;    case 1: {      if (millis() - oldMillis_Ampel >= dauer_A_GRUEN) {        ampelState = 2;      }    }; break;    // Autoampel Gelb    case 2: {      set_A_Ampel(LOW, HIGH, LOW);      ampelState = 3;      oldMillis_Ampel = millis();    }; break;    case 3: {      if (millis() - oldMillis_Ampel >= dauer_A_GELB) {        ampelState = 4;      }    }; break;    // Autoampel Rot    case 4: {      set_A_Ampel(HIGH, LOW, LOW);      ampelState = 5;      oldMillis_Ampel = millis();    }; break;    case 5: {      if (millis() - oldMillis_Ampel >= dauer_A_F) {        ampelState = 6;      }    }; break;    // Fussgaengerampel Gruen    case 6: {      set_F_Ampel(LOW, HIGH);      F_HALT_blink = false;      digitalWrite(F_HALT, LOW);      ampelState = 7;      oldMillis_Ampel = millis();    }; break;    case 7: {      if (millis() - oldMillis_Ampel >= dauer_F_GRUEN) {        ampelState = 8;      }    }; break;    // Fussgaengerampel Rot    case 8: {      set_F_Ampel(HIGH, LOW);      ampelState = 9;      oldMillis_Ampel = millis();    }; break;    case 9: {      if (millis() - oldMillis_Ampel >= dauer_F_A) {        ampelState = 10;      }    }; break;    // Autoampel Rot und Gelb    case 10: {      set_A_Ampel(HIGH, HIGH, LOW);      ampelState = 11;      oldMillis_Ampel = millis();    }; break;    case 11: {      if (millis() - oldMillis_Ampel >= dauer_A_ROTGELB) {        ampelState = 0;      }    }; break;        default: break;  } }

Sie können dieses Programm nun durch weiteren Quellcode erweitern. Die Ampel könnte für sich neben anderen Aufgaben laufen. Sie könnten z.B. Sensoren abfragen, oder Servos steuern.

Finale

Ich habe für Sie noch eine Variante erstellt, die eine Nachtschaltung enthält. Wie in der Realität blinken dann die gelben LEDs. Dafür muss man den Taster gedrückt halten.

Das Problem dabei ist, dass wenn man den Taster gedrückt hält, dann die Nachtschaltung aktiviert wird und man nicht loslässt, die Nachtschaltung wieder auf Normalbetrieb umschaltet. Also muss dieses Mal auch darauf geachtet werden, dass der Taster einmal losgelassen wird, bevor man wieder durch Gedrückthalten in den normalen Modus zurückkehrt. Ich habe zwei weitere Zustände in die switch-case-Anweisung eingefügt, die die gelben LEDs blinken lassen. Diese beiden Zustände wechseln sich endlos ab, bis man wieder den Taster gedrückt hält. Im Gegensatz zur gelben Fußgängerampel, die nur aktiviert werden kann, wenn die Autoampel rot ist, kann die Nachtschaltung jederzeit aktiviert und deaktiviert werden.

Den Sketch dazu finden Sie hier als Download mit erklärenden Kommentaren.

Alle gezeigten Arduino-Sketches finden Sie hier auch gesammelt als Download und inklusive beschreibenden Kommentaren.

Wie immer gilt: es ist nur eine Möglichkeit, es zu programmieren. Es gibt sicher noch andere Lösungen. Auch kann man den Code weiter verkürzen und optimieren.


Fazit

Verwenden Sie die delay()-Funktion nur dann, wenn Sie nicht auf Eingänge reagieren müssen. Ansonsten blockiert das Programm, was eventuell nicht gewollt ist. Auf einem Mikrocontroller, wie dem Nano V3 mit ATmega328p mit 16MHz Taktfrequenz, kann man zyklische Zeitabfragen für Zeitsteuerungen verwenden, ohne dass es zur Laufzeit auffällt, dass zwischendurch Befehle ausgeführt werden. Man erhält so ein nicht-blockierendes Programm mit flüssigem Ablauf.

Die Umsetzung wie hier gezeigt ist immer gleich: Zeit mit gewünschtem Intervall vergleichen. Wenn Intervall überschritten, dann irgendetwas tun und Zeit neu speichern. Ansonsten tu was Anderes.

Nicht-blockierendes Entprellen von Tastern kann man auf die gleiche Weise umsetzen.

Ich hoffe, ich konnte zum Thema Pausen etwas für Ihre nächsten Projekte beisteuern. Viel Spaß beim Nachbasteln.

Für arduinoProjekte für anfänger

10 commentaires

Andreas Wolter

Andreas Wolter

Einen entsprechenden Blog für Raspi und Python gibt es im Moment nicht. Das Prinzip lässt sich aber auch dorthin übertragen.
Es geht immer darum, einen bestimmten Codebereich innerhalb einer Dauerschleife erst dann zu betreten, wenn eine gewisse Zeit abgelaufen ist. In Python entspricht die delay() Funktion der Funktion sleep(). Die sollte man nicht verwenden, damit das Programm nicht blockiert. Es gibt einige Beiträge, die sich mit MicroPython beschäftigen und in denen auch gezeigt wird, wie man Timer o.ä. verwendet.

Grüße,
Andreas Wolter
AZ-Delivery Blog

m g

m g

Hallo,
sehr interessanter Beitrag.
Vielen Dank hierfür.
Wie sieht das in Python mit einem Raspberry Pi aus?
Hat jemand gute Informationsquellen für genau dieses Thema?
oder ist vielleicht ein entsprechender Blog geplant?

Reinhold

Reinhold

Vielen Dank für die Darstellung der vielen unterschiedlichen Unterbrechungen! Das ist sehr hilfreich!
Danke
Reinhold

Eckard

Eckard

Hier eine Lösung gegen den Überlauf.

/*
Delay Ersatzprogramm, Überlaufsicher
Wichtig für Programme die im Dauerlauf betrieben werden
Die Funktion " millis() hat nach ~49 Tagen einen Overflow, " micros() " nach ~71 Minuten.
Zur freien Verwendung!!!!!!!!!!!
*/

#define debug 1

unsigned long startzeit; // Millis
unsigned long akzeit; // Millis

// Für jede Verzögerung ein Variablen Paar
long pause1 = 0; // Zwischespeicher für abgelaufene Zeit
long PAUSE1 = 2000; // Verzögerungszeit 1 festlegen
long pause2 = 0; // Zwischespeicher für abgelaufene Zeit
long PAUSE2 = 200; // Verzögerungszeit 2 festlegen
long pause3 = 0; // Zwischespeicher für abgelaufene Zeit
long PAUSE3 = 15200; // Verzögerungszeit 3 festlegen
long pause4 = 0; // Zwischespeicher für abgelaufene Zeit
long PAUSE4 = 30000; // Verzögerungszeit 4 festlegen
// usw.

void setup() {
Serial.begin(9600);
while (!Serial) {
;
}

pinMode(LED_BUILTIN, OUTPUT); startzeit = millis();

}

void loop() {

akzeit = millis(); // Verzögerungszeiten PauseX+=LOOPZEIT; vor Überlauf if (startzeit < akzeit) { pause1 += (akzeit – startzeit); // Loopzeit aufaddieren pause2 += (akzeit – startzeit); // Loopzeit aufaddieren // usw. für jede Verzögerung startzeit = akzeit; } if (akzeit < startzeit) { /* Überlauf abfangen. Funktion " millis() " hat nach ~49 Tagen einen Überlauf.*/ pause1 += ((0xFFFFFFFF – startzeit) + akzeit); /* Loopzeit aufaddieren bei Überlauf — ( Restmillis bis Überlauf + aktuelle millis() nach Überlauf )*/ pause2 += ((0xFFFFFFFF – startzeit) + akzeit); // usw. für jede Verzögerung startzeit = akzeit; } // Verzögerungszeit ohne delay() if (pause1 >= PAUSE1) {

#ifdef debug
Serial.print("PAUSE1= “);
Serial.println(pause1);
digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN)); /* LED in den entgegengesetztn Zustand versetzen, LED-Statusvariable eingespart */
#endif
pause1 = 0;
//
// CODE
//
//
//
//
}
if (pause2 >= PAUSE2) {
#ifdef debug
Serial.print(”PAUSE2= ");
Serial.println(pause2);
digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN)); /* LED in den entgegengesetztn Zustand versetzen*/
#endif
pause2 = 0;
//
// CODE
//
//
//
}

}

Andreas Wolter

Andreas Wolter

Danke für die Hinweise.
@Tillomar: manchmal sieht man den Baum vor lauter Wäldern nicht. Beim Programmieren hat mich das tatsächlich schon stutzig gemacht, aber ich hab es nicht gesehen. Ich schau da nochmal drüber.

@Eckard: den Hinweis hatte ich vergessen. Das ist korrekt. Der Datentyp ist unsigned long mit einem Maximalwert von ca. 4,29 Mrd., was von ms in Tage umgerechnet ca 49,71 bedeutet. Somit hat man dann den Überlauf und die Subtraktion wird ein unpassendes Ergebnis liefern.

@Bernd: das ist eine gute Frage. Der verwendete Timer müsste theoretisch wieder von vorn starten. Man könnte einen Zähler einbauen ebenfalls mit unsigned long, der den Überlauf mitzählt. Den könnte man mit in die Berechnung einbeziehen. Dann hätte man 4,29 Mrd. mal 49,71 Tage. Das müsste reichen.

Ich füge Ihr Feedback als Update hinzu. Vielen Dank!

Grüße,
Andreas Wolter
AZ-Delivery Blog

Bernd

Bernd

Danke für die klare Darstellung der Thematik.
Welchen Ansatz würden Sie vorschlagen, wenn man den Code länger laufen lassen will und es zu einem Überlauf von millis() kommt?

Barremans

Barremans

A very good tutorial.
For beginners, using the millis function will take some effort.
But once you knowing it, you will see the benefits of it.
Interrupts is even higher level to learn, but certainly to learn when you want to make automated projects.
Thanks for this lovely tutorial.
Grtz Barre

Thomas

Thomas

Super erklärt. Vielen Dank dafür.

Eckard

Eckard

Ein Hinweis für Anfänger!
Wer mit millis() arbeitet und seinen Sketch länger als 49 Tage laufen lässt, muss den Überlauf von millis() abfangen.

Tillomar

Tillomar

In Blink_3LEDs_noDelay_taster_debounce_gedrueckthalten.ino und Ampel_no_delay.ino:

Die Abfrage von TASTERstate_old in
if (TASTERstate == LOW && TASTERstate_old == HIGH)…
ist unnütz, denn in der Bedingung zuvor:
if (TASTERstate != TASTERstate_old && millis() – TASTERmillis_old > prellZeit) …
wurde bereits sichergestellt, daß die Zustände unterschiedlich sind, und TASTERstate ist das Ergebnis von digitalRead(), kann also nur zwei Zustände annehmen.

Laisser un commentaire

Tous les commentaires sont modérés avant d'être publiés