Oftmals ist es notwendig, Aktionen in einer definierten Zeitfolge auszuführen. Ein typisches Beispiel ist das Blinken einer LED oder das regelmäßige Auslesen von Sensorwerten z.B. im Abstand von jeweils einer Sekunde. Hierzu gibt es verschiedene Varianten. In der folgenden Auflistung wird das Beispiel "Blinkende LED" in verschiedenen Versionen ausgeführt.

In­halts­ver­zeich­nis

Einfache Zeitschleife

Task-Scheduler

Interrupt

Software-Timer (nativ)

Arduino-Klasse Ticker

Arduino (ESP8266) Bibliothek Urs-ESP8266-Timer

Klasse UrsTickerBase

Klasse UrsOneShotBase

Versionshistorie

Download


Einfache Zeitschleife

Bei dieser Methode wird die Zeitverzögerung durch Einfügen eines Aufruf der Funktion delay() mit der notwendigen Verzögerungszeit aufgerufen. Dies ist auch die Variante, die in dem einfachen Programm Arduino Blink verwandt wird.

const uint32_t OnDuration  = 200;
const uint32_t OffDuration = 800;

// the loop function runs over and over again forever
void loop() {
  digitalWrite(LedPin, HIGH); // turn the LED on (HIGH is the voltage level)
  delay(OffDuration);         // wait
  digitalWrite(LedPin, LOW);  // turn the LED off by making the voltage LOW
  delay(OffDuration);         // wait
}

Durch Anpassung der Wartezeiten lässt sich das An-/Aus-Verhältnis abwandeln.

Der Große Nachteil dieser Methode ist, dass


Task-Scheduler

Über Systemzeit

Stellt das System eine System-Uhr zur Verfügung lassen sich über diese Aktionen zeitlich steuern. Man legt fest, zu welcher Zeit die Funktion ausgeführt werden muss und prüft regelmäßig ab, ob dieser Zeitpunkt erreicht ist. Bei periodisch auszuführenden Funktionen muss bei Ausführung der Aktion der Zeitpunkt der nächsten Ausführung festgelegt werden.

const uint32_t OnDuration  = 200;
const uint32_t OffDuration = 800;
uint32_t NextActionMillis;
bool LedState = false;

void loop()
{ DoAllTheThingsThatNeedToBeDone();

  // LED blinken
  if (millis() > NextActionMillis)
  { LedState = !LedState; // Umsachalten
    if (LedState)
    { digitalWrite(LedPin, HIGH); // LED einschalten
      NextActionMillis += OnDuration;
    }
    else
    { digitalWrite(LedPin, LOW); // LED ausschalten
      NextActionMillis += OffDuration;
    }
  }
}

Der Vorteil dieser Methode ist, dass die CPU nicht durch Wartschleifen blockiert wird. Für den Fall, dass DoAllTheThingsThatNeedToBeDone() sehr lange dauert, kann es sein, dass die Überprüfung der Systemzeit –und damit die Ausführung der Aktion– verspätet stattfindet.

Per Schleifenzählung

Steht keine Systemzeit zur Verfügung, kann man die Dauer der Hauptschleife abschätzen und Schleifendurchläufe zählen.

const uint32_t OnDuration  = 200;
const uint32_t OffDuration = 800;
uint32_t NextActionLoopCount;
uint32_t LoopCount = 0;
bool LedState = false;

void loop()
{ DoAllTheThingsThatNeedToBeDone();
  delay(1); // Bestimmt i.W. die Dauer der Schleife

  // LED blinken
  if (LoopCount++ > NextActionLoopCount)
  { LedState = !LedState;
    if (LedState)
    { digitalWrite(LedPin, HIGH); // LED einschalten
      NextActionLoopCount += OnDuration;
    }
    else
    { digitalWrite(LedPin, LOW); // LED ausschalten
      NextActionLoopCount += OffDuration;
    }
  }
}

Die Präzision dieser Methode hängt stark davon ab, wie konstant der Zeitverbrauch von DoAllTheThingsThatNeedToBeDone()  ist.


Interrupt

Soll die Aktion unabhängig von einer expliziten Ansteuerung im Hauptprogramm bietet es sich an, hierfür interne Zeitgeber zu nutzen, die nach Ablauf der eingestellten Zeit einen Interrupt auslösen. Der ESP8266 bietet hierfür einen Hardware- und einen Software-Timer. Es gibt nur einen Hardware-Timer. Dieser ist also eine kostbare Ressource und sollte der ESP8266-Bibloithek zur Verfügung stehen.

Software-Timer (nativ)

Eine elegante Möglichkeit, zeitgesteuert Funktionen auszuführen, ist die Nutzung eines Software-Timers. Dieser ist zwar ggf. nicht so präzise wie ein "echter" Hardware-Timer, dafür aber deutlich einfacher zu handhaben. Vor allen Dingen gibt es keine Konflikte mit anderen Funktionen, die ebenfalls auf die Hardware zugreifen wollen. Die Dokumentation sagt hierzu:

Please note that os_timer APIs listed below are software timers executed in task, hence timer callbacks may not be precisely executed at the right time; it depends on priority. If you need a precise timer, please use a hardware timer which can be executed in hardware interrupt.

Das ganze funktioniert so: Es wird eine verkettete Liste von Kontroll-Strukturen geführt, in der jeder Konsument des Timers verwaltet wird. Zu regelmäßigen Zeitpunkten wird diese Liste abgearbeitet. Für die Timer, deren Zeit abgelaufen ist, wird die hinterlegte Callback-Funktion aufgerufen. Für diese gilt, wie auch bei "normalen" Interrupts: Möglichst schnell beenden und auf Threadsicherheit achten.

Der native Zugriff auf diesen Timer erfolgt über das ESP8266 SDK API. Dieses stellt die nachfolgend beschriebenen Methoden zur Verwaltung der Timer zur Verfügung. Im Arduino-Umfeld sind die Prototypen des SDK in der Datei "user_interface.h" hinterlegt.

extern "C" {
#include "user_interface.h"
}
Funktion Beschreibung Anmerkung
Typ os_timer_t
ETSTIMER
struct _ETSTIMER_
Eine Variable dieses Typs dient zur Aufnahme der Verwaltungsfinformationen eines Software-Timers. Auf die Elemente dieses Typs wird nicht direkt zugegriffen. Die Verwaltung erfolgt über die restlichen Funktionen. Beispiel:
os_timer_t myTimer;

struct _ETSTIMER_ und ETSTIMER sind in ets_sys.h definiert, os_timer findet man in os_type.h

void os_timer_setfn(
os_timer_t *pTimer,
os_timer_func_t *pFunction,
void *pArg)
Die Funktion os_timer_setfn() dient zum Eintragen der Callback-Funktion in die Timer-Verwaltungsstruktur. Die Callback-Funktion hat den Prototyp void function(void *pArg).
pTimer: Zeiger auf die Timer-Verwaltungsstruktur
pFunction: Adresse der Callback-Funktion
pArg: Zeiger auf das Argument der Callback-Funktion (kann NULL sein)
Der übergebene Parameter pArg wird der Callback-Funktion zur Verfügung gestellt. Bespiel:
int counter = 0;

os_timer_setfn(&myTimer, timerCallback, &counter);

void timerCallback(void *pArg)
{ tickOccured = true;
  *((int *) pArg) += 1;
}
void os_timer_arm(
os_timer_t *pTimer,
uint32_t milliseconds,
bool repeat)
os_timer_arm() aktiviert den Timer.
pTimer: Zeiger auf die Timer-Verwaltungsstruktur
milliseconds: Timer-Intervall
repeat: gibt an, ob der Timer neu gestartet werden soll, sobald er Null erreicht hat (true: mit Wiederholung, false: One-Shot)
Beispiel:
Timer mit einer Periodendauer von 1 Sec:
os_timer_arm(&myTimer, 1000, true);
Timer, der nach 5 Sekunden einmalig ausgelöst wird:
os_timer_arm(&myTimer, 5000, false);
void os_timer_disarm (os_timer_t *ptimer) os_timer_disarm() deaktiviert den Timer.
pTimer: Zeiger auf die Timer-Verwaltungsstruktur
Beispiel:
os_timer_disarm(&myTimer);

Es besteht die Möglichkeit, das Timer-API auf Mikrosekunden umzustellen. Diese Einstellung gilt dann für alle Software-Timer.

Ein komplettes Beispielprogramm:

//
// ESP8266 Timer Beispiel
//
#include "Arduino.h"

extern "C" {
#include "user_interface.h"
}

os_timer_t Timer1;         // Verwaltungsstruktur des Timers
int Counter = 0;           // Argument für die Callback-Funktion
bool TickOccured = false;  // Flag, dass in der Callback-Funktion gesetzt wird
                           // Die aufwändigen und zeitintensiven Funktionen werden
                           // anhand dieses Flags im Hauptprogramm durchgeführt.


void timerCallback(void *pArg)
{ TickOccured = true;
  *((int *) pArg) += 1;
} 


void setup() 
{ Serial.begin(9600);
  Serial.println();
  Serial.println();
  Serial.println("ESP8266 Timer Test");
  Serial.println("--------------------------");
  TickOccured = false;
 
 os_timer_setfn(&Timer1, timerCallback, &Counter);
 os_timer_arm(&Timer1, 1000, true);
}

void loop()
{ if (TickOccured)
  { Serial.println();
    Serial.println("Tick Occurred");
    Serial.print("Millis: "); Serial.println(millis());
    Serial.print("Counter: "); Serial.println(Counter);
    TickOccured = false;
  }
 
  delay(0); 
}

Die Programmausgabe:

ESP8266 Timer Test
--------------------------

Tick Occurred
Millis: 1231
Counter: 1

Tick Occurred
Millis: 2231
Counter: 2

Tick Occurred
Millis: 3231
Counter: 3

Tick Occurred
Millis: 4231
Counter: 4

...

Arduino-Klasse Ticker

Im GitHub-Verzeichnis des ESP8266 Community Forum findet man die Library Ticker. Diese kapselt die Struktur ETSTIMER und stellt mit diversen Varianten die Methoden attach() und detach() zum Initialisieren der Ticker-Instanz bereit. Die Ticker-Funktion und ggf. notwendige Parameter müssen außerhalb der Klasse bereit gestellt werden.


Arduino (ESP8266) Bibliothek Urs-ESP8266-Timer

Diese Bibliothek stellt zwei Klassen zur Kapselung der ESP8266-Timer. UrsTickerBase dient als Basisklasse für sich wiederholende Aktionen und UrsOneShotBase für einmalig auszuführende Aktionen.

Klasse UrsTickerBase

UrsTickerBase kapselt den Timer vollständig. Sowohl benötigte Daten als auch die Timer-Funktion sind Teil der Klasse.

 UrsTickerBase ist abstrakt und muss zur Implementierung der Callback-Funktion abgeleitet werden. Folgende öffentlichen und geschützten Methoden werden bereit gestellt:

class UrsTickerBase
{ ... 

  public:
    uint32_t getInterval() {return _Interval;}; // Liefert das aktuell eingestellte Intervall in ms.
    void setInterval(uint32_t milliSeconds);    // Setzt das Intervall auf einen neuen Wert.
    void start();                               // Startet den Ticker. Ggf. Neustart.
    void start(uint32_t milliSeconds);          // Startet den Ticker (ggf. Neustart) mit dem angegebenen Intervall
    void stop();                                // Stoppt den Ticker
    bool isRunning() {return _isRunning;};      // True: der Ticker ist aktiv.

  protected:
    UrsTickerBase(uint32_t milliSeconds); // Initialisiert eine neue Instanz der UIrsTickerBase-Klasse mit dem angegebenen Intervall.
    ~UrsTickerBase() { stop(); }          // Stoppt den Ticker.
    virtual void timerCallback() = 0;     // Funktion, die beim Ablauf des Timers aufgerufen wird.
};

UrsTickerBase verwenden

Die Verwendung ist recht einfach. Die Klasse wird abgeleitet und dabei die Methode timerCallback() implementiert. Notwendige Variablen und Methoden werden in die Klasse eingefügt.

Das folgende Beispiel soll bewirken, dass ein Zähler in Abständen von einer Sekunde hochgezählt wird. Jedes Mal, wenn dies geschehen ist, soll der neue Wert und der Zeitpunkt der Änderung über die RS232-Schnittstelle ausgegeben werden.

Damit der Zeitbedarf der Interrupt-Routine möglichst klein bleibt, wird dort nur der Zähler erhöht und die Zeit gespeichert. Die Ausgabe erfolgt im Hauptprogramm.

Das Ganze ist in der Klasse CounterTicker gekapselt.

#include <UrsTickerBase.h>

class CounterTicker : public UrsTickerBase
{ private:
    void timerCallback();      // Callback-Funktion für diese Klasse.
    int counter = 0;           // Zähler mit Initialwert 0.
    unsigned long TickMillis;  // Wert von millis() zum Zeitpunkt des Ticks.

  public:
    CounterTicker(uint32_t milliSeconds) // Initialisiert eine neue Instanz der CounterTicker-Klasse.
      : UrsTickerBase(milliSeconds) {};
    bool tickOccured;                    // True, wenn ein Tick ausgelöst wurde (muss vom umgebenden Programm gelöscht werden).
    void printResult();                  // Aktuellen Zustand ausgeben.
};

Die Implementierung der Callback-Funktion und der Zustandsausgabe ...

#include "CounterTicker.h"

// Callback-Funktion für CounterTicker
void CounterTicker::timerCallback()
{ counter++;             // Zähler erhöhen
  TickMillis = millis(); // Zeit merken
  tickOccured = true;    // Markierung setzen
}

// Ausgabe des Zustands
void CounterTicker::printResult()
{ Serial.print("TickMillis: "); Serial.println(TickMillis);
  Serial.print("Counter: ");    Serial.println(counter);
}

... und des Hauptprogramms:

#include "CounterTicker.h"

CounterTicker Ct(1000);

void setup() 
{ Serial.begin(74880);

  Serial.println();
  Serial.println("--------------------------");
  Serial.println("URS Ticker Example");
  Serial.println("--------------------------");

  Ct.start(); // Ticker starten
}

void loop() 
{ if (Ct.tickOccured)        // Zeit abgelaufen
  { Ct.tickOccured = false;  // Markierung löschen
    Serial.println("Tick Occurred");
    Ct.printResult();
  }

  delay(0);
}

Die zugehörige Programm-Ausgabe:

--------------------------
URS Ticker Example
--------------------------
Tick Occurred
TickMillis: 19030
Counter: 1
Tick Occurred
TickMillis: 20030
Counter: 2
Tick Occurred
TickMillis: 21030
Counter: 3
...

UrsTickerBase Implementierung

Erwähnenswert ist eigentlich nur die Implementierung der Callback-Funktion. Das ESP8266 SDK API ist nicht objektorientiert. Deshalb muss eine Wrapper-Funktion (UrsTickerBaseCallbackWrapper) den Transfer ins objektorientierte Umfeld leisten. Damit dies geschehen kann, wird bei der Initialisierung einer Instanz dieser Klasse die Wrapper-Funktion als Callback-Funktion angegeben und als Parameter für die Callback-Funktion wird ein Zeiger auf die Instanz (this) festgelegt.

os_timer_setfn(&_osTimer, UrsTickerBaseCallbackWrapper, this);

Wenn nun der Timer die Callback-Funktion aufruft, wird als Parameter ein Zeiger auf die betroffen Instanz von UrsTickerBase übergaben und die Funktion kann die entsprechende Funktion der Klasse aufrufen.

void UrsTickerBaseCallbackWrapper(void *pUrsTickerBase) 
{ UrsTickerBase * p = (UrsTickerBase *) pUrsTickerBase; 
  p->timerCallback();
}

Da timerCallback eine virtuelle Funktion ist, wird die korrekte, klassenspezifische Funktion aufgerufen.

Weitere Details können dem kommentierten Quellcode entnommen werden (s. Download).


Klasse UrsOneShotBase

Bei der Klasse UrsOneShotBase wird die Callback-Funktion, getriggert durch Aufruf von start(), nur ein einziges Mal nach Ablauf der vorgegebenen Zeit aufgerufen ("MonoFlop").

Da bei jedem Aufruf von start() der Timer neu geladen wird, ist das Verhalten "nachtriggerbar".

Das API:

class UrsOneShotBase
{ ...

  public:
    void start(uint32_t milliSeconds);       // Startet Timer.
    void stop();                             // Stoppt den Timer. Entlädt os_timer. 
    bool isRunning() { return _isRunning; }; // True, wenn auf die Ausführung eines Schusses gewartet wird.

  protected:
    UrsOneShotBase();
    ~UrsOneShotBase() { stop(); }            // Entlädt den Timer, unterbricht das Warten.
    virtual void timerCallback() = 0;        // Funktion, die beim Ablauf des Timers aufgerufen wird.
};

UrsOneShotBase verwenden

Die Verwendung ist entsprechend der von UrsTickerBase. Als Beispiel wird eine Klasse vorgestellt, die eine LED für eine bestimmte Zeit anschaltet.

#include <UrsOneShotBase.h>

class Flasher : public UrsOneShotBase
{ private:
    void timerCallback();
    uint8_t _ledPin;

 public:
   Flasher(uint8_t LedPin) : UrsOneShotBase() { _ledPin = LedPin; };
   void flash(uint32_t Duration);
};

Der Klasse wird im Konstruktor die Nummer des Pins mitgegeben, an den die LED nageschlossen. Die Funktion flash schaltet den Pin in den Output-Modus und legt in auf High-Potential. Nach Ablauf der angegebenen Zeit wird in der Callback-Funktion der Pin wieder auf Low-Potential gelegt.

#include "Flasher.h"

void Flasher::flash(uint32_t Duration)
{ pinMode(_ledPin, OUTPUT);
  digitalWrite(_ledPin, HIGH);
  start(Duration);
}

void Flasher::timerCallback()
{ digitalWrite(_ledPin, LOW);
}

Das Hauptprogramm:

#include "Flasher.h"

Flasher LedFlash(2); // LED liegt an Pin #2.

void setup()
{
}

void loop()
{ LedFlash.flash(200); // LED 200 ms leuchten lassen.
  delay(1000);         // 1 Sekunde zwischen den Blitzen.
}

Beachte: Der Abstand zwischen zwei Blitzen ist 1.000 ms (nicht 1.200 ms).

UrsOneShotBase Implementierung

Die Implementierung ist im Wesentlichen identisch mit der von UrsTickerBase. Die zusätzliche Zustandsvariable _isArmed ist notwendig, weil nicht wie bei UrsTickerBase anhand von _isRunning eindeutig erkannt werden kann, ob der Timer geladen ist.

Weitere Details können dem kommentierten Quellcode entnommen werden (s. Download).

Versionshistorie

Datum Version Änderung
2016-05-04 1.0 Basisversion
2019-03-21 1.1 Die Angabe der Intervalle erfolgt in ms. Umbenennung der Funktionsparameter vom microSeconds in milliSeconds.
2019-12-14 1.2
  • Die Zugiffsstufe der internen Variablen von 'private' nach 'protected' geändert.
  • _isRunning', '_isArmed' als 'volatile' deklariert.

 

Download

Das ZIP-Archiv für Bibliothek UrsTickerBase zum Download. (enthält auch UrsOneShotBase). Die entpackten Dateien ins Verzeichnis <user>\Documents\Arduino\libraries kopieren (siehe Installing Additional Arduino Libraries).

Das Archiv enthält die Bibliotheksdateien