Programmieren ohne ‚delay‘

Eines der einfachsten Programme für einen Mikroprozessor ist der „Blink“-Sketch, der dementsprechend für die meisten Einsteiger Grundlage der ersten eigenen Programme ist:

// the setup function runs once when you press reset or power the board
void setup() {
  // initialize digital pin LED_BUILTIN as an output.
  pinMode(LED_BUILTIN, OUTPUT);
}
// the loop function runs over and over again forever
void loop() {
  digitalWrite(LED_BUILTIN, HIGH);   // turn the LED on (HIGH is the voltage level)
  delay(1000);                       // wait for a second
  digitalWrite(LED_BUILTIN, LOW);    // turn the LED off by making the voltage LOW
  delay(1000);                       // wait for a second
}

Eine LED wird im Sekunden-Takt ein- und ausgeschaltet, die Befehlskette lautet einfach

  •  LED einschalten
  •  1000 Millisekunden warten
  •  LED ausschalten
  •  1000 Millisekunden warten …

 Danach geht´s wieder von vorne los.

Versucht der Programmier-Einsteiger nun, dieses Programm z.B. um weitere LEDs zu erweitern, so stößt er schnell auf die Problematik der Abhängigkeit und erhält auf seine Fragen die lapidare Antwort „schau´ Dir ‚BlinkWithoutDelay an‘ – da ist das Prinzip erklärt“.

Zwar ist m.E. im zugehörigen Tutorial https://www.arduino.cc/en/Tutorial/BlinkWithoutDelay das Prinzip tatsächlich recht anschaulich erklärt – der Einsteiger hält den Aufwand aber zunächst nicht für gerechtfertigt, denn das Problem lässt sich ja vielleicht auch mit mehreren ‚delay‘-Befehlen lösen:

Anstelle der Befehlskette

  • Pizza in die Mikrowelle
  • 10 Minuten warten
  • Pizza aus der Mikrowelle holen
  • eMail prüfen

prüft man z.B. alle 2 Minuten die eMails und kommt zu der Befehlskette

  • Pizza in die Mikrowelle
  • eMail prüfen
  • 2 Minuten warten
  • eMail prüfen
  • 2 Minuten warten
  • eMail prüfen
  • 2 Minuten warten
  • eMail prüfen
  • 2 Minuten warten
  • eMail prüfen
  • 2 Minuten warten
  • Pizza aus der Mikrowelle holen

Wenn man sich das bildlich vorstellt, wird schnell klar, dass so niemand arbeitet und dass das spätestens dann nicht mehr funktioniert, wenn tatsächlich eine eMail kommt, oder man parallel dazu noch eine Tasse Tee (drei Minuten) zubereiten möchte.

Schon in dem einfachen ‚Blink‘-Programm steht als Kommentar „the loop function runs over and over again forever” – und genau darum geht es: Die Ausführung des Programms nicht anzuhalten, sondern ganz gezielt Befehle auszuführen, wenn sie dran sind. Man stellt die Pizza in die Mikrowelle und merkt sich die Startzeit, gießt den Tee auf und merkt sich die Startzeit und prüft regelmäßig, ob die 10 Minuten für die Pizza oder die 3 Minuten für den Tee abgelaufen sind.

Genau das macht ‚BlinkWithoutDelay‘ nun für die LED – es prüft endlos, ob die Zeit bis zum nächsten Umschalten abgelaufen ist und wenn das Ergebnis „ja“ lautet, wird es ausgeführt:

void loop() {
  unsigned long currentMillis = millis();             // aktuelle Zeit bestimmen
  if (currentMillis - previousMillis >= interval) {   // Zeitintervall abgelaufen ?
  previousMillis = currentMillis;                     // Schaltzeitpunkt speichern
  if (ledState == LOW) {                              // LED umschalten
    ledState = HIGH;
    }
  else{
    ledState = LOW;
  }
  digitalWrite(ledPin, ledState);
  }
}

Beim Blinken einer LED sieht man nun leider tatsächlich keinerlei Vorteil – das Programm erscheint dem Einsteiger nur viel unübersichtlicher. Ich habe es daher mal für 5 LEDs an einem ATtiny85 erweitert. Für den Einsatz auf einem Arduino sind natürlich auch mehr als 5 LEDs möglich und die Portnummern anzupassen. Das Programm ist nur unwesentlich größer, kann aber nun 5 LEDs unabhängig mit verschiedenen Frequenzen und unsymmetrisch (High-Low-Verhältnis) blinken lassen, weil pausenlos nacheinander für alle LEDs geprüft wird, ob sie geschaltet werden müssen – das ist mit Verwendung von ‚delay‘ so nicht mehr zu realisieren.

int ledZahl = 5;                                   // Anzahl der LEDs
int ledPin[] = {0, 1, 2, 3, 4};                    // Pins der LEDs
unsigned long currentMillis = 0;
unsigned long previousMillis[] = {0, 0, 0, 0, 0};
int interval[] = {750, 800, 850, 950, 1000};       // Blink-Intervalle
int highlowq[] = {5, 5, 5, 2, 2};
void setup() {
  for (int i = 0; i < ledZahl; i++) {
    pinMode(ledPin[i], OUTPUT);
  }
}
void loop() {
  currentMillis = millis();
  for (int i = 0; i < ledZahl; i++) {     if(currentMillis - previousMillis[i] > interval[i]) {
      previousMillis[i] = currentMillis;
      digitalWrite(ledPin[i], HIGH);
    }
    else if(currentMillis - previousMillis[i] > interval[i]/highlowq[i]) {
      digitalWrite(ledPin[i], LOW);
    }
  }
}

Der prinzipielle Aufbau eines Programms ohne ‚delay‘ unterscheidet sich also grundlegend von Befehlsketten mit ‚delay‘ – und deshalb kann man ein Programm auch kaum „mal eben umschreiben“. Bei der Erstellung eines Programms ist es also sinnvoll, schon von Anfang an möglichst ohne ‚delay‘ zu arbeiten – sowohl um das Prinzip zu lernen, als auch um später bei Erweiterungswünschen nicht wieder komplett von vorne beginnen zu müssen.