Zyklen für Mikrocontroller

Immer wieder kommt es vor, dass μC kontrollierte Vorgänge in einem festen Zyklus aufgerufen werden sollen. Die oftmals gewählte Lösung lautet dann:

  • void loop()
  • {
  • // do anything
  • delay(500);
  • }

In Zeile 4 wird 500 ms gewartet. Das Programm ist geblockt.

Der Knackpunkt an dieser Lösung ist, dass mit dem Delay der Controller 'einschläft' und keine weiteren Aufgaben übernehmen kann. Auf diese Weise ist es nicht möglich, dass als Beispiel zwei LED mit unterschiedlichen Frequenzen blinken können. Dies ist suboptimal und sollte korrigiert werden.

Besser wäre es, wenn Funktionen mit unterschiedlicher Taktung auch unabhängig voneinander aufgerufen werden. Was interessiert die Funktion A, wann Funktion B irgendetwas tun soll. Gar nichts! Die Lösung muss in einer Entkopplung der Funktionen liegen.

Ein leicht zu lösendes Problem ist die Zeiterfassung. Mikrocontroller auf denen das Arduino-Framework läuft können mit dem Befehl 'millis()' die seit dem Start des μC vergangene Zeit in Millisekunden abrufen. Die Genauigkeit zunächst nicht bekannt, aber das hat vorerst keine Relevanz. Wichtig ist es, dass überhaupt die Position auf einem Zeitstrahl abgerufen werden kann. Um die Details können wir uns später kümmern.

Eine mögliche Lösung dazu lautet:

  • const int intervalLength = 500;
  • unsigned long nextCycle;
  •  
  • void setup()
  • {
  • nextCycle = millis();
  • }
  •  
  • void loop()
  • {
  • if (millis() > nextCycle) {
  • // do anything
  • nextCycle += intervalLength;
  • }
  • }

In Zeile 1 wird die Variablen 'intervalLength' mit dem Zeitintervall in ms initialisiert.
'nextCycle' in Zeile 2 ist für den Zeitstempel gedacht, in dem der Zyklus das nächste Mal ausgeführt werden soll.
Die im Setup liegende Zeile 6 initialisiert 'nextCycle' auf den aktuellen Zeitstempel, sodass der erste Zyklus sofor ausgeführt wird. Falls eine Verzögerung gewünscht ist, muss an dieser Stelle 'nextCycle' mit einem Zeitstempel initialisiert werden, der entsprechend in der Zukunft liegt.
Nach diesen Vorbereitungen wird im Loop (Zeilen 9 bis 15) die 'delay'-Anweisung durch eine If-Bedingung (Zeilen 11 bis 24) ersetzt. In Zeile 11 wird geprüft, ob der Zyklus abgearbeitet werden soll. Falls nicht, fährt das Programm einfach mit der Anweisung nach dem If-Block fort. Sollte der Zyklus reif sein, wird er ausgeführt und zum Schluss der Zeitstempel für die nächste Ausführung aktualisiert.

Mit diesem Snippet sind wir einen Schritt näher an der Lösung angekommen. Mehrere Kopien dieses Konstrukts sind in der Lage, unterschiedliche Funktionen in einem festen Takt vollkommen unabhängig voneinander aufzurufen. Das Handling ist jedoch ziemlich kompliziert und sollte vereinfacht werden. Wünschenswert wäre eine einfache Abfrage, ob der Zyklus abgelaufen ist und die Funktion ausgeführt werden soll. So in der Art:

  • If (ZyklusAbgelaufen)
  • {
  • // do anything
  • }

An diesem Punkt angekommen, bleibt nichts weiteres übrig, als die Zyklussteuerung in eine eigene Klasse zu packen. Die Anforderungen sind klar umrissen. Es muss:

  • eine Zykluszeit festegelegt
  • der aktuelle Zeitstempel erfasst
  • Die Prüfung auf Ablauf des Zyklusses berechnet
  • die Funktion aufgerufen
werden können.

Auf die Programmierung von Klassen gehe ich jetzt nicht detailiert ein. Dies würde den Rahmen des Beitrages sprengen. Ich nenne die Klasse 'Interval'.
Der Code lautet:

Verwendung in der Main-Methode

Dateiname main.cpp
  • #include <Arduino.h>;
  • #include "interval.h";
  •  
  • unsigned long actMillis;
  • unsigned long *pActMillis;
  • Interval t1;
  •  
  • void setup() {
  • Serial.begin(9600);
  •  
  • actMillis = millis();
  • pActMillis = &actMillis;
  • t1 = Interval(pActMillis, 2500);
  • t1.start();
  • }
  •  
  • void loop() {
  • actMillis = millis();
  •  
  • if (t1.tick()) {
  • Serial.print(" - actual Millis: ");
  • Serial.print(actMillis);
  • Serial.println(" - t1 -> *** tick");
  • }
  • }

Zeile 4 und 5 enthalten eine Variable und dessen Pointer für einen Zeitstempel. Zeile 6 deklariert eine Instanz der Intervall-Klasse. Die Datenstrukturen sind somit angelegt und müssen 'nur' noch in Betrieb gesetzt werden.

In der Setup-Funktion wird der Zeitstempel initialisiert und dem Pointer die Adresse des Zeitstempels übergeben (Zeile 11 und 12). Jeder Instanz einer Intervall-Klasse wird dieser Pointer übergeben. Dies hat den Vorteil, dass es bei der Verwendung mehrerer Intervall-Instanzen zu keinen Zeitverschiebungen kommt, die Probleme bei der Synchronisierung der Intervalle verursachen könnten.

Zum Schluss wird der Intervall gestartet.

In der Loop-Funktion wird in Zeile 18 zuerst der Zeitstempel aktualisiert. Die Zeilen 20 bis 24 zeigen die Verwendung der Intervall-Klasse. Die Methode 'tick()' wird für einen Zyklus true, wenn die Intervall-Zeit angelaufen ist. Dies wird über eine If-Anweisung abgefragt und zur Ausführung der gewünschten Aktionen verwendet. Der Einfachheit halber wird in dem Beispiel nur der Zeit ausgegeben.

Header-Datei

Dateiname interval.h
  • #pragma once
  •  
  • class Interval
  • {
  • private:
  • bool isRunning;
  • unsigned long endTime;
  • unsigned long *pActTime;
  •  
  • public:
  • unsigned int intervalLength;
  •  
  • Interval(void);
  • Interval(unsigned long *pActTime, unsigned int intervalLength);
  •  
  • void start(void);
  • void stop(void);
  • bool getIsRunning(void);
  •  
  • bool tick(void);
  • unsigned long getEndTime(void);
  • unsigned long getActTime(void);
  • };

Die Headerdatei gibt einen schnellen Einblick über die Funktionen der Intervall-Klasse. Zur Instanziierung wird der Konstruktor aus Zeile 14 benötigt. Mit ihm werden der Pointer für den Zetstempel und die Zykluszeit übergeben.

Das Intervall lässt sich starten und stoppen; der Run-State ist auslesbar. Die Tick-Methode wurde als zentrales Element im Rahmen der Main-Funktion angesprochen.

Die Endzeit (Zeile 21) ist der Zeitstempel, an dem das Intervall das nächte Mal abläuft. Die aktuelle Zeit (Zeile 22) wird unter anderem gebraucht, wenn das Intervall in einer anderen Klasse als Zeitgeber verwendet werden soll. Beide Zeiten sind nach außen hin selbstredend readonly.

Cpp- Datei

Dateiname interval.cpp
  • #include <Arduino.h>
  • #include "interval.h"
  •  
  • Interval::Interval(void) { }
  •  
  • Interval::Interval(unsigned long *pActTime, unsigned int intervalLength)
  • {
  • isRunning = false;
  • this->>pActTime = pActTime;
  • this->intervalLength = intervalLength;
  • }
  •  
  • void Interval::start(void) {
  • endTime = *pActTime + intervalLength;
  • isRunning = true;
  • }
  •  
  • void Interval::stop(void) {
  • isRunning = false;
  • }
  •  
  • bool Interval::getIsRunning(void) {
  • return isRunning;
  • }
  •  
  • bool Interval::tick(void)
  • {
  • if (isRunning && (endTime < *pActTime))
  • {
  • endTime += intervalLength;
  • return true;
  • }
  •  
  • return false;
  • }
  •  
  • unsigned long Interval::getEndTime(void) {
  • return endTime;
  • }
  •  
  • unsigned long Interval::getActTime(void) {
  • return *pActTime;
  • }

Im Konstruktor werden die Zeiten gesetzt. Die aktuelle Version lässt keinen Autostart zu (isRunning = false). Es wäre jedoch denkbar mit einem weiteren Parameter den Startzustand mitzugeben. Auf diese Weise könnte eine weitere Anweisung in der Setup-Funktion eingespart werden.

Beim Starten wird das Ende des nächsten Zyklus und der RunState gesetzt. Die Methoden 'stop()' und 'getIsRunning()' sind selbsterklärend, wie auch 'getEndTime()' und 'getActTime()'.

Die Methode 'tick()' bestimmt, ob der Zyklus abgelaufen ist und gibt in dieser Situation für einen Zyklus ein true zurück. Dies geschiht nur, wenn das Intervall läuft und die aktuelle Zeit größer ist, als der Zeitstempel für das Ende des Intervalls. Um möglichst gleich lange Intervalle zu generieren, wird zum letzten Endzeitpunkt die Intervalllänge addiert. Sollte es zu Verzögerungen kommen, werden die auf diese Weise ausgeglichen.

Die nächtbeste Version das Problem unterschiedlicher Zyklen zu lösen wäre, sich mit der Threading-Klasse zu verwenden. Die zugehörige Doku befindet sich auf der Microsoft-HP unter (Link zur Doku)

©2018 | www.klaus-sucker.de | Template von www.on-mouseover.de/templates/