Dipl. Phys. Helmut Weber





Achtung:

Alle Beiträge sind Copyright (C) 2018 Dipl. Phys. Helmut Weber

Jedes Kopieren - egal, in welcher Form - bedarf der schriftlichen Erlaubnis.

Beispiel-Programme nur für privaten Bedarf.

Links innerhalb dieser Seite:


Vergleich RTOS CoopOS / (Tabelle)

Beispiel-Sourcen



Der CoopOS Scheduler (Linux preempt_RT)



Es gibt viele - und erfolgreich eingesetzte - Ansätze und Libraries für kooperatives Multitasking. Viele davon sind allerdings abhängig vom Betriebsystem.
CoopOS basiert auf der Grundlage von Ansi-C und ist daher extrem leicht portierbar. Es funktioniert auf kleinsten 8-Bit Prozessoren wie auch unter Supercomputerrn.
Die Taskswitchzeiten sind - passende Tasks vorausgesetzt - extrem klein. Deshalb wird es gerade dann eingesetzt, wo es um schnellste Programm-Reaktionen  auf
irgendwelche - äußeren oder inneren - Ereignisse geht. CoopOS nutzt dabei Eigenschaften des C-Compilers (auch sehr alter Compiler) ein, um jeden Task u einer Art
State-machine zu machen, die ihren Zustand bei Taskswitch behält und diesen selbst wieder herstellen kann.

CoopOS kann auch neben (bzw. unter)  Betriebssystemen wie RTOS oder Linux eingesetzt werden.
In CoopOS finden sich viele Elemente wieder, die man auch unter einem RTOS findet:
  InitTask();
  taskYield();
  taskDelay();
usw.
 
Heutige Prozessoren verfügen oft über mehrere Kerne. Zielsetzung ist es:
1) Bei Proessoren mit einem Kern CoopOS als einziges "Betriebssystem" laufen zu lassen
    oder
2) Bei Prozessoren mit mehreren Kernen soll CoopOS auf einem vom Rest des Betriebssystems möglichst isoliertem Kern laufen.

Linux preempt_RT kommt diesem Wunsch mit ISOLCPUS im config weitestgehend nach.

Der Scheduler  ist die Zentrale, von der alle Tasks aufgerufen werden und wohin die Kontrolle zurückgegeben wird, wenn die Tasks die Kontrolle freiwillig (kooperativ) zurückgeben.


Ein Task wird vom Scheduler aufgerufen. Der Task macht dort weiter, wo er die Kontrolle an den Scheduler abgegeben hat:


Task1

Der Scheduler entscheidet also, wann der Task die Kontrolle zurückbekommt !


Der Scheduler geht in einem einfachen FOR-NEXT Loop alle vorhandenen Tasks durch und setzt alle Tasks auf READY, deren  Delay abgelaufen ist.

Dann sucht er den Task, der

       A) READY ist

              UND

       B) Die höchste Priorität hat

 Ein Task, der lediglich ein   taskSwitch() enthält ist also immer READY. Er wird ausgeführt, wenn kein anderer Task READY ist.

Ein solcher Task sollte die niedrigste Priorität haben !  Er verdrängt den Task "Idle", der sonst angesprungen würde.

Alle anderen Tasks sollten irgendwie pausieren. Sei es durch  taskDelay() oder taskWaitSignal() oder ähnlich.

Ein Task, der mit   taskWaitSignal(sig)   pausiert, kann auch vom Interrupt aus mit einem taskSetSignal(sig) gestartet werden.

Ein Task, der auf ein Signal wartet, bekommt - wenn das Signal eintrifft - kurzzeitig die höchste Priorität 99 , egal, wie seine sonstige Priorität ist.

Signale (und weitere Ereignisse) werden so besonders schnell bearbeitet.


Es ist zwar nicht verboten, dass mehrere Tasks gleiche Priorität haben, aber es ist übersichtlicher, wenn alle Tasks unterschiedliche Prioritäten haben.



Dieses Vorgehen ist nur sinnvoll mit einer Zeitsteuerung!

Enthält Task1 statt taskSwitch() ein taskDelay(Zeit), dann wird nach einem Aufruf von Task1 zwar wieder mit der Prüfung von Task1 begonnen. Da aber der für eine Zeit als DELAYED gemeldet ist, werden weitere Tasks geprüft.

Die Folge:

Tasks1 hat die höchste Priorität. Jedesmal, wenn der Scheduler die Kontrolle erhält, prüft er IMMER ob Tasks1 wieder gestartet werden kann.


Scheduler1


Beispiel:


Tasks1 enthält in seiner   while(1)-Schleife ein

   TaskDelay(100000);   // Delay wird in NANO-Sekunden angegeben

Dies ist die Aufforderung an den Scheduler, Task1 nach 100 Microsekunden erneut zu starten.


Task3 berechnet PI nach einem sehr langsamen Approximationsverfahren.

(Task3, weil gleich noch ein Task2 dazukommt)

Es enthält den Wunsch:

   TaskDelay(1000);   // Delay wird in NANO-Sekunden angegeben

Task2 möchte also jede Microsekunde neu gestartet werden  ;)

Das kann nicht funktionieren, aber als Folge wird Task2 immer READY sein.

Aber mit dem  taskDelay  wird die Kontrolle an den Scheduler zurückgegeben. Und der prüft zuerst, ob Task1 wieder gestartet werden kann, da die 100µs vergangen sind,


Das Ergebnis:

Task2 wird nahezu die gesamte Rechenzeit benutzen, aber Task1 wird regelmässig gestartet.

Nehmen wir an, dass Task2 jedesmal 30µs rechnet. Wenn dann Task1 gestartet wrden sollte, dann rechnet Task2 erst zu Ende.

Für Task 1 bedeutet das, dass die Startzeit verspätet wird. Im Extremfall wird Task1 erst nach 130µs starten.

Aber Tasks1 wird schnellstmöglich gestartet.




Signale


Häufig wird gewünscht, das Tasks gezielt nach einem (inneren oder äußeren) Ereignis schnellstmöglich aufgeweckt werden sollen.

Dies wird dem Scheduler durch sogenannte Signale mitgeteilt,

Das Senden von Signale soll

Schnell sein (um auch von Interrupt-Routinen genutzt werden zu können)

Unabhängig vom Betriebssystem sein

Die durch Signale aufzuweckenden Tasks befinden sich nach

    taskWaitSignal(Signalnumber)

im state=WAITSIGNAL

Da sie nicht READY sind, werden sie vom Scheduler ignoriert. Wenn jedoch irgendwo - am Scheduler vorbei - das Macro

    taskSendSignal(Signalnumber)

aufgerufen wird, dann werden alle Tasks, die auf dieses Signal warten, in den state=HIGHPRIORITY vesetzt, Gleichzeitig wird für den Scheduler ein Flag HIPRIO gesetzt.

Wenn dieses Flag gesetzt ist, wird der Scheduler beim nächsten Aufruf zunächst ALLE Tasks dieser Art aufrufen - und zwar genau in der Reihenfolge, in der sie initialisiert wurden.

Vorteile:

Nach einem Signal kann berechnet werden, wann der erste Task, der darauf wartet, gestartet wird.

In unserem Beispiel wird ein Task2 eingeführt, der jedesmal von Task1 per Signal gdestartet wird - das Signal kommt also alle 100µs. Gemessen wird die Zeit, die vergeht, bis Tasks2 startet.

Also 3 Tasks

    Task1 soll alle 100µs starten und ein Signal senden

    Task2 wartet auf das Signal

    Task3 berechnet PI im Hintergrund

Hier zunächst die Verteilung: wie genau wird Tasks wirklich im 100µs Takt gestartet?

Plot18

                                                           Man beachte den logariithmischen Maßstab!

Aber für HARDREALTIME rechnen wir mit einer maximalen Verzögerung von 100 µs (38µs waren mit einer Wahrscheinlichkeit von 1:100000000 gemessen worden).

    Die Verzögerung schließt alles ein: Berechnung von PI (max. 40 µs)

    Linux


43000000 >>>>>>>>>> MaxTime: 40 µs   >>>>>>>>>>>>>>>>>PI IST 3.141592617057276


Dazu kommt der Delay von Senden des Signals bis zum Start von Task2:

18Start

                                                      Man beachte den logarithmischen Maßstab!


Auch hier rechnen wir großzügig mit einer Verzögerung von 50 µs (gemessen; 28µs)

Das bedeutet:

Während in den allermeisten Fällen (SOFTREALTIME) die Reaktion spätestens nach ca. 100 µs erfolgt, ist hier die Betrachtung nach der bei HARDREALTIME geforderten Garantie.

Nach HARDREALTIME Forderung kann man sagen:

Nach spätestens 250 µs (200 + 50)  erfolgt garantiert eine Reaktion eines Tasks auf ein äußeres Signal !



Zusammenfassung:

Der CoopOS Scheduler auf dem Raspberry Pi 3 benötigt i.d.R. weniger als 1µs, um einen Task zu starten.

Da die Tasks sehr kompakt sind, können problemlos sehr viele Tasks installiert werden. Deshalb ist es möglich, die Aufgaben modular zu gestalten und zu testen.

Für den Zugriff auf globale Variablen sind keine Schutzmechanismen (Semaphore) notewendig, da imm nur genau ein Task darauf zugreift.

Viele Libraries sind nicht "reentrant" und machen den Einsatz unter einem RTOS problematisch.

CoopOS erlaubt derzeit wohl die schnellste und gleichzeitig deterministischste Reaktion auf äußere Ereignisse.

Codesys. OpenPCL, Node Red sind zwar gute und gebräuchliche Hilfsmittel zu Programmierung von Microcontrollern und embedded systems, aber die Ergebnisse sind weder schnell noch (unter Linux) deterministisch.

Linux preempt-RT in Kombination mit CoopOS leistet beides !


Automotive


Beispiel: in einem Automobil sollen durch einen Abstandssensor die Airbags ausgelöst werden.

Die schnellste Reaktion ist i.d.R. durch reine Hardware gegeben. Der Abstandssensor gibt ein Signal an einen TTL-Baustein bei Unterschreiten eines Mindestabstands. Wenige Nanosekunden später werden auf mehreren Leitungen die Airbags ausgelöst. Es kommt also bei jedem Parkvorgang zum Auslösen der Airbags ;)

Sensoren und Aktoren sind heute per CAN-Bus vernetzt, über den Werte und Signale verteilt werden.

Zur Entscheidung, ob ein Airbag ausgelöst werden soll, gehört neben dem Abstand, Wert der aktuellen Verzögerung und der Geschwindigkeit!

Vielfach wird für die Programmierung der einzelnen Knoten im CAN-Bussystem SPS (Speicherprogrammierbare Steuerung) eingesetzt.

Vorteile:

    Definierte (grafische) Programmierung mit standartisierten Elementen.

    Dadurch schnelle Produktentwicklung

    Deterministische Zykluszeiten

Der Nachteil: sehr langsam gegenüber speziell programmierten Microcontrollern!


Wir wollen ein Assistenzsystem entwickeln, das hilft, Unfälle zu vermeiden oder bei unvermeidbaren Unfällen passend zu reagieren.

In unserem Beispiel von oben würde Task3 (statt PI zu berechnen) ständig die Werte der Sensoren per CAN-Bus empfangen und auswerten. Sensoren könnten sein:

(Quelle: https://www.uni-koblenz-landau.de/de/koblenz/fb4/ist/AGZoebel/Lehre/ss2011-ordner/sem_asida/landua)

Im Allgemeinen werden die folgenden Sensor-Typen in Pre-Crash-Systemen eingesetzt:

    – Radar (Nahbereich, Fernbereich)

    – Laser-Scanner – Ultraschall-Sensoren

    – Digital-Kameras

    – GPS-Sensoren

    – Augenlid-Erkennung

    – ESP-Sensoren (Rad-Geschwindigkeit, Lenkrad-Winkel, Querbeschleunigung, GierGeschwindigkeit)

    – Sensoren von Brems-Assistenten (um Notbremsung zu entdecken)

Task3 würde weiterhin Gefahrensituationen erkennen und ggf. über zu treffende Maßnahmen entscheiden:

1.  Signale, die eine Reaktion des Fahrers auslösen sollen

2.  Bei ausbleibender Reaktion: Maßnahmen, die den Fahrer unterstutzen, dass eine ¨ Kollision vermieden werden kann

3.  Bei einer unvermeidlichen Kollision: Autonome Reaktion, um die Unfallfolgen zu mindern

Task3 übermittelt mit einem Signal die zu verfolgende Startegie an Task1

Task1 sendet Signal an die einzelnen Tasks, die die gewünschten Reaktionen auslösen - in vorher festgelegter Reihenfolge.

Task3 sendet Signale an die Tasks, die entsprechend der Strategie zu verfolgen sind, z.Bsp.

(1) Warnlicht, Sitzvibrator

(2) Sanftes Bremsen, Warnlicht, Schließen der Scheiben

(3) Gurtstraffer, Airbags, Vollbremsung

Alle Reaktionen werden natürlich über den CAN-Bus initialisiert


Für Airbags wird sonst häufig mit einer maximal erlaubten Latenz von 3ms gerechnet.

Ich bin sicher, dass all das mit einem Raspberry PI unter Linux preempt_RT und CoopOS in  weniger als 100 µs erfolgen kann.


Das beschriebene System ist also für solche Aufgaben geeignet!














Technischer Hintergrund

Das Folgende gilt für den Scheduler in Linux preempt_RT und auch für "bare metal"



Wer als Pilot an Formel 1 Rennen teilnimmt geht ein höheres Risiko ein, als jemand, der sich sehr vorsichtig durch den Straßenverkehr bewegt.

Aber heute leben Rennfahrer längst nicht mehr so gefährlich, wie vor 50 Jahren. Der technische Fortschritt hat hier für viel mehr Sicherheit gesorgt.


Ein wesentlicher Aspekt bei einem RTOS ist Sicherheit. Aber das verhindert eben auch die hohe Geschwindigkeit.

Bei CoopOS haben alle Tasks grundsätzlich alle Freiheiten, um auf globale Variablen zuzugreifen. Ein Kompromiss wäre, die Einzelteile von CoopOS

in C++ Klassen zu kapseln, um dass weitgehend zu verhindern.

Eine weitere Möglichkeit  ist es, auch bei CoopOS die komplette Taskumgebung wie bei einem RTOS zu sichern (Eigener Stack usw.)

Zusätzlich kann im Interrupt (hier: FIQ), der unabhängig von CoopOS stattfindet und nicht gesperrt werden kann, eine Prüfung stattfinden, ob der aktuelle

Task - aus welchen Gründen auch immer - sich nicht mehr kooperative verhält. Dieser Task kann gestopt werden und der Scheduler neu aufgerufen

werden.

Alle diese Möglichkeiten wurden ausprobiert. Auch in CoopOS läßt sich Sicherheit einbauen!


Es bleibt aber der Aspekt: Alle Tasks MÜSSEN sich kooperativ verhalten und dürfen nicht abstürzen, wenn das ganz System funktionieren soll.

Es ein Aberglaube, dass mit einem RTOS 100% Sicherheit gewährleistet ist. Wenn ein Task abstürzt, arbeitet zwar der Scheduler und die anderen Tasks

weiter - aber das System erfüllt nicht mehr seine Aufgabe!   Das "Pathfinder-Problem" ist berühmt geworden!

Ein embedded system unter CoopOS muss vielleicht noch mehr getestet werden, als ein System unter RTOS.

Aber der Scheduler - das Herz eines jeden RT Operating Systems - ist bei CoopOS weiter kleiner und weniger komplex, als ein RTOS.

Es lassen sich leicht eigene Werkzeuge (Tracing, Histogramme usw. ) einbauen und nutzen.So lassen sich Problemfälle recht schnell herausfinden.

RTOS sind vergleichsweise wesentlich komplexer. Es läßt ist wie mit alten und neuen PKWs: Früher ab es weniger Komfort, aber man konnte alles selbst prüfen

und ggf. reparieren. Das ist heute weitgehend ausgeschlossen.

Kooperatives Multitasking verbrachte lange ein Nischendasein. Inzwischen ist allerdings ein neues lebhaftes Interesse festzustellen. Egal, ob bei C/C++Compilern

oder neueren  Programmiersprachen - es werden immer mehr sogenannte Coroutines eingebracht. Und das ist kooperatives Multitasking.

Und dafür gibt es 2 Gründe:  a) Geschwindigkeit  b) einfache Handhabung


Hier sollen in ein paar Zeilen Code die Prinzipien gezeigt werden:

Die einfachste Version:

Zwei Tasks laufen abwechselnd und arbeiten einen Programmteil ab. Den Zustand merkt sich jeder Task in einer statischen Variablen

#include <stdlib.h>
#include <stdio.h>

//-----------------------------
// These tasks are a state machine
// and save their state until
// they are called next time

void Task1() {
static int state;
  switch(state) {
    case (0): goto L0;
    case (1): goto L1;
    case (2): goto L2;
    case (3): goto L3;
  }
L0:
  while(1) {
    state=1;
L1:
    printf("Task1 - State 1\n");
    state=2;
    return;
L2:
    state=3;
    printf("Task1 - State 2\n");
    return;
L3:
    state=1;  
    printf("Task1 - State 3\n");
    return;
  }
}


//-----------------------------
void Task2() {
static int state;
  switch(state) {
    case (0): goto L0;
    case (1): goto L1;
    case (2): goto L2;
    case (3): goto L3;   
  }
L0:
  while(1) {
    state=1;
L1:
    printf("Task2 - State 1\n");
    state=2;
    return;
L2:
    state=3;
    printf("Task2 - State 2\n");
    return;
L3:
    state=1; 
    printf("Task2 - State 3\n");
    return;
  }
}

//-----------------------------

 
//-----------------------------
// so called SuperLoop:
int main() {
  while(1) {
    Task1();
    Task2();
  }
}


Im folgenden Beispiel passiert genau das Gleiche. Aber mit den Makros sieht man an Task 3, wie es sehr viel übersichlicher werden kann (Task 3).

Es wird dabei die Eigenschaft ALLER C-Compiler benutzt, die Zeilennummer __LINE__ zu kennen und ins Programm mit einbauen zu können.

#include <stdlib.h>
#include <stdio.h>

//-----------------------------
void Task1() {
static int state;
  switch(state) {
    case (0): goto L0;
    case (1): goto L1;
    case (2): goto L2;
    case (3): goto L3;
  }
L0:
  while(1) {
    state=1;
L1:
    printf("Task1 - State 1\n");
    state=2;
    return;
L2:
    state=3;
    printf("Task1 - State 2\n");
    return;
L3:
    state=1;  
    printf("Task1 - State 3\n");
    return;
  }
}


//-----------------------------
void Task2() {
static int state;
  switch(state) {
    case (0): goto L0;
    case (1): goto L1;
    case (2): goto L2;
    case (3): goto L3;   
  }
L0:
  while(1) {
    state=1;
L1:
    printf("Task2 - State 1\n");
    state=2;
    return;
L2:
    state=3;
    printf("Task2 - State 2\n");
    return;
L3:
    state=1; 
    printf("Task2 - State 3\n");
    return;
  }
}

//-----------------------------
//          MACROS
//-----------------------------

#define taskBegin()                                                            \
static int _mark = 0;                                                          \
  switch (_mark) {                                                             \
  case 0:
 
#define taskEnd()                                                              \
  _mark = 0;                                                                   \
}
 
#define taskSwitch()  _mark = __LINE__;   return ;  case __LINE__:;                                                           
 


//-----------------------------
// This Task does the same as
// Task1, Task2.
// But it looks much better
//-----------------------------

void Task3() {
 
  taskBegin();
 
  while(1) {
    printf("Task3 - State 1\n");
    taskSwitch();
    printf("Task3 - State 2\n");
    taskSwitch();
    printf("Task3 - State 3\n");
    taskSwitch();
  }
     
  taskEnd();
}


//-----------------------------
// This task is lik3 Task3, but
// the MACROS are explicitly
// written
//-----------------------------

void Task4() {
//taskBegin();
static int _mark = 0;    
                                                    
  switch (_mark) {                                                            
    case 0:
 
        while(1) {
          printf("Task4 - State 1\n");
          //while(1);
          //taskSwitch();
          _mark = __LINE__; return ;  case __LINE__:;                                     
         
          printf("Task4 - State 2\n");
          //taskSwitch();
          _mark = __LINE__; return ;  case __LINE__:;                                                                            
         
          printf("Task4 - State 3\n");
          //taskSwitch();
          _mark = __LINE__; return ;  case __LINE__:;                                                        
       
        }
     
  //taskEnd();
  _mark = 0;                                                                  
  }
}

 
//-----------------------------

int main() {
  while(1) {
    Task1();
    Task2();
    Task3();
    Task4();

  }
}


Es gibt viele Methoden, ein kooperatives Multitasking einzuführen. Ein C-Befehl wurde extra dafür gemacht (setjmp / longjmp)

Hier ein Beispiel;

#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>

void Task1() {
static int first=1;
static jmp_buf buf;
int x;
 
  if (first) {
    first=0;
  }
  else {
    longjmp(buf,1);
  }
  while(1) {
    printf("Task1 Start\n");
    x=setjmp(buf);
    if (x==0) return;
    printf("Task1 - Point 2\n");
    x=setjmp(buf);
    if (x==0) return;
   
  }
}


void Task2() {
static int first=1;
static jmp_buf buf;
int x;
 
  if (first) {
    first=0;
  }
  else {
    longjmp(buf,1);
  }
  while(1) {
    printf("Task2 Start\n");
    x=setjmp(buf);
    if (x==0) return;
    printf("Task2 - Point 2\n");
    x=setjmp(buf);
    if (x==0) return;
   
  }
}


#define taskInit()    static jmp_buf buf2; static int first=1; int x; if (first) { first=0; } else longjmp(buf2,1);
#define taskSwitch()  x=setjmp(buf2); if (x==0) return;

void Task3() {
  taskInit();
 
  while(1) {
    printf("Task3 Start\n");
    taskSwitch();
    printf("Task3 - Point 2\n");
    taskSwitch();
  }
}



int main() {
  while(1)  {
    Task1();
    Task2();
    Task3();
  }
}


Es ist sicher erkennbar, wie einfach sich die Methoden des Task-Wechsels austauschen lassen.

setjmp/longjmp ist ebenfalls seit den Anfangstagen dabei. Es beinhaltet sogar ein rudmentäres Sichern der Stack-Umgebung.

Es ist deshalb etwas langsamer, als die erste Methode.


Scheduler

Bisher war es nur ein einfacher Superloop. Alle Tasks kommen nacheinander dran, um einen Taskteil abzuarbeiten. Aber es gibt noch keinerlei

Verwaltung der Tasks. Dafür braucht man einen Scheduler, der die Tasks verwaltet. Hier ein Scheduler, der das Timing der Tasks überwacht.

Das Beispiel zeigt sehr deutlich, wie simpel man ein schon in dieser Form brauchbares Multitasking mit zeitlicher Steuerung herswtellen läßt.

Würden ein paar Namen geändert, dann würde es als ein RTOS-Beispiel durchgehen.

Der formale Programmaufbau ist sehr ähnlich !


Lediglich micros() ist OS/Hardware abhängig.

Es läuft in dieser Form auch auf den ältesten Computern und modernen 8-Bit Microcontrollern (Arduino) bis hin zum Supercomputer.

    Leicht anpassbar

    Überall gleich schnell

    Gut geeignet für Simulationen

    Kleine Einarbeitungszeit


// (C) 2019 Dipl. Phys. H. Weber

#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>
#include <time.h>

#define LINUX

// --------------------------------------------------------------------
// This part is depends on OS or hardware:
#ifdef LINUX
long micros() {
long            ms; // Microseconds
time_t          s;  // Seconds
struct timespec spec;
static int first=1;
static long start;

    clock_gettime(CLOCK_REALTIME, &spec);
    s  = spec.tv_sec;
    if (first) { first=0; start=s*1000000 + spec.tv_nsec / 1000; }
    return s*1000000 + spec.tv_nsec / 1000       -  start;  
}
#endif
// --------------------------------------------------------------------

 


#define taskInit()      static jmp_buf buf2; static int first=1; int x; if (first) { first=0; } else longjmp(buf2,1);
#define taskSwitch()    x=setjmp(buf2); if (x==0) return(0);
#define taskDelay(mics) x=setjmp(buf2); if (x==0) return(mics);

long Task1() {
static int i;
  taskInit();
  while(1) {
    printf("%ld Task1 Start #%d\n", micros() ,i++);
    taskSwitch();
    printf("%ld Task1 - Point 2\n", micros());
    taskDelay(1000000);
  }
  return 0;
}


long Task2() {
  taskInit();
  while(1) {
    printf("%ld Task2 Start\n", micros());
    taskDelay(500000);
    printf("%ld Task2 - Point 2\n", micros());
    taskDelay(500000);
  }
  return 0;
}


long Task3() {
  taskInit();
 
  while(1) {
    printf("%ld Task3 Start \n", micros());
    taskSwitch();
    printf("%ld Task3 - Point 2\n", micros());
    taskDelay(2000000);
  }
  return 0;
}


long Task4() {
static long loops;
  taskInit();
 
  while(1) {
    loops++;
    if ((loops%10000000)==0) {
      printf("%ld Task4 x 10.000.000 X %ld\n", micros(), loops/10000000);
    }
    taskSwitch();
  }
  return 0;
}


enum state { READY, DELAYED };

struct task {
  int     ID;
  state   State;
  long    (*Func)();
  long    Delay;
  long    LastCalled;
};


#define MAXTASKS 10
int     nTasks;

struct task Tasks[MAXTASKS];


// Create a task
void MakeTask( long (*f)() ) {
  Tasks[nTasks].ID = nTasks;
  Tasks[nTasks].State = READY;
  Tasks[nTasks].Func = f;
  Tasks[nTasks].Delay = 0;
  nTasks++;
}

// Start the tasks
void Scheduler() {
long ret;
  for (int i=0; i<nTasks; i++) {
    if ((micros()-Tasks[i].LastCalled) >= Tasks[i].Delay) Tasks[i].State=READY;
    if (Tasks[i].State==READY) {
      ret=Tasks[i].Func();
      Tasks[i].LastCalled=micros();
      Tasks[i].Delay=ret;
      if (ret) Tasks[i].State=DELAYED;
    }
  }
}
 


int main() {

// create 4 tasks
  MakeTask(Task1);
  MakeTask(Task2);
  MakeTask(Task3);
  MakeTask(Task4);
 
// run the created tasks
  while(1)  {
    Scheduler();
  }
}


CoopOS ist natürlich doch etwas komplexer - aber immer noch sehr überschaubar!

Aber das Beispiel zeigt die grundlegenden Merkmale eines (kooperativen)  RTOS.

Viele Beispiele in Tutorials für RTOS lassen sich so sehr einfach übernehmen!


Ich hoffe, ich konnte das Prinzip von CoopOS verdeutlichen. Sonst hilft: kopieren und ausprobieren.

Top













Vergleich RTOS CoopOS


 Eigenschaft
 RTOS
 CoopOS
 Effizienz = Auslastung der CPU
 Hoch
 Mittel
 Interrupt Latenz
 Kurz
 Sehr kurz
 Taskswitch Zeit
 Mittel
 Sehr kurz
 Zeit vom Interrupt bis zum Task
 Mittel
 Sehr kurz
 Task Delay in
 ms
 µs
 Automatisches Starten eines höher priorisierten Tasks
 Nein
 Ja
 Automatische Prioritäten Änderung
 Teilweise
 Ja
Taskstart deterministisch im µs Bereich
 Nein
 Ja
 Semaphoren für Resourcen (Screen) erforderlich ?
 Ja
 Nein
 Semaphoren möglich
 Ja
 Ja
Schutz der Tasks gegeneinander
 Hoch
 Niedrig
 Schnelle gegenseitige Steuerung der Tasks
 Nein
 Ja
 Nutzung des "Idle" Tasks
 Teilweise
 Ja


Top