Multitasking mit AVRs

Komplexe Aufgaben meistern?

Als ich anfing mir Gedanken über die Software für den zweiten Staubsaugerroboter zu machen, habe ich gemerkt, dass diese sehr komplex werden wird. Denn der Roboter hat viele verschiedene Aufgaben zu erledigen, die nacheinander oder teilweise auch parallel abgearbeitet werden sollen. Nun gibt es verschiedene Varianten der Verwaltung solcher Aufgaben, angefangen bei state machines (Zustandsautomaten) bis hin zu Multitasking mit Real-Time-Operating-Systemen (RTOS).

State machine

State machines sind relativ einfach gestrickt und daher verhältnismäßig leicht zu verstehen. Mit ihnen können zwar keine Aufgaben (engl. Tasks) parallel bearbeitet werden (Multitasking), aber sie ermöglichen bereits eine Organisation der zu erledigenden Aufgaben. Für das Funktionsprinzip einer state machine möchte ich auf den Artikel bei Mikrocontroller.net verweisen. Ein Nachteil von state machines ist, dass angefangene Aufgaben (Code-Abschnitte oder tasks) nur an einem Stück bearbeitet und nicht unterbrochen werden können.

Erweiterte state machine

Bei eigenen Experimenten habe ich eine state machine um eine „Warteschlange“ ergänzt, sodass ein Zustand der state machine mehr als nur einen anderen Zustand nach sich ziehen kann. Die Warteschlange besteht dabei aus einem eindimensionalen Array und wird nach dem FIFO-Prinzip (engl. first in first out) mit den abzuarbeitenden Zuständen gefüllt, d.h. der zuerst eingereihte Zustand wird auch zuerst bearbeitet. Die einzelnen Zustände werden dabei durch 8-Bit Zahlen repräsentiert, die nachher in der Switch-Anweisung in main() zu den jeweiligen Code-Abschnitten führen, sodass insgesamt 255 verschiedene Tasks (Zustände) definiert werden können (Zustand 0 bedeutet Leerlauf). Das folgende Beispiel in C zeigt das anhand von drei ewig wiederholenden LCD-Anzeigen:

#include <avr/io.h>
#include <util/delay.h>
#include "lcd.h"
//Länge der Warteschlange, 64 Zustände können sich einreihen, beliebig verlängerbar
#define TODOSIZE 64
uint8_t ToDo[TODOSIZE] = {0}; //Deklaration der Warteschlange als Array, Initialisierung mit 0
uint8_t ToDoCount = 0; //Aktuelle Länge der Warteschlange
//-------------------------------------------------------------------------
// addZustand() fügt den übergebenen Zustand an das Ende der Warteschlange an
//-------------------------------------------------------------------------
void addZustand(uint8_t Zustand) //Zustand in Liste einfügen
{
  //Wenn die Warteschlange nicht bereits voll ist
  if(ToDoCount < TODOSIZE)
  {
    //Übergebenen Zustand anfügen
    ToDo[ToDoCount] = Zustand;
    //Warteschlangenlänge erhöhen
    ToDoCount++;
  }
}
//-------------------------------------------------------------------------
// delZustand() löscht den Zustand in der Warteschlange am der übergebenen
// Stelle und lässt die nachfolgenden Zustände aufrücken
//-------------------------------------------------------------------------
void delZustand (uint8_t Stelle)
{
  //Nachfolgende Zustände um eins vorrücken
  //Der zu Löschende wird dabei überschrieben
  for(uint8_t i = Stelle; i <= TODOSIZE; i++)
  {
    //Zustand an Stelle i+1 auf Stelle i vorrücken
    ToDo[i] = ToDo[i+1];
  }
  //Letzte freigewordene Stelle mit 0 (kein Zustand = Leerlauf) initialisieren
  ToDo[TODO_SIZE-1] = 0;
  //Warteschlangenlänge verringern
  ToDoCount--;
}
//-------------------------------------------------------------------------
// main() beinhaltet die Switch-Anweisung und darin die Code-Abschnitte (Tasks)
//-------------------------------------------------------------------------
int main (void)
{
  //Initialisierungen
  lcd_init(LCD_DISP_ON);
  //Endlosschleife: Pro Schleifendurchlauf wird ein Zustand abgearbeitet
  while(1)
  {
    //Auszuführenden Code anhand des ersten Zustandes in der Warteschlange ermitteln
    switch(ToDo[0])
    {
      //"Hallo Welt" auf LCD anzeigen
    case 1:
      lcd_clrscr();
      lcd_puts("Hallo Welt");
      //Drei Zustände in die Warteschlange einreihen
      addZustand(2);
      addZustand(3);
      addZustand(1);
      //LCD-Anzeige 1 Sekunde stehen lassen...
      _delay_ms(1000);
      break;
//-------------------------------------------------------------------------
      //"Wie geht es Dir?" auf LCD anzeigen
    case 2:
      lcd_clrscr();
      lcd_puts("Wie geht es Dir?");
      //LCD-Anzeige 1 Sekunde stehen lassen...
      _delay_ms(1000);
      break;
//-------------------------------------------------------------------------
      //"Mir geht es gut!" auf LCD anzeigen
    case 3:
      lcd_clrscr();
      lcd_puts("Mir geht es gut!");
      //LCD-Anzeige 1 Sekunde stehen lassen...
      _delay_ms(1000);
      break;
//-------------------------------------------------------------------------
      //Nichts zu tun = Leerlauf
    case 0:
      break;
    }
    //Ersten (abgearbeiteten) Zustand löschen
    delZustand(0);
  }
  return 0;
}

Dadurch, dass sich Zustand 1 nach dem Hinzufügen von Zustand 2 und 3 auch selbst „hinten anstellt“, entsteht eine Endlosschleife der drei Anzeigen. Zustände könnten z.B. auch in einer Interruptroutine (Tastendruck bzw. andere Eingabe) zur Warteschlange hinzugefügt werden.

Zustand Aktion (Task) Veranlasste Zustände Nächster Zustand Warteschlange
1 “Hallo Welt” ausgeben 2, 3 und 1 2 2, 3, 1
2 “Wie geht es Dir?” ausgeben 3 3, 1
3 “Mir geht es gut!” ausgeben 1 1

Multitasking

AVRs sind erstmal grundsätzlich nicht fähig „echtes“ Multitasking, d.h. wirklich paralleles Bearbeiten von Tasks, zu betreiben. Ein Prozessor kann eben nur einen Befehl zur Zeit ausführen. Allerdings kann man mit einem Trick trotzdem mehrere Aufgaben nebeneinander abarbeiten und zwar indem der Mikrocontroller in kleinen Zeitabständen zwischen den Aufgaben hin und her wechselt. So bekommen alle Aufgaben ihre Rechenzeit. Die Zeitabstände sind dabei sehr klein.

Es gibt außerdem noch zwei Methoden zu unterscheiden, wie entschieden wird, wann ein sogenannter context switch (wechsel des Prozesses/der Task) eingeleitet wird. Beim Kooperativen Multitasking entscheidet dabei jede Task selbst, wann zu einer anderen Task gewechselt wird; die Tasks kooperieren. Beim Präemptiven Multitasking wird in strikten Zeitabständen umgeschaltet, der Wechsel wird den Tasks „von außen aufgezwungen“. Einen sehr lesenswerten englischen Text zu einer Umsetzung von Multitasking in C für AVRs gibt es hier bei avrfreaks.net. Nicht ganz so ausführlich aber auf Deutsch ist dieser Text bei controllersandpcs.de über Multitasking bei AVRs.

FemtoOS

Vor einer ganzen Weile habe ich in der Elektor einen Artikel über Multitasking mit AVRs gelesen. Dort wurde das mit FemtoOS gemacht. FemtoOS ist ein Betriebssystem für kleine AVRs, das mit wenig Speicherplatz auskommt. Zurzeit werden 44 AVRs unterstützt. Man kann mehrere Tasks (maximal 16) definieren, die dann zwar nicht parallel, aber doch gleichberechtigt (oder nach eingestellten Prioritäten) fast in Echtzeit abgearbeitet werden. Positiv fand ich, dass ich ein mitgeliefertes Beispiel auf Anhieb mit einem ATmega48 zum Laufen bringen konnte. Dabei blinken acht LEDs unterschiedlich schnell, unabhängig voneinander von acht Tasks gesteuert.

Allerdings gibt es viele Einstellungen bei FemtoOS und ich blicke noch nicht vollständig durch, auch weil vieles durch Präprozessorcode sehr verwirrend für mich ist. Deshalb habe ich bisher z.B. die LCD-Routinen nicht zum Laufen bringen können, sondern nur leicht abgeänderte Beispielprogramme. Soweit ich das bisher beurteilen kann, liegt das daran, dass die Kommunikation mit dem Controller des LCD zeitkritisch ist und deshalb durch das Zwischenschalten anderer Tasks nicht mehr funktioniert. Wie man zeitkritischen Code bei FemtoOS behandelt, d.h. das Zwischenschalten anderer Tasks verhindert, muss ich aber noch herausfinden.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.

Bitte beantworte kurz folgende Frage, um zu zeigen, dass du kein Roboter bist: *