Multitasking in C und Arduino

Ich zeige hier, wie man Mikrocontroller so programmiert, dass ein Programm mehrere Aufgaben (Threads) parallel abarbeiten kann. Das Beispiel basiert auf Arduino, doch es lässt sich ebenso mit anderen Entwicklungsumgebungen und Programmiersprachen umsetzen.

Einleitung

Dieses kleine Programm verdeutlicht den Knackpunkt:

#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 loop() {
    digitalWrite(LED_ROT, LOW);  // rote LED aus schalten
    delay(100);
    digitalWrite(LED_ROT, HIGH); // rote LED ein schalten
    delay(100);
}

Das Arduino Framework ruft die setup() Funktion einmal auf, und danach immer wieder loop() in einer Endlosschleife.

Dieses Programm lässt nur die rote LED blinken. Stelle dir nun vor, du sollst alle drei LEDs unabhängig voneinander in unterschiedlichen Intervallen blinken lassen. Mit dem obigen Ansatz ist das nicht machbar, denn du bräuchtest dazu drei Loops, die parallel ausgeführt werden. Dafür haben sich zwei Lösungen haben sich etabliert:

Präemptiv mit Betriebssystem Das Betriebssystem unterbricht laufende Threads, um Rechenzeit an andere Vorgänge zu vergeben. Bei jedem Wechsel muss das Betriebssystem den Zustand des laufenden Threads sichern, um ihn später beim Fortsetzen wieder herzustellen. Dazu reserviert das Betriebssystem für jeden Thread einen eigenen Stack. Im Mikrocontroller Umfeld sind diese Betriebssysteme unter dem Namen "Real Time Operating System" (RTOS) bekannt.
Kooperativ mit Zustandsautomaten Zustandsautomaten merken sich ihren Zustand selbst und geben von sich aus möglichst viel Rechenzeit an andere Threads ab. Sie werden auch "Endliche Automaten" (EA) oder "Finite State Machines" (FSM) genannt. Für kleine Mikrocontroller eignet sich die diese Variante besser, weil sie nur einen gemeinsam genutzten Stack benötigt, also weniger RAM.

Multitasking LED-Blinker

Die drei LEDs sollen unabhängig voneinander blinken:

Wir brauchen dazu drei Threads, die parallel abgearbeitet werden. Jeder Thread wird als separater Zustandsautomat implementiert. Im folgenden Beispiel zeige ich, wie man das mit Hilfe der switch/case Anweisung machen kann.

Quelltext

#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, EIN} zustand = AUS;
    static unsigned long int warteSeit = 0;

    switch (zustand) {
        case AUS:
            if (millis() - warteSeit >= 100) {   // Bedingung: nach 100 ms
                digitalWrite(LED_ROT, HIGH);     // Reaktion: LED ein schalten
                warteSeit = millis();  
                zustand = EIN;                   // Nächster Zustand: EIN
            }          
            break;

        case EIN: 
            if (millis() - warteSeit >= 100) {   // Bedingung: nach 100 ms
                digitalWrite(LED_ROT, LOW);      // Reaktion: LED aus schalten 
                warteSeit = millis();  
                zustand = AUS;                   // Nächster Zustand: AUS
            }          
            break;
    }
}

void thread_gelb() {
    static enum {AUS, EIN} zustand = AUS;
    static unsigned long int warteSeit = 0;

    switch (zustand) {
        case AUS:   
            if (millis() - warteSeit >= 253) {
                digitalWrite(LED_GELB, HIGH); 
                warteSeit = millis();  
                zustand = EIN; 
            }          
            break;

        case EIN: 
            if (millis() - warteSeit >= 253) {
                digitalWrite(LED_GELB, LOW); 
                warteSeit = millis();  
                zustand = AUS; 
            }          
            break;
    }
}

void thread_gruen() {
    static enum {AUS, EIN} zustand = AUS;
    static unsigned long int warteSeit = 0);

    switch (zustand) {
        case AUS:   
            if (millis() - warteSeit >= 378) {
                digitalWrite(LED_GRUEN, HIGH); 
                warteSeit = millis();  
                zustand = EIN; 
            }          
            break;

        case EIN: 
            if (millis() - warteSeit >= 378) {
                digitalWrite(LED_GRUEN, LOW); 
                warteSeit = millis();  
                zustand = AUS; 
            }          
            break;
    }
}

void loop() {
    thread_rot();
    thread_gelb();
    thread_gruen();
}

Erklärung

Das Programm hat drei fast identische Threads, die als Zustandsautomat implementiert sind. Bei jedem Aufruf wird geprüft, ob die gewünschte Zeit verstrichen ist. Wenn ja, wird die LED umgeschaltet und zum nächsten Zustand gewechselt. Wichtig ist, dass die Thread-Funktion niemals hängen bleibt. Jeder einzelne Aufruf darf nur wenige Mikrosekunden dauern. Dadurch wird Multitasking ermöglicht, denn nun können wir viele Threads in die Hauptschleife loop() einbauen. Es sieht von außen betrachtet so aus, als ob sie gleichzeitig laufen würden.

Innerhalb der Threads sind delay() aufrufe Tabu, denn dadurch würde die Ausführung aller anderen Threads blockiert werden. Das obige Beispiel zeigt, wie man delay() durch millis() ersetzt. Millis() liefert die aktuelle Systemzeit in Millisekunden (ein fortlaufender Zähler).

Das Schlüsselwort "static" vor den lokalen Variablen sorgt dafür, dass sie ihren Wert zwischen den Funktionsaufrufen nicht vergessen. Sie liegen im gleichen Speicherbereich, wie globale Variablen.

Es ist wichtig, dass die verstrichene Zeit mit einer Subtraktion berechnet wird und dass die dabei verwendeten Variablen den gleichen unsigned (!) Typ wie millis() haben. Nur dann stimmt das Ergebnis auch, wenn der Zeit-Zähler zwischendurch einmal überläuft, was zwangsläufig irgendwann passiert.

if ( millis() - warteSeit >= 100 )  // richtig

if ( millis() >= warteseit + 100 )  // falsch!

Planung eines Zustandsautomaten

Wenn man Zustandsautomaten entwickelt, dokumentiert man die möglichen Zustände und Reaktionen auf Bedingungen oder Ereignisse. Das Zustandsdiagramm einer fiktiven Klimaanlage könnte so aussehen:

komplexes Zustandsdiagramm

Die orangen Kreise stellen die möglichen Zustände dar. Jeder Zustand kann mehrere Bedingungen/Ereignisse definieren, die eine Reaktion und einen Wechsel in den nächsten Zustand auslösen. Das stellen die schwarzen Pfeile mit ihren Beschriftungen dar. Man kann das auch tabellarisch darstellen:

Zustand Bedingung/Ereignis Reaktion Nächster Zustand
AUS Schalter is an Anlage starten EIN
EIN Schalter ist aus Anlage herunter­fahren AUS
Es ist zu warm Kühlung ein schalten KÜHLEN
Es ist zu kalt Heizung ein schalten HEIZEN
KÜHLEN Schalter ist aus Anlage herunter­fahren AUS
Es ist kalt genug Kühlung aus schalten EIN
Kühlung defekt Störung anzeigen und Kühlung aus schalten STOERUNG
HEIZEN Schalter ist aus Anlage herunter­fahren AUS
Es ist warm genug Heizung aus schalten EIN
Heizung defekt Störung anzeigen und Heizung aus schalten STOERUNG
STOERUNG Schalter ist aus Anlage herunter­fahren AUS

Implementierung in C

In Arduino würde man die Steuerung der Klimaanlage etwa so programmieren:

#define SCHALTER      2   // Eingang PD2, Hauptschalter HIGH=Ein, LOW=Aus
#define ANLAGE        3   // Ausgang PD3, Anlage ist bei HIGH in Betrieb
#define KUEHLUNG      4   // Ausgang PD4, Anlage kühlt, wenn HIGH
#define HEIZUNG       5   // Ausgang PD5, Anlage heizt, wenn HIGH
#define STOER_LAMPE   6   // Ausgang PD6, Leuchtet bei HIGH
#define STOER_SENSOR  7   // Eingang PD7, Geht auf HIGH wenn eine Störung erkannt wurde
#define FUEHLER_TEMP  A0  // Eingang PA0, Temperaturfühler, liefert die Raumtemperatur direkt in °C

#define SOLL_TEMP     21  // Soll-Temperatur 21°C

void setup() {
    pinMode(SCHALTER,     INPUT);
    pinMode(ANLAGE,       OUTPUT);
    pinMode(KUEHLUNG,     OUTPUT);
    pinMode(HEIZUNG,      OUTPUT);
    pinMode(STOER_LAMPE,  OUTPUT);
    pinMode(STOER_SENSOR, INPUT);
}

void herunterfahren() {
    digitalWrite(ANLAGE,      LOW);
    digitalWrite(KUEHLUNG,    LOW);
    digitalWrite(HEIZUNG,     LOW);    
    digitalWrite(STOER_LAMPE, LOW);
}


void thread_steuerung() {
    static enum {AUS, EIN, KUEHLEN, HEIZEN, STOERUNG} zustand = AUS;

    switch (zustand) {
        case AUS:
            if (digitalRead(SCHALTER)==HIGH) {                // Bedingung: Schalter is an
                digitalWrite(ANLAGE, HIGH);                   // Reaktion: Anlage starten
                zustand = EIN;                                // Nächster Zustand: EIN
            }          
            break;

        case EIN: 
            if (digitalRead(SCHALTER)==LOW) {                 // Bedingung: Schalter is aus
                herunterfahren();                             // Reaktion: Anlage herunterfahren
                zustand = AUS;                                // Nächster Zustand: AUS
            }
            else if (analogRead(FUEHLER_TEMP) > SOLL_TEMP) {  // Bedingung: es ist zu warm  
                digitalWrite(KUEHLUNG, HIGH);                 // Reaktion: Kühlung ein schalten
                zustand = KUEHLEN;                            // Nächster Zustand: AUS
            }
            else if (analogRead(FUEHLER_TEMP) < SOLL_TEMP) {  // Bedingung: es ist zu kalt  
                digitalWrite(HEIZUNG, HIGH);                  // Reaktion: Heizung ein schalten
                zustand = HEIZEN;                             // Nächster Zustand: AUS
            }
            break;

        case KUEHLEN: 
            if (digitalRead(SCHALTER)==LOW) {                 // Bedingung: Schalter is aus
                herunterfahren();                             // Reaktion: Anlage herunterfahren
                zustand = AUS;                                // Neuer Zustand: AUS
            }
            else if (analogRead(FUEHLER_TEMP) <= SOLL_TEMP) { // Bedingung: es ist kalt genug
                digitalWrite(KUEHLUNG, LOW);                  // Reaktion: Kühlung aus schalten
                zustand = EIN;                                // Nächster Zustand: EIN
            }
            else if (digitalRead(STOER_SENSOR)==HIGH) {       // Bedingung: Kühlung defekt
                digitalWrite(STOER_LAMPE, HIGH);              // Reaktion: Störung anzeigen und Kühlung aus schalten
                digitalWrite(KUEHLUNG, LOW);
                zustand = STOERUNG;                           // Nächster Zustand: STOERUNG
            }
            break;
            
        case HEIZEN: 
            if (digitalRead(SCHALTER)==LOW) {                 // Bedingung: Schalter is aus
                herunterfahren();                             // Reaktion: Anlage herunterfahren
                zustand = AUS;                                // Nächster Zustand: AUS
            }
            else if (analogRead(FUEHLER_TEMP) >= SOLL_TEMP) { // Bedingung: es ist warm genug
                digitalWrite(HEIZUNG, LOW);                   // Reaktion: Heizung aus schalten
                zustand = EIN;                                // Nächster Zustand: EIN
            }
            else if (digitalRead(STOER_SENSOR)==HIGH) {       // Bedingung: Kühlung defekt
                digitalWrite(STOER_LAMPE, HIGH);              // Reaktion: Störung anzeigen und Heizung aus schalten
                digitalWrite(HEIZUNG, LOW);
                zustand = STOERUNG;                           // Nächster Zustand: STOERUNG
            }
            break;

        case STOERUNG:
            if (digitalRead(SCHALTER)==LOW) {                 // Bedingung: Schalter is aus
                herunterfahren();                             // Reaktion: Anlage herunterfahren
                zustand = AUS;                                // Nächster Zustand: AUS
            }
            break;            
    }
}

void loop() {
    thread_steuerung();
    // hier könnte man weitere Threads hinzufügen
}
Startseite