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:
- rot im 100ms Intervall
- gelb im 253ms Intervall
- grün im 378ms Intervall
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:
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 |
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
}
