Startseite

Multitasking mit Arduino

Multitasking bedeutet, dass ein Gerät mehrere Aufgaben gleichzeitig erledigen kann, so wie eine Fabrik mit mehreren Fließbändern unterschiedliche Produkte gleichzeitig herstellen kann. Die Fließbänder entsprechen im Programm den Threads, und die Arbeitsschritte an den Bändern entsprechen im Programm den Tasks.

Da ich immer wieder gefragt werde, wie man Multitasking auf Mikrocontrollern ohne Betriebssystem implementiert, habe ich hier ein konkretes Beispiel für Arduino aufgeschrieben. Das Prinzip lässt sich ebenso mit anderen Programmiersprachen umsetzen.

Das folgende Beispielprogramm (Sketch) lässt drei LEDs unterschiedlich schnell blinken. Die Methode nennt man "Zustandsautomat" oder "Endlicher Automat", auf Englisch: "state machine".

#define LED_ROT   2    // Pin PD2
#define LED_GELB  3    // Pin PD3
#define LED_GRUEN 4    // Pin PD4

void setup() 
{
  pinMode(LED_ROT,  OUTPUT);
  pinMode(LED_GELB, OUTPUT);
  pinMode(LED_GRUEN,OUTPUT);
}

void thread_rot()
{
    static enum {AUS, WARTE_AUS, EIN, WARTE_EIN} status=AUS;
    static unsigned long int warteSeit;
    switch (status)
    {
        case AUS:
            digitalWrite(LED_ROT,LOW); // LED aus schalten
            warteSeit=millis();
            status=WARTE_AUS;
            break;

        case WARTE_AUS:
            if (millis()-warteSeit >= 100) // wenn 100ms verstrichen sind
            {
                status=EIN;
            }
            break;

        case EIN:
            digitalWrite(LED_ROT,HIGH); // LED ein schalten
            warteSeit=millis();
            status=WARTE_EIN;
            break;

        case WARTE_EIN:
            if (millis()-warteSeit >= 100) // wenn 100ms verstrichen sind
            {
                status=AUS;
            }
            break;
    }
}

void thread_gelb()
{
    // genau der gleiche Code wie für die rote LED, aber mit 250ms.
}

void thread_gruen()
{
    // genau der gleiche Code wie für die rote LED, aber mit 400ms.
}

void loop() 
{
    thread_rot();
    thread_gelb();
    thread_gruen();
}
Die wichtigste Eigenschaft dieser Threads ist, dass sie niemals blockieren. Sie hängen nie in Warteschleifen fest und sie verzichten auf die delay() Funktion. Bei jedem Aufruf führt der Thread nur einen kleinen Task aus, der nur wenige Mikrosekunden in Anspruch nimmt.

Beim ersten Aufruf ist der Thread im Status AUS. Im zugehörigen Task wird die LED aus geschaltet und dann die aktuelle Zeit in der Variable warteSeit gespeichert. Dann wird zum nächsten Status WARTE_AUS weiter geschaltet.

Beim zweiten Aufruf ist der Thread im Status WARTE_AUS. Im zugehörigen Task wird geprüft, ob die gewünschte Wartezeit erreicht (oder gar überschritten) wurde. Wenn das der Fall ist, wird auf den nächsten Status EIN weiter geschaltet.

Beim dritten Aufruf ist die gewünschte Zeit noch nicht erreicht, daher bleibt der Thread im Status WARTE_AUS.

Beim vierten Aufruf ist die gewünschte Zeit noch nicht erreicht, daher bleibt der Thread im Status WARTE_AUS.

Beim fünften Aufruf ist die gewünschte Zeit noch nicht erreicht, daher bleibt der Thread im Status WARTE_AUS.
...
Irgendwann ist es dann so weit - der Status wechselt nach EIN.

Beim darauf folgenden Aufruf wird die LED eingeschaltet und zum Status WARTE_EIN gewechselt. Dann wird erneut gewartet, bis 100ms verstrichen sind. Danach beginnt der Thread wieder von oben mit dem Status AUS

Der entscheidende Trick ist, dass die Thread-Funktion niemals hängen bleibt. Jeder einzelne Aufruf dauert nur wenige Mikrosekunden. In der Hauptschleife loop() kannst du daher sehr viele solcher Threads aufrufen. Es sieht von aussen betrachtet so aus, als ob sie gleichzeitig laufen würden.

Das Schlüsselwort "static" vor den lokalen Variablen sorgt dafür, dass sie ihren Wert zwischen mehreren Funktionsaufrufen nicht vergessen.

Bei der Abfrage der verstrichenen Zeit ist es wichtig, dass die Variable (warteSeit) den gleichen Integer Typ hat, wie der Rückgabewert von millis() und dass man die Differenz mit einer Subtraktion bildet. Diese Berechnung funktioniert sogar korrekt, wenn der Zeit-Zähler von seinem Maximal-Wert auf 0 über läuft. Bei anderen Formeln, die auf Addition beruhen, klappt das nicht.

Für Leute, die Arduino nicht kennen

Die millis() Funktion liefert die aktuelle Systemzeit in Millisekunden. Sie beruht auf einem Hardware-Timer, in dessen Interrupthandler eine Variable fortlaufend hoch gezählt wird.

Die setup() Funktion wird einmal beim Programmstart ausgeführt. Danach wird die loop() Funktion unendlich oft immer wieder ausgeführt. Arduino verhält sich so, als ob die main() Funktion des Programmes so aussehen würde:

int main()
{
    setup();
    for (;;) // forever
    {
        loop();
    }
}