#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 die loop() Funktion. Etwa so:
int main() { setup(); while(1) { loop(); } }
Das obige Programm lässt eine LED blinken. Stelle dir nun vor, du sollst 3 LEDs unabhängig voneinander in folgenden Intervallen blinken lassen:
Präemptiv mit Betriebssystem | Das Betriebssystem unterbricht laufende Threads, um Rechenzeit an andere Threads zu vergeben. Bei jedem Wechsel muss das Betriebssystem den Zustand des laufenden Threads sichern, um ihn später beim Fortsetzen wieder herzustellen. Das betrifft alle CPU Register und den Stack. Die bekannteste Umsetzung für Mikrocontroller ist unter dem Namen "Real Time Operating System" (FreeRTOS) bekannt. |
Kooperativ mit Zustandsautomaten | Zustandsautomaten merken sich ihren Zustand selbst in Variablen außerhalb des Stack, und sie 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 kooperative Variante besser, weil sie weniger Speicher und CPU Leistung benötigt. Ein einzelner Zustandsautomat macht noch kein Multithreading, aber dorthin ist es nur ein kleiner Schritt. Im Folgenden erkläre ich zuerst den Zustandsautomaten und danach, wie man mehrere davon parallel ausführt.
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 herunterfahren | 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 herunterfahren | 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 herunterfahren | AUS |
Es ist warm genug | Heizung aus schalten | EIN | |
Heizung defekt | Störung anzeigen und Heizung aus schalten | STOERUNG | |
STOERUNG | Schalter ist aus | Anlage herunterfahren | AUS |
#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 loop() { 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; // Neuer Zustand: EIN } break; case EIN: 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 zu warm { digitalWrite(KUEHLUNG, HIGH); // Reaktion: Kühlung ein schalten zustand = KUEHLEN; // Neuer Zustand: AUS } else if (analogRead(FUEHLER_TEMP) < SOLL_TEMP) // Bedingung: es ist zu kalt { digitalWrite(HEIZUNG, HIGH); // Reaktion: Heizung ein schalten zustand = HEIZEN; // Neuer 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; // Neuer 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; // Neuer Zustand: STOERUNG } break; case HEIZEN: 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 warm genug { digitalWrite(HEIZUNG, LOW); // Reaktion: Heizung aus schalten zustand = EIN; // Neuer 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; // Neuer Zustand: STOERUNG } break; case STOERUNG: if (digitalRead(SCHALTER)==LOW) // Bedingung: Schalter is aus { herunterfahren(); // Reaktion: Anlage herunterfahren zustand = AUS; } break; } }
Jede der drei LEDs kann zwei Zustände haben: AUS und EIN. Nach einigen Millisekunden soll der Zustand gewechselt werden. Wir brauchen also drei Zustandsautomaten, die quasi parallel ausgeführt werden. Ihre Diagramme sehen so aus:
Wir können es auch tabellarisch darstellen. Für die rote LED:
Zustand | Bedingung/Ereignis | Reaktion | Nächster Zustand |
---|---|---|---|
AUS | 100 ms später | LED ein schalten | EIN |
EIN | 100 ms später | LED aus schalten | AUS |
Zustand | Bedingung/Ereignis | Reaktion | Nächster Zustand |
---|---|---|---|
AUS | 253 ms später | LED ein schalten | EIN |
EIN | 253 ms später | LED aus schalten | AUS |
Zustand | Bedingung/Ereignis | Reaktion | Nächster Zustand |
---|---|---|---|
AUS | 378 ms später | LED ein schalten | EIN |
EIN | 378 ms später | LED aus schalten | AUS |
#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: 100 ms später { digitalWrite(LED_ROT, HIGH); // Reaktion: LED ein schalten warteSeit = millis(); zustand = EIN; // Neuer Zustand: EIN } break; case EIN: if (millis() - warteSeit >= 100) // Bedingung: 100 ms später { digitalWrite(LED_ROT, LOW); // Reaktion: LED aus schalten warteSeit = millis(); zustand = AUS; // Neuer 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(); }
Irgendwann ist es dann so weit, die rote LED wird eingeschaltet. Außerdem merkt sich das Programm in der Variable "warteSeit" den Zeitpunkt dieses Ereignisses. Der Status wechselt nach EIN. Dann wird solange gewartet, bis weitere 100 ms verstrichen sind - der Status wechselt nach AUS.
Der entscheidende Trick ist, dass die Thread-Funktion niemals hängen bleibt, also kein delay() benutzt. Jeder einzelne Aufruf dauert nur wenige Mikrosekunden. In der Hauptschleife loop() kannst du daher 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 den Funktionsaufrufen nicht vergessen.
Bei der Berechnung der verstrichenen Zeit ist es wichtig, dass die Variable "warteSeit" den gleichen unsigned (!) Integer Typ hat, wie der Rückgabewert von millis() und dass man die Differenz mit einer Subtraktion bildet. Denn 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! { ... }
Wer AVR Mikrocontroller ohne Arduino Framework programmieren möchte, kann von meiner Hello World Vorlage abgucken, wie man so einen Millisekunden-Zähler realisiert.
Zustand | Bedingung/Ereignis | Reaktion | Nächster Zustand |
---|---|---|---|
AUS | pause Flag wurde gesetzt | keine | PAUSE |
100 ms später | LED ein schalten | AUS | |
EIN | 100 ms später | LED aus schalten | AUS |
PAUSE | pause Flag wurde gelöscht | LED ein schalten | EIN |
bool pause_flag=false; void thread_rot() { static enum {AUS, EIN, PAUSE} zustand = AUS; static unsigned long int warteSeit = 0; switch (zustand) { case AUS: if (pause_flag==true) // Bedingung: wenn das pause Flag gesetzt wurde { ; // Reaktion: keine, die LED ist schon aus zustand = PAUSE; // Neuer Zustand: PAUSE } else if (millis() - warteSeit >= 100) // Bedingung: 100 ms später { digitalWrite(LED_ROT, HIGH); // Reaktion: LED ein schalten warteSeit = millis(); zustand = EIN; // Neuer Zustand: EIN } break; case EIN: if (millis() - warteSeit >= 100) // Bedingung: 100 ms später { digitalWrite(LED_ROT, LOW); // Reaktion: LED aus schalten warteSeit = millis(); zustand = AUS; // Neuer Zustand: AUS } break; case PAUSE: if (pause_flag==false) // Bedingung: wenn das pause Flag gelöscht wurde { digitalWrite(LED_ROT, HIGH); // Reaktion: LED ein schalten warteSeit = millis(); zustand = EIN; // Neuer Zustand: EIN } break; } }
void thread_rot() { static enum {AUS, EIN} zustand = AUS; static unsigned long int warteSeit = 0; switch (zustand) { case AUS: if (millis() - warteSeit >= 100) { digitalWrite(LED_ROT, HIGH); warteSeit = warteSeit + 100; zustand = EIN; } break; case EIN: if (millis() - warteSeit >= 100) { digitalWrite(LED_ROT, LOW); warteSeit = warteSeit + 100; zustand = AUS; } break; } }
Ein anderer auch durchaus häufig anzutreffender Lösungsansatz besteht darin, dass die Hauptschleife ihre Threads in regelmäßigen Intervallen ausführt (z.B. alle 10 ms) und die Treads ihre eigenen Aufrufe zählen, um Zeitspannen zu ermitteln (10 Aufrufe = 100 ms).
void thread_rot() { static enum {AUS, EIN} zustand = AUS; static unsigned int intervalle = 0; switch (zustand) { case AUS: if (++intervalle >= 10) // Bedingung: 100 ms später { digitalWrite(LED_ROT, HIGH); intervalle = 0; zustand = EIN; } break; case EIN: if (++intervalle >= 10) { digitalWrite(LED_ROT, LOW); intervalle = 0; zustand = AUS; } break; } } void loop() { static unsigned long int warteSeit = 0; if (millis() - warteSeit >= 10) { thread_rot(); thread_gelb(); thread_gruen(); warteSeit = warteSeit + 10; } }