CORTOS: ein Mini-Echtzeitkern für den AVR

    english page is missing - you can help the project with a translation!

Einleitung

    Um mehrere Dinge quasi parallel auf einem Prozessor erledigen zu können, zerteilt man die Programmaufgaben in sog. Tasks, welche dann die Rechenleistung unter sich aufteilen. Hier gibt es prinzipiell zwei verschiedene Techniken:
  • Zeitschlitzzuteilung:
    Der Prozessor wird nach einer bestimmten Zeit von einer Task abgezogen und für die Bearbeitung einer anderen Task zugeteilt.
  • Cooperatives Multitasking:
    Jede Task gibt von sich aus die Kontrolle wieder zurück und es kann eine andere Task Rechenleistung erhalten.
  • Beide Methoden haben Vor- und Nachteile, auf die ich hier nicht näher eingehen will. Das nachfolgend vorstellte CORTOS ist ein recht einfaches, aber für die Bedürfnisse in einer kleinen embedded-Anwendung recht geeignetes kooperatives System.

Überblick über CORTOS

    CORTOS verwaltet kooperative Tasks, diese können folgende Zustände haben:
    inaktivDiese Task will keine Rechenzeit haben
    schlafendDiese Task ruht z.Z., sie will irgendwann wieder Rechenzeit haben.
    bereitDiese Task will Prozessorzeit haben.
    Jede Task gibt nach Ausführung von selbst den Prozessor wieder ab und liefert dabei einen Rückgabewert mit zurück, welcher den zukünftigen Zustand der Task angibt (s.u.).

    Diese Tasks werden bei CORTOS in einer Liste verwaltet, welche folgende Parameter ja Task enthält. Der Listenindex ist die sogenannte TASK_ID:
    CodeadresseEinsprungadresse der Task
    ZustandDas ist ein Flag, welches anzeigt, ob die Task rechnenbereit oder inaktiv ist.
    AufwachzeitHier wird die Zeit hinterlegt, wann diese Task wieder Prozessorzeit haben möchte. Diese zeit wird in Ticks gerechnet und läuft als signed integer.
    Da in einer embedded-Umgebung alle möglichen Aufgaben vorher bekannt sind, wird in CORTOS diese Liste statisch angelegt, die Liste wird also zur Compilezeit erstellt. Dadurch entfällt der Code-Overhead für das Hinzufügen oder Wegnehmen von Listenelementen, in Gegenzug muß man aber beim Taskwechsel immer durch alle Listenelemente durchsuchen (ist aber i.d.R. weniger Aufwand).

    Für den Zustandsübergang einer Task gibt es folgende Aufrufe, wobei zu unterscheiden ist, ob ein Aufruf aus einer Interruptroutine (ISR) oder aus einer anderen Task erfolgt:
  • set_task_ready(TASK_ID): die angebene Task wird bereit.
  • isr_set_task_ready(TASK_ID): die angebene Task wird bereit. Diese Routine ist innerhalb Interrupts zu verwenden (solange global Interrupt enable gesperrt ist).
  • set_task_blocked(TASK_ID): die angebene Task wird inaktiv.
    Hinweis: dieser Aufruf darf nicht aus der Task erfolgen, welche inaktiv werden soll, der normale Rückgabewert der Task würde diese Einstellung wieder überschreiben.
  • Eine Task kann sich selbst mit Hilfe Ihres Rückgabewertes einen zukünftigen Zustand zuweisen:
     O: die Task ist sofort wieder bereit (d.h. sie hat eigentlich nur der Fairness halber zurückgegeben.)
     -1: die Task wird inaktiv
    1 .. 20000: die Task will für die angegebene Zeit schlafen.
  • Anmerkungen zur Schlafenszeit: diese wird nicht ab Rückgabe gerechnet, sondern als Additiv zur letzten Aufwachzeit hinzugezählt, dadurch wird bei einer Task eine bestimmte Rate erzwungen, auch wenn zwischenzeitlich mal eine andere Task vielleicht nicht ganz so kooperativ war.

Details zu CORTOS

Idle-Zustand

    Wenn keine Routine ausführbereit ist, so wird die IDLE_TASK (diese ist immer ready) aufgerufen, welche den Prozessor schlafen legt und so für einen lange Akkulaufzeit sorgt. CORTOS wacht einfach beim nächsten Tick von selbst wieder auf.

    Soll CORTOS nicht einschlafen, sondern ständig prüfen, ob Tasks ready sind, dann läßt man in der IDLE_TASK den sleep() weg. Eine Task, die mit 0 returned, wird dadurch im gleichen Zeitschlitz nochmal aufgerufen.

Tick-Hook

    Routinen, welche bei jedem Lauf des Kerns aktiviert werden sollen, werden sozusagen auf den Tick 'draufgehängt'. Das bietet sich z.B. bei Tastaturabfragen an.

Tick-Rate

    Die Hauptschleife von CORTOS wird sinnvollerweise alle paar ms durchlaufen. Hierzu wird ein Timerinterrupt benötigt, welcher an den CORTOS-Kern ein 'Event' (eben den Tick) sendet. Der Timerinterrupt inkrementiert auch die Systemzeit, gemäß der entschieden wird, ob eine Task von schlafend in bereit wechselt.

Producer-Consumer, Fifos

    Für Ereignisübergabe von Interrupts in Task oder zwischen Tasks werden Fifos verwendet. Die Task, welche auf ein Ereignis wartet, heißt Consumer. Sollte eine Routine (Consumer) auf ein externes Ereignis warten, so muß sie beim entsprechenden Fifo warten.
    Das erfolgt so:
  • Die Task prüft, ob was im Fifo ist: Aufruf von cr_fifo_filled(fifo_id)
  • Wenn da was vorhanden ist, wird es mit cr_fifo_get_nowait(fifo_id) abgeholt und verarbeitet.
  • Wenn da nichts vorhanden ist, dann gibt die Task an CORTOS mit -1 zurück (=not ready)
  • Wenn ein neues Ereignis in das Fifo reinkommt (cr_fifo_put(fifo_id)), dann setzt die Fifosteuerung die Task wieder ready. Damit das Fifo weiß welche Task gemeint ist, muß die Consumertask vorher das mal eingestellt haben: cr_fifo_set_comsumer(fifo_id, TASK_ID)

Verwendung von Cortos

    Cortos ist keine Echtzeitlibrary, sondern wird im Sourcecode dazu gebunden. Hierzu habe ich es in folgende Module unterteilt:
  • cortos.c: Das ist der eigentliche Kern, hier ist Hauptsprungverteiler (Scheduler), dieser Teil ist unabhängig von der Anwendungssoftware.
  • cortos.h: Das ist der header, hier sind die Typen und Objekte (also Task ready machen usw.) definiert, auch dieser Teil ist unabhängig von der Anwendungssoftware.
  • cortos_tasklist.h: Das ist der header, welcher die Anwendungstask (und davon nur den ENUM) enthält. Es handelt sich dabei um einen enum, wobei der erste und letzte Eintrag nicht verändert werden darf:
       typedef enum {  TASK_IDLE,           // must be always there, always the first one, low prio
                       TASK_POWER_UP,       // Usertask: long term init at power up
                       TASK_KEYBOARD,       // Usertask: local keyboard (here only PROG)
                       ...
                       TASK_DCC_DECODE,     // Usertask: (highest prio)
                       size_of_tasklist,    // must be always there
                  } t_task_id;
  • cortos_tasklist.c: Hier wird die Tasklist instanziiert.
       t_task tasklist[] =
         // id                     ready, wakeup, call
         {  [TASK_IDLE]          = {    1, 0,      task_idle},
            [TASK_POWER_UP]      = {    1, 0,      power_up_sequence},
            [TASK_KEYBOARD]      = {    1, 0,      keyboard},
            ...
            [TASK_DCC_DECODE]    = {    0, 0,      dcc_decode},  // highest prio
         };
    Folgendes gilt es zu beachten:
    • Jede Task (vom Typ t_cr_task) wird fest in die Liste eingetragen, es gibt kein an- und abmelden. Wenn man eine Task mit ready=1 einträgt, dann wird sie aufgerufen (sofern wakeup = 0 ist), bei ready = 0 ist die Task inaktiv.
    • Die TASK_IDLE muß immer vorhanden sein und immer ready sein.
    • In der Liste darf kein Eintrag sein, der nicht auch schon im enum t_task_id aufgeführt ist.
  • main.c: Hier muß nach allem Initialisierungen am Ende der main-routine der scheduler in einer Endlosschleife gerufen werden:
    // now start up CORTOS
    
        volatile signed char main_systick;
        init_timer();
        main_systick = get_systick_int8();
        sei();
        while(1)
          {
            if ((signed char)(get_systick_int8() - main_systick) > 0)
              {
                main_systick = get_systick_int8();
                // keyboard_poll();             // here we go with hook functions
              }
            scheduler();                        // run task engine (from cortos.c)
          }
    Hook-Funktionen (also Dinge, die ständig per Poll mitlaufen sollen) kann man direkt in dieser Schleife einklinken, alternativ kann man auch eine Task dafür bauen.

Beispiele

  • Blinker:
    Um eine LED zu blinken, legt man eine Task an. Diese schaltet abhängig von led_state den Port um und gibt dann die halbe Blinkperiode zurück. Dadurch erhält diese Task nach dieser Zeit wieder die CPU zugeteilt.
       #define LED_BLINK_PERIOD  800000L     // 800ms
    
       #define LEDPOS 3
       static uint8_t led_state;
       t_cr_task led_blinker(void)
        {
          if (led_state) {led_state = 0; PORTA &= ~(1<<LEDPOS);}
          else           {led_state = 1; PORTA |= (1<<LEDPOS);}
          return(LED_BLINK_PERIOD / 2 / SYSTICK_PERIOD);
        }
  • Schaltverzögerung:
    Dieses Beispiel zeigt, wie man eine Schaltzeit in der cortos-Umgebung einbaut. Im Beispiel wird eine Kehrschleife angesteuert, diese ist mit einem bistabilen Relais implementiert, welches mal in die eine, mal in die andere Lage geschaltet werden soll. Der Spulenstrom soll je für 20ms aktiviert sein.
    • Schritt 1: Man baut eine Routine für jede Lage und fürs Abschalten, hier also:
         #define RESERVER_ON()   {PORTH.OUTCLR = (1 << REVERS1);  \
                                  PORTH.OUTSET = (1 << REVERS2);  \
                                 }
         #define RESERVER_OFF()  {PORTH.OUTCLR = (1 << REVERS2);  \
                                  PORTH.OUTSET = (1 << REVERS1);  \
                                 }
         #define RESERVER_IDLE() {PORTH.OUTCLR = (1 << REVERS1) | (1 << REVERS2);  \
                                 }
         
    • Schritt 2: Man baut eine Task, welche die Spulen abschaltet und sich dann schlafen legt:
          t_cr_task kehrschleife_relais_off(void)
            {
              RESERVER_IDLE();
              return(-1);   // sleep again
            }
         
    • Schritt 3: man nimmt diese Task in die Liste der Tasks auf:
           t_task tasklist[] =
              // id                     ready, wakeup, call
             {  ...
                [TASK_REVERSER_IDLE] = {    0, 0,      kehrschleife_relais_off},
                ....
             }  
    • Schritt 4: beim Schalten ruft man nun nach der Aktivierung der Spule die Task auf und gibt dabei eine Verzögerungszeit mit:
          RESERVER_ON();
          set_task_ready_delayed(TASK_REVERSER_IDLE, 20000L / SYSTICK_PERIOD);   // nach 20ms Spule wieder abschalten
         
      Jetzt wird also das Makro RESERVER_ON ausgeführt (das schaltet die Spule ein), und dann die Task 'scharf' geschaltet. Cortos sorgt nun dafür, dass nach 20ms diese Task aufgerufen wird. Die schaltet dann sich Spule ab und teilt mittels des Return-Codes -1 cortos mit, dass man keine weiteren Aufrufe will.

Anwendung