Rothschopf Werner

rowex ganz privat

Angebote

Arduino: LED oder Relais per Tastendruck einschalten und mit Zeitablauf abschalten

Auf dieser Seite erarbeiten wir einen Arduino Sketch um mit einem Tastendruck einen Ausgang (und einer daran angeschlossenen LED oder Relais) einzuschalten und nach dem Loslassen des Tasters soll der Ausgang noch einige Zeit aktiv bleiben. Ähnlich wie der Lüfter auf dem PC: auch wenn das Licht bereits abgeschalten ist, läuft der Lüfter nach.

Ausgehend von einem typischen Anfänger-Sketch verbessern wir den Sketch zu einer nicht-blockierdenden Variante unter Verwendung von millis(). Anschließend lernen wir den Umgang mit Arrays und abschließend steigen wir in die Objekt Orientierte Programmierung (OOP) ein. Das ganze in einer knappen Stunde.

Anfänger: Blockierende Variante

Beginnt man mit der Arduino Programmierung, kommt man vermutlich relativ schnell auf diesen Ablauf:

  • Den Button mit digitalRead auslesen
  • Wenn Button gedrückt
    • LED einschalten
    • 3 Sekunden warten
    • Die LED wieder ausschalten

Ein einfacher Sketch dazu könnte so aussehen:

/*
  by noiasca 
  http://werner.rothschopf.net
*/

const byte buttonPin = A0;     // a GPIO for the button, the button will connect the GPIO to GND
const byte outputPin = 2;      // a GPIO as output, a LED or a Relais

void setup() {
  pinMode(buttonPin, INPUT_PULLUP);
  pinMode(outputPin, OUTPUT);
}

void loop() {
  if (digitalRead(buttonPin) == LOW) {
    digitalWrite(outputPin, HIGH);
    delay(3000); // dirty delay
    digitalWrite(outputPin, LOW);
  }
}

Der Sketch funktioniert, aber der Arduino ist die meiste Zeit mit "warten und nichts tun" beschäftigt. Mit jedem Tastendruck ist der Arduino für 3 Sekunden blockiert und man kann den Sketch nicht einfach für mehrere Buttons erweitern. Wir ahnen es schon: wir müssen das delay() elimieren.

Die Verkabelung gestaltet sich einfach: Der Taster schließt GPIO A0 gegen GND. Daher können wir die internen pullups nutzen. An den Ausgang kann eine LED mit Vorwiderstand oder ein Relais-Modul angeschlossen werden. Solltest du andere GPIOs verwenden reicht die Änderung der Konstante.

Nicht blockierende Variante nach "BlinkWithoutDelay"

Mit der Arduino IDE kommt das Beispiel 02. Digital | BlinkWithoutDelay. Dieses zeigt wie wir die Zeitfunktion millis() nutzen können, um nicht blockierenden Code zu schreiben. Während BlinkWithoutDelay die LED sowohl ein wie auch ausschaltet, werden wir den Pin per Tastendruck einschalten und nach Zeitablauf abschalten.

/*
  by noiasca 
  http://werner.rothschopf.net
*/

const byte buttonPin = A0;     // a GPIO for the button, the button will connect the GPIO to GND
const byte outputPin = 2;      // a GPIO as output, a LED or a Relais
uint32_t previousMillis = 0;   // the timestamp when we have pressed the button

void setup() {
  pinMode(buttonPin, INPUT_PULLUP);
  pinMode(outputPin, OUTPUT);
}

void loop() {
  if (digitalRead(buttonPin) == LOW) {
    digitalWrite(outputPin, HIGH);
    previousMillis = millis();   // save the time when we switched on the output
  }
  if (millis() - previousMillis > 3 * 1000UL && digitalRead(outputPin) == HIGH)  // if time has passed and the output is still HIGH
  {
    digitalWrite(outputPin, LOW);
  }
}

Auch in dieser Variante wird mit digitalRead der Button eingelesen. Wir warten nun aber nicht mit delay(), sondern merken uns mit previousMillis = millis(); sozusagen die aktuell Uhrzeit *).

Das zweite if prüft, zwei Bedingungen:
millis() - previousMillis > 3 * 1000UL
ist die jeweils aktuelle Uhrzeit *) minus der gemerkten Uhrzeit größer als 3 Sekunden. Das UL hinter 1000 wird wichtig sobald wir größere Zeiträume als 32767 Millisekunden benötigen. Mit UL wird sichergestellt, dass der Arduino diesen Term als unsigned long berechnet.

digitalRead(outputPin) == HIGH stellt sicher, dass wir den Funktionsblock der IF Anweisung nur dann ausführen, wenn der Output auch tatsächlich noch eingeschalten ist.

Beide Bedingungen verknüpfen wir mit dem logischen UND (&&). Sind beide Bedingungen wahr, wird der Output abgeschaltet.

Somit haben wir unseren Sketch auf eine nicht blockierende Variante umgeschrieben.

*) "aktuelle Uhrzeit" ist es natürlich nicht ganz korrekt. millis() gibt die Millisekunden seit Start des Arduinos zurück.

Eine eigene Funktion für unser Nachlaufrelais

Damit der loop() kurz und übersichtlich bleibt, empfiehlt es sich, das Auslesen des Buttons sowie das Zeitmanagement des Ausgangs in eine eigene Funktion auszulagern. Nennen wir die Funktion "tickButtonLed()". "tick" sowie das ticken einer mechanischen Uhr. Man könnte auch run... oder do... verwenden. Die Analogie auf die Uhr gefällt mir wegen der Verwendung von millis():

/*
  by noiasca 
  http://werner.rothschopf.net
*/

const byte buttonPin = A0;     // a GPIO for the button, the button will connect the GPIO to GND
const byte outputPin = 2;      // a GPIO as output, a LED or a Relais

void tickButtonLed()
{
  static uint32_t previousMillis = 0;  // the timestamp when we have pressed the button
  if (digitalRead(buttonPin) == LOW) {
    digitalWrite(outputPin, HIGH);
    previousMillis = millis();         // save the time when we switched on the output
  }
  if (millis() - previousMillis > 3 * 1000UL && digitalRead(outputPin) == HIGH)  // if time has passed and the output is still HIGH
  {
    digitalWrite(outputPin, LOW);
  }
}

void setup() {
  pinMode(buttonPin, INPUT_PULLUP);
  pinMode(outputPin, OUTPUT);
}

void loop() {
  tickButtonLed();
}

Was wir auch sehen, wir ändern mit static uint32_t previousMillis = 0; den Scope der Variable. previousMillis muss nicht global sein. Sie wird nur von der Funktion tickButtonLed() benötigt. Normalerweise geht der Wert von lokalen Variablen nach Verlassen einer Funktion verloren. In diesem Fall benötigen wir den Wert jedoch auch beim nächsten Funktionsaufruf für die Überprüfung, ob die Zeit abgelaufen ist. Wir müssen daher diese Variable static (statisch) machen. Damit bleibt der Wert auch nach Verlassen der Funktion erhalten.

Zwei Buttons - zwei LEDs

Wir hätten nun alle Vorrausetzungen um mehrere Buttons mehrere LEDs ansteuern zu lassen. Die GPIO Definitionen ergänzen, das setup() erweitern,  die Funktion duplizieren und einen neuen Namen vergeben, sowie die neue Funktion auch im loop() aufrufen.

Auch wenn ein derartiger Code funktioniert, müssen wir festhalten, dass sich allerlei Code-Duplikate im Sketch ergeben würden. Dazu muss es doch eine bessere Variante geben.

Viele Buttons - viele LEDs: Der einfache Umgang mit Arrays

Mit Hilfe von Arrays wird es sehr einfach, gleichartige Daten zu verwalten. Weiters können wir mitttels for Schleifen auf Arrays bequem zugreifen. Schauen wir uns diesen Code an:

/*
  by noiasca 
  http://werner.rothschopf.net
*/
const byte buttonPin[] {A0, A1, A2}; // GPIOs for the buttons, the button will connect the GPIO to GND
const byte outputPin[] {2, 3, 4};    // GPIOs as output, LEDs or relais
const byte groups = sizeof(buttonPin);  // "count" how many pin groups are used

void tickButtonLed()
{
  static uint32_t previousMillis[groups];  // the timestamp when we have pressed the button
  for (byte i = 0; i < groups; i++)
  {
    if (digitalRead(buttonPin[i]) == LOW) {
      digitalWrite(outputPin[i], HIGH);
      previousMillis[i] = millis();   // save the time when we switched on the output
    }
    if (millis() - previousMillis[i] > 3 * 1000UL && digitalRead(outputPin[i]) == HIGH)  // if time has passed and the output is still HIGH
    {
      digitalWrite(outputPin[i], LOW);
    }
  }
}

void setup() {
  for (byte i = 0; i < groups; i++)
  {
    pinMode(buttonPin[i], INPUT_PULLUP);
    pinMode(outputPin[i], OUTPUT);
  }
}

void loop() {
  tickButtonLed();
}

Statt den einzelnen Variablen definieren wir für die GPIOs zwei Arrays:

const byte buttonPin[] {A0, A1, A2}; 
const byte outputPin[] {2, 3, 4};

In den geschweiften Klammern intialisieren wir das Array mit den Pins. D.h. unsere beiden Arrays haben je eine Größe von 3 Elementen. Am Beispiel von outputPin sehen wir uns an, wie der Controller intern die Werte behandelt:

outputPin[0] = 2;
outputPin[1] = 3;
outputPin[2] = 4;

Kommen weitere Buttons und LEDs hinzu, reicht es, die entsprechenden Pins im Array zu ergänzen. Der Kompiler kümmert sich darum das Array groß genug zu machen.

Im Code werden wir öfters die Größe des Arrays benötigen. Wir schreiben die Größe des Array aber nicht hardcoded in the Sketch, sondern lassen dies automatisch ermitteln:  const byte groups = sizeof(buttonPin);
In groups steht nun die Größe des Arrays 3 zur Verfügung.

Springen wir kurz in das setup(): Mit Hilfe der for Schleife for (byte i = 0; i < groups; i++) legen wir für jeden Pin den pinMode fest. Da groups gleich 3 ist, können wir durch die Indexe 0, 1 und 2 iterieren. Ähnlich verfahren wir auch im tickButtonLed(). Mit Hilfe for (byte i = 0; i < groups; i++) greifen wir auf jeden definiertenButton Pin bzw. LED Pin zu.

Der Sketch skaliert sehr einfach: es ist lediglich notwendig, je einen buttonPin und einen ledPin im Array zu ergänzen. Im restlichen Sketch sind keinerlei Änderungen mehr notwendig. Der Sketch wächst mit dem Bedarf mit. Eine Änderung in der Logik (z.B. eine andere Nachlaufzeiten) findet sich ein einziges Mal im Code - und - der Sketch ist weiterhin nicht blockierend. Du brauchst zusätzlich eine blinkende LED - kein Problem, ergänze den Code und lass eine separate LED blinken. Vorrausetzung - auch die Blinkled ist nicht blockierend.

Variablen zu Strukturen (struct) verbinden

Betrachtet man obigen Sketch, stellt man fest, dass die Arrays buttonPin[] und outputPin[] in enger Beziehung stehen. Jede Button-LED Kombination braucht je ein Element dieser beiden Arrays. Genau genommen haben wir noch ein drittes Array: previousMillis[].  In C++ kann man (auch unterschieldliche) Variablen zu einer Struktur verbinden. Es handelt sich um eine Struktur, weil die Daten in einer bestimmten Art und Weise angeordnet und verknüpft werden, um den Zugriff auf sie und ihre Verwaltung effizient zu ermöglichen.

/*
  topic: struct
  by noiasca
  http://werner.rothschopf.net
*/

struct Groups {
  const byte buttonPin;
  const byte outputPin;
  uint32_t previousMillis;
};

Groups group[] {
  {A0, 2, 0},  // index 0: button Pin, outputPin, previousMillis
  {A1, 3, 0},
  {A2, 4, 0}
};

const byte noOfgroups = sizeof(group) / sizeof(group[0]); // "count" how many pin groups are used

void tickButtonLed()
{
  for (byte i = 0; i < noOfgroups; i++)
  {
    if (digitalRead(group[i].buttonPin) == LOW) {
      digitalWrite(group[i].outputPin, HIGH);
      group[i].previousMillis = millis();         // save the time when we switched on the output
    }
    if (millis() - group[i].previousMillis > 3 * 1000UL && digitalRead(group[i].outputPin) == HIGH)  // if time has passed and the output is still HIGH
    {
      digitalWrite(group[i].outputPin, LOW);
    }
  }
}

void setup() {
  for (byte i = 0; i < noOfgroups; i++)
  {
    pinMode(group[i].buttonPin, INPUT_PULLUP);
    pinMode(group[i].outputPin, OUTPUT);
  }
}

void loop() {
  tickButtonLed();
}

Zunächst müssen wir die Struktur definieren. Nach dem Keyword struct vergeben wir einen Namen. Namen von Strukturen sollen mit einem Großbuchstabe beginnen. Anschließend definieren wir die Variablen die in dieser Struktur verwaltet werden:

struct Groups {
  const byte buttonPin;
  const byte outputPin;
  uint32_t previousMillis;
};

Nach Anlage der Struktur benötigen wir auch eine konkrete Variable. In unserem Falle wieder ein Array da wir mehrere Gruppen verwalten möchten. In den geschweiften Klammern Initialisieren wir jedes Element: Es finden sich wieder die einzelnen verwendeten Pins.

Groups group[] {
  {A0, 2, 0},  // index 0: button Pin, outputPin, previousMillis
  {A1, 3, 0},
  {A2, 4, 0}
};

Die Anzahl an Gruppen lassen wir wieder automatisch ermitteln. Dazu benötigt es einen kleinen Trick. sizeof(group) liefert uns nicht 3 zurück, sondern 18. Warum? Zunächst besteht die Struktur aus 3 Variablen der Größen 1byte + 1byte + uint32_t = 6. Dann haben wir aktuell 3 Elemente angelegt, daher 3 * 6 = 18. So wie wir mit sizeof(group) die Gesamtgröße ermitteln können, können wir auch mit sizeof(group[0]) die größe des ersten Elements ermitteln. Ein Element hat 6 Byte. Mit einer einfachen Division 18 / 6 kommen wir auf unsere 3 Button-LED Kombinationen. Daher der Code:

const byte noOfgroups = sizeof(group) / sizeof(group[0]); // "count" how many pin groups are used

Die for Schleife ändert sich (von der Variablenbennennung abgesehen) nicht.

Der Zugriff auf die einzelnen Variablen in der Gruppe ist relativ einfach, in i haben wir wieder die Indexe 0, 1, 2 und können daher in jedem Durchlauf auf die Variablen zugreifen:

group[i].buttonPin
group[i].outputPin
group[i].previousMillis

Mit der Struktur haben wir weitere Ordnung in unsen Code gebracht und einen großen Schritt in die Richtung der objektorientierung gemacht. Aber bleiben wir noch etwas bei der prozeduralen Programmierung

Abschließendes Beispiel: range based for Loop für Arrays

Jetzt haben wir schon mehrmals sizeof(array) verwendet um die Größe von Arrays zu ermitteln. Meistens brauchen wir das nur für for Schleifen. Mit C++11 wurden sogennante Range based for Loops eingeführt die eine weitere Vereinfachung ermöglichen. Wieder ein Beispiel:

/*
  topic: range based for loop
  by noiasca
  http://werner.rothschopf.net
*/

struct Groups {
  const byte buttonPin;
  const byte outputPin;
  uint32_t previousMillis;
};

Groups group[] {
  {A0, 2, 0},  // index 0: button Pin, outputPin, previousMillis
  {A1, 3, 0},
  {A2, 4, 0}
};

void tickButtonLed()
{
  for (auto & i : group)
  {
    if (digitalRead(i.buttonPin) == LOW) {
      digitalWrite(i.outputPin, HIGH);
      i.previousMillis = millis();   // save the time when we switched on the output
    }
    if (millis() - i.previousMillis > 3 * 1000UL && digitalRead(i.outputPin) == HIGH)  // if time has passed and the output is still HIGH
    {
      digitalWrite(i.outputPin, LOW);
    }
  }
}

void setup() {
  for (auto & i : group)
  {
    pinMode(i.buttonPin, INPUT_PULLUP);
    pinMode(i.outputPin, OUTPUT);
  }
}

void loop() {
  tickButtonLed();
}

Zunächst fällt auf, dass wir keinen Zähler noOfGroups mehr brauchen.

Dann kommt die erste range based for loop:

  for (auto & i : group)

Wir haben wieder eine Variable i. Diese ist jedoch nicht vom Typ byte, sondern erhält "automatisch" den Typ der Variable group. Weiters legen wir keine Kopie des einzelnen group elements an, sondern wir erstellen mit dem & lediglich eine Referenz auf das jeweilige Element.  D.h. bei jedem Durchgang - für jedes Element des Array group - steht die Referenz (i) zur Verfügung. i referenziert beim ersten Durchang auf group[0], dann auf group[1], und schließlich auf group[2]. Daher können wir in der range based for loop vereinfacht auf die Variablen von group zugreifen:

i.buttonPin 
i.outputPin
i.previousMillis

Gleich verfahren wir im setup beim Festlegen von pinMode.

Schlussendlich sparen wir uns mit der range based for loop eine Variable sowie eine Menge Schreibarbeit.

Damit endet der Teil 1 der prozeduralen Programmierung und ich möchte auf den Teil 2: Objektorientierung verweisen.

Zusammenfassung

  • Wir haben schrittweise einen Sketch mit delay() auf nicht blockiernde Programmierung mit Hilfe von millis() umgebaut
  • Anschließend haben wir gelernt, gleichartige Daten in Arrays zu halten
  • Dann haben wir uns an Strukturen versucht
  • um sie dann noch einfacher einer range based for loop zu behandeln

Links

Werbung: Die mit Sternchen (*) gekennzeichneten Verweise sind sogenannte Provision-Links (Affiliate). Wenn du auf so einen Verweislink klickst und über diesen Link einkaufst, bekomme ich von deinem Einkauf eine Provision. Für dich verändert sich der Preis nicht.


Protokoll

First upload: 2020-04-08 | Version: 2020-04-09