Arduino in C++: Blink

Beispiele rund um die LED auf den Arduino-Boards.

Die LED auf Arduino-Boards ist zwischen Masse (GND) und einem Pin angeschlossen. Wie oben im Bild zu sehen, ist der dazugehörige Pin unter der Konstante LED_BUILTIN abrufbar.


Table of Contents


Wie lässt man diese LED nun blinken?

Um die LED ein- und wieder ausschalten zu können, muss der Pin LED_BUILTIN in den Ausgangsmodus OUTPUT gewechselt werden.

Ausgänge sind Wechselschalter, die zwischen GND und VCC umschalten können. Man erhält an dem Pin eine (geringfügig belastbare) Verbindung entweder zur Masse (0 V) oder zur Betriebsspannung (5 V).

Im Programm können Ausgänge dann jederzeit umgeschaltet werden und man spricht dabei von den Zuständen LOW (0) und HIGH (1).

Wie, wann und wo ändert man den Modus?

Der Modus eines Pins kann jederzeit geändert werden und wird durch den Aufruf von pinMode(pin, mode) festgelegt. Diese ändert daraufhin ein Bit im zuständigen Data Direction Register (DDRx):

pinMode(LED_BUILTIN, OUTPUT);

// ohne Arduino:
DDRB = DDRB | bit(5);

Da sich der Modus des Pins im Laufe des Programms nicht ändern wird, kann dieser Aufruf in die Routine setup geschrieben werden. Diese wird nur einmal beim Start des Programms aufgerufen:

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
}

void loop() {
}

Das obige Programm ändern den Modus für LED_BUILTIN von INPUT (Vorgabe) auf OUTPUT. Die LED leuchtet noch nicht.

Wie schaltet man einen Ausgang um?

Der Zustand eines Ausgangs kann jederzeit geändert werden. Die Funktion digitalWrite(pin, state) ändert ein Bit im zuständigen Port Register (PORTx):

digitalWrite(LED_BUILTIN, HIGH);

// ohne Arduino:
PORTB = PORTB | bit(5);

Da der Zustand fortlaufend umgeschaltet werden soll wird dies in die loop Routine geschrieben. Diese wiederholt sich in einer Endlosschleife:

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
}

void loop() {
  digitalWrite(LED_BUILTIN, HIGH);
}

Nun ist LED_BUILTIN ein Ausgang und mit der Betriebsspannung verbunden - die LED leuchtet dauerhaft auf.

Der Aufruf von digitalWrite dauert circa 8 Takte und würde sich somit 2 Millionen mal pro Sekunde wiederholen (16 MHz Quarz). Praktisch passiert jedoch nichts, da sich der Zustand des PORTx Registers in der Zwischenzeit nicht ändert.

Wenn man von HIGH wieder nach LOW umschaltet, dann ist der Stromkreis wieder unterbrochen und die LED leuchtet nicht mehr. In der Hauptroutine schalten wir den Ausgang nun mittels digitalWrite ein und wieder aus. 

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
}

void loop() {
  digitalWrite(LED_BUILTIN, HIGH);
  digitalWrite(LED_BUILTIN, LOW);
}

Nun wird dies jedoch so schnell geschehen, dass die LED nicht aufleuchten wird.

Wie verzögert man den Programmablauf?

In einfachsten Abläufen kann man Verzögerungen mittels delay(ms) einbauen. Diese Funktion hält den Programmablauf an und führt die Ausführung erst nach Ablauf der übergebenen Anzahl von Millisekunden fort:

delay(1000); // 1000 ms = 1 Sekunde abwarten

Um die LED abwechselnd für 1 Sekunde ein- und auszuschalten könnte man also 2 Verzögerungen einsetzen:

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
}

void loop() {
  digitalWrite(LED_BUILTIN, HIGH);
  delay(1000);
  digitalWrite(LED_BUILTIN, LOW);
  delay(1000);
}

Oder man könnte auch folgendes schreiben:

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
}

void loop() {
  // Abfragen
  bool leuchtet = digitalRead(LED_BUILTIN);
  // Umschalten
  digitalWrite(LED_BUILTIN, leuchtet ? LOW : HIGH);
  // Warten
  delay(1000);
}

Sollen mehrere Aufgaben gleichzeitig abgearbeitet werden oder mit anderen Bausteinen kommuniziert werden, dann kann die delay(ms)-Funktion zu Problemen führen.

Im nächsten Abschnitt geht es nun darum eine Lösung für dieses Problem zu finden.


Warten ohne zu blockieren

Anstatt den Programmablauf anzuhalten um abzuwarten, muss eine andere Lösung gefunden werden. Eine Möglichkeit, die den Rahmen dieses Artikels sprengen würde, wäre die Verwendung von ISR. Praktikabler ist das Zählen der verstrichenen Zeit um bei Bedarf im Programmablauf zu verzweigen. Sehen wir uns folgendes Beispiel an:

const unsigned long now = millis();
if ((now - last) >= interval) {
  last = now;
  // mindestens 1 Sekunde verstrichen..
}

Die Funktion millis() gibt die Anzahl der Millisekunden zurück, die seit dem Start des Programms vergangen sind. Sie wird verwendet, um den aktuellen Zeitpunkt zu erfassen.

Die aktuelle Zeit (in ms) wird in der Konstanten now gespeichert. Typ unsigned long ist notwendig, da millis() große Zahlen liefert. Arduino kann somit bis 49 Tage in Millisekunden zählen und fängt dann wieder bei 0 an.

Es wird geprüft, ob die Differenz zwischen der aktuellen Zeit (now) und einer gespeicherten Zeit (last) größer oder gleich einem festgelegten Intervall (interval) ist. Das Intervall wird typischerweise in Millisekunden angegeben (z.B. 1000 für 1 Sekunde).

Wenn die Bedingung erfüllt ist, wird last mit dem aktuellen Zeitpunkt (now) aktualisiert. Somit wird der Mechanismus zurückgesetzt, um von vorn zu beginnen.

Und wie geht das jetzt?

Zuerst definieren wir eine Konstante (const) für den Intervall und erstellen eine Variabel um den letzten Zeitpunkt festzuhalten. Üblicherweise schreibt man diese an den Anfang der Datei, beispielsweise vor die setup Routine. Der Rest besteht daraus die Verzweigung im Programmablauf einzuschreiben:

const unsigned long interval = 1000UL;
unsigned long last = 0;

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
}

void loop() {
  // ... andere Tätigkeiten
  //

  // Nach Ablauf der Zeit die LED umschalten
  const unsigned long now = millis();
  if ((now - last) >= interval) {
    last = now;
    digitalWrite(LED_BUILTIN, digitalRead(LED_BUILTIN) ? LOW : HIGH);
  }
}

Mehrere LEDs blinken lassen

Neben LED_BUILTIN sollen nun auch LED_RX und LED_TX blinken.

Als erstes schauen wir, oben im Bild, an welchen Pins die LEDs angeschlossen sind und notieren dieses im Quelltext. LED_RX ist an D4 und LED_TX an D5:

const byte LED_RX = 4; 
const byte LED_TX = 5;

Dann legen wir fest in welchen Zeitabständen die LEDs umschalten sollen:

const unsigned long rxInterval = 250UL;
const unsigned long txInterval = 500UL;
const unsigned long ledInterval = 1000UL;

Den Zeitpunkt des letzten Umschaltens speichern wir getrennt für jede LED:

unsigned long rx = 0; 
unsigned long tx = 0;
unsigned long led = 0;

Außerdem schreiben wir uns diesmal zwei Helfer um etwas Tipparbeit zu sparen. Diese Prozedur prüft den Zustand eines Ausgangs und schaltet diesen dementsprechend um:

void toggle(const byte pin) {
  digitalWrite(pin, digitalRead(pin) == LOW ? HIGH : LOW);
}

Wobei die folgende Routine das Kernstück dieses Beispiels darstellt. Es wird der übergebene Pin umgeschaltet, wenn die übergebene Anzahl von Millisekunden abgelaufen ist. Der Zeitpunkt des letzten Umschaltens wird unter der übergebenen Adresse(*) zwischengespeichert:

void blink(const byte pin, 
           const unsigned long interval, 
           unsigned long* last) {
  const unsigned long now = millis();
  if ((now - *last) >= interval) {
    *last = now;
    toggle(pin);
  }
}

Der Rest ist dann ganz einfach. Pins als Ausgänge festlegen und in der Hauptroutine kontinuierlich die Unteroutinen aufrufen.

void setup() {
  pinMode(LED_RX, OUTPUT);
  pinMode(LED_TX, OUTPUT);
  pinMode(LED_BUILTIN, OUTPUT);
}

void loop() {
  blink(LED_RX, rxInterval, &rx);
  blink(LED_TX, txInterval, &tx);
  blink(LED_BUILTIN, ledInterval, &led);
}

Der komplette Quelltext an einem Stück:

// LED_RX ist an D4 und LED_TX an D5.
const byte LED_RX = 4; 
const byte LED_TX = 5;

// In welchem Interval sollen die LEDs umschalten?
const unsigned long rxInterval = 250UL;
const unsigned long txInterval = 500UL;
const unsigned long ledInterval = 1000UL;

// Zeitpunkt des letzten Umschaltens
unsigned long rx = 0; 
unsigned long tx = 0;
unsigned long led = 0;

// Die folgende Prozedur schaltet einen Pin um.
void toggle(byte pin) {
  digitalWrite(pin, digitalRead(pin) == LOW ? HIGH : LOW);
}

// Die folgende Prozedur schaltet pin nach interval um.
void blink(const byte pin, const unsigned long interval, const unsigned long* last) {
  const unsigned long now = millis();
  if ((now - *last) >= interval) {
    *last = now;
    toggle(pin);
  }
}

void setup() {
  pinMode(LED_RX, OUTPUT);
  pinMode(LED_TX, OUTPUT);
  pinMode(LED_BUILTIN, OUTPUT);
}

void loop() {
  blink(LED_RX, rxInterval, &rx);
  blink(LED_TX, txInterval, &tx);
  blink(LED_BUILTIN, ledInterval, &led);
}

USB Morse Blinker

Mit den vorausgegangen Erkenntnissen soll nun ein praktisches Beispiel umgesetzt werden. Ein über das Terminal übermittelter Punkt-Strich-Code soll über die LED abgespielt werden.

Wir legen fest, dass maximal 80 Zeichen eingegeben werden können und die Eingabe mit ENTER abgeschlossen werden muss. Nach Beendigung der Wiedergabe soll eine Meldung im Terminal ausgegeben werden und es kann eine erneute Eingabe erfolgen. Falls während der Wiedergabe eine Eingabe erfolgen sollte, dann wird die Wiedergabe gestoppt und die vorherige Eingabe überschrieben.

Bei einem Punkt leuchtet die LED kurz, bei einem Strich lang. Alle anderen Zeichen werden ignoriert und erzeugen eine kurze Pause. Nach der Wiedergabe erfolgt eine lange Pause und die Wiedergabe beginnt erneut.

 

Related post

AVR in C: Blink

Dieser Artikel zeigt, wie man die LED an einem Arduino-Board ohne Verwendung…