Unbeabsichtigter Aufruf einer virtuellen Methode.

Ok, Du hast es gewollt :wink:

  1. Es gibt keinen TaskStatus. (Task.h: enum state {READY, DELAYED, ..} class Task { state State; ... }

Das führt dazu, dass auch pausierende Tasks aufgerufen werden, die - da pausierend - sofort mit taskSwitch:

#define taskSwitch()                               \
    do {                                                     \
        lable = &&LC_CONCAT(taskLable, __LINE__);    \
        return;                                                                    \
       LC_CONCAT(taskLable, __LINE__):;                    \
    } while(0)


#define taskPause(Task_interval)                                  \
    timeStamp = millis();                                                 \
    while((millis() - timeStamp) < (Task_interval)) taskSwitch()

zurückzukehren. Welch eine Verschwendung. Bei länger wartenden Tasks kann sich das millionenfach wiederholen, so dass 20 wartende Task einen Task, der jetzt dran ist, erheblich verzögern können.
Ergebnis: 200000 TaskSwitches /s aber dabei nur echte 2 TaskSwitches für READY Tasks.
Ein Task, der pausiert, darf nicht aufgerufen werden! Punkt.

Bei RTOS ( die, die mit Listen arbeiten) wird ein pausierender Task auf eine andere Liste verschoben.

  1. Die Verwaltung mit der Liste ist fragwürdig. Es wird nur zugefügt. Das geht besser (und schneller) mit einem
    Array[MAXTASKS];
    Damit ist auch eindeutig, ob ein neuer Task Platz hat - da statisch reserviert. Mit "new" muss in jedem Fall (vorab) kontrolliert werden, ob der Speicherplatz dafür noch reicht.

  2. Und wann und wie lösche ich einen Task? Jederzeit? Oder nur bei einem bestimmten Zustand seiner selbst?

  3. Es fehlen Zustandüberwachungen wie: (serial output):
    Starte Task nur, wenn Resource frei ist.
    Oder: Bleibe blockiert, bis ein anderer Task (oder Interrupt) dich wieder aktiviert.

  4. Es fehlen Taskprioritäten, um dafür zu sorgen, dass der wichtigere (READY-) Task zuerst aufgerufen wird.

  5. Die Tasks (A,B,C) werden "rückwärts" initialisiert und auch (bei gleichen Delays) abgearbeitet.
    Das kann bei Tasks, deren Wirkung - gerade beim Start - aufeinander aufbaut, zu Problemen führen:

Sch-Begin: BlinkTask begin, pin: 5 Name: C <<<
Sch-Begin: BlinkTask begin, pin: 12 Name: B <<<
Sch-Begin: BlinkTask begin, pin: 2 Name: A <<<
construct task: Add
Constr 14
BlinkTask begin, pin: 14 Name: F
construct task: Add
Constr 14
BlinkTask begin, pin: 14 Name: Hallo1
Loop Task: Hallo1 Pin:14 <<< Reihenfolge rückwärts
Loop Task: F Pin:14 <<< ...
Loop Task: C Pin:5
Loop Task: B Pin:12
Loop Task: A Pin:2

Folge von: An Liste (add(Node)) wird vorne angefügt - nicht hinten!

Mit anderen Worten: als "Fingerübung" sehr schön - in der Praxis so (noch!) nicht zu gebrauchen.
Also: Tasks haben Zustände (state), die vom Scheduler berücksichtigt werden:

For i < TaskAnzahl

    if (DELAYED) 
      Test, ob von DELAYED auf READY

    if (READY) 
      call Task

End For

Eine solche Schleife übergeht z.Bsp. auch BLOCKED Tasks ohne Zeitverlust.

Warum taskPause in millis() statt micros(), wenn 200000 TaskSwitches ausgeführt werden (alle 5 us) ?

Jeder Task muss von class Task ableitet werden. Warum ? Es reicht, wenn class Task die Funktionspointer für
begin, loop
enthält. Wobei loop eigentlich reicht.
Dann können mit TaskInit("name", BeginPt, LoopPt); Funktionen vom Scheduler aufgerufen werden - wesentlich übersichtlicher. Alle Tasks sind von class Task - egal, was sie letztendlich tun!

Tasks können als einfache Funktionen (mit taskBegin, taskEnd) deklariert sein - wesentlich übersichtlicher und einfacher, als für jeden neuen Task eine neue Klasse zu definieren/abzuleiten.
Sinn von OOP ist es schließlich, Dinge zu abstrahieren und die Benutzung des Gesamtproduktes zu vereinfachen, statt sie unnötig zu komplizieren.

Da aber TaskLoop sowieso vom Scheduler aufgerufen wird, ist Task.h eigentlich auch überflüssig:
Es reicht struct (von mir aus class) Task:

class task {
    char     *name;
    char     ID;
    state    State;
    void     *Param;
    void     (*FunctionLoop)();  // Von InitTask besetzt
...
}

Damit kann - wenn gewünscht - auch eine Parameter-Struktur an einen aufgerufenen Task weitergereicht werden. Die auch von mehreren Tasks benutzt werden kann.

Bitte nicht böse sein. Ich finde Deinen Ansatz nach wie vor Klasse! Aber etwas lösen von den "Fingerübungen" und mehr die Praxis im Hinterkopf haben könnte Sinn machen.

Hier eine simple Aufgabe:
BlinkTask1 : Loop: schalte LED 1000 us HIGH, dann 1000 µs LOW
BlinkTask2 : Wenn Blink2 auf LOW geschaltet hat - und nur dann:
Loop: schalte LED 100 us High, dann 100 us LOW.

Lasse 5 weitere Tasks laufen und kontrolliere das Ergebnis mit einem LA:
Wie genau werden die Zeiten eingehalten? Wie lange braucht Task2, um auf die LOW Flanke von
Task1 zu reagieren?

Eine gute Intertask-Kommunikation ist sehr wesentlich für ein solches System. Reine Delays sind die Ausnahme!

Ich hoffe sehr, ich habe eher Deinen Ehrgeiz angestachelt, als Dich entmutigt.
Bleibe am Ball - Du bist auf einem guten Weg - der noch weit ist. Durchhalten :wink:

Mit besten Wünschen und Grüßen