[Projekt] Multitasking

Hi.

Das Thema (kooperatives) Multitasking und endliche Automaten wird immer wieder gerne genommen. Das ist auch nicht wirklich ein großes Wunder, denn viele, ja fast alle, Arduino Programme lassen sich auf diese "Software Design Pattern" runter brechen.
SPS Programmierer sind da besser dran, die kennen fast nichts anderes.

Die Stichworte mal als Sammlung:
Endlicher Automat, State Machine, Multitasking,
Coroutinen, Ablaufsteuerung, Schrittkette,
BlinkWithoutDelay, Ersetzen von delay(),
Unterbrechen von Schleifen

Als Wunderwaffe gegen alle diese Sorgen möchte ich euch meine TaskMacros
vorstellen. Siehe, die Arduino Library im Anhang.
OK, ok, auch Wunderwaffen können nicht alles, bzw. haben ihre Einschränkungen...

Der Werkzeugkasten:

Als erstes mal die Umgrenzung einer Task:
taskBegin() Damit wird ein Task Block eingeleitet
taskEnd() Das Ende eines Taskblockes

Taskkontrolle:
taskSwitch() Gibt die Rechenzeit an andere Tasks ab. Es wird im nächsten Durchlauf an dieser Stelle weiter gearbeitet.
taskPause(interval) Diese Task pausiert, bis interval abgelaufen.
taskWaitFor(condition) Diese Task pausiert, bis condition true wird.

Genug der Theorie!
Hier mal ein Beispiel für BlinkMitDelay:

void blink()
{
   static byte count = 0;
   while(1)   // Endlosschleife, totale Blockade
   {
      count++;
      digitalWrite(13,count & 1);
      delay(500);  // blockiert für 500 ms
   }   
}

void setup()
{
  pinMode(13, OUTPUT);
}

void loop
{
  blink();
}

Das funktioniert!

Die Nachteile dieses Vorgehens sind natürlich offensichtlich.
Sowohl die while Schleife, als auch das Delay, blockieren.
Möchte man noch zusätzlichen Kram in loop() unter bringen, wird man daran versagen.

Der Umbau auf BlinkOhneDelay:

#include <TaskMacro.h>

void blink()
{
   static byte count = 0;
   
   taskBegin();
   while(1)   // blockiert dank der TaskPause nicht 
   {
      count++;
      digitalWrite(13,count & 1);
      taskPause(500);   // gibt Rechenzeit ab
   }
   taskEnd();   
}

void setup()
{
  pinMode(13, OUTPUT);
}

void loop
{
  blink();
}

Noch ein Beispiel....
Blink so modifiziert, dass man eine kurze Leuchtdauer (100ms) und eine lange Dunkelphase (700ms) bekommt

void blink()
{
   taskBegin();
   while(1)   // blockiert dank der TaskPause nicht 
   {
      // Schritt 1
      digitalWrite(13,1);  // LED ein
      taskPause(100);   // gibt Rechenzeit ab
      
       // Schritt 2
      digitalWrite(13,0);   // LED aus
      taskPause(700);   // gibt Rechenzeit ab
      
      // gehe zu Schritt 1
   }
   taskEnd();   
}

Eine einfache Form der Schrittkette.

In den Beispielen finden sich noch ein paar Varianten. Z.B. wie man For Schleifen unterbrechbar macht. Und das Warten auf Flags.

Noch was erwähnenswertes? .....
Klar!

1:
Man sollte bedenken, dass lokale Variablen immer nur einen Durchlauf überleben. Man wird in solchen Tasks also eher statische, oder globale, Variablen verwenden.

2:
Dinge, welche bei jedem Aufruf erledigt werden wollen, haben sich vor taskBegin(); einzufinden.

3:
Switch/Case sind nicht in den Tasks verwendbar, da ich diese Kontrollstruktur darin für die Steuerung verwende.

Ansonsten:
Viel Spaß mit diesen Macros....!

Verbesserungen?
Vorschläge?

PS:
An dieser Stelle möchte ich mich insbesondere bei "Adam Dunkel" und "Donald E. Knuth" bedanken.


Edit:
Neue Version hochgeladen
(Die alte Version wurde 15 mal runter geladen)

Schritte können jetzt benannt und angesprungen werden. Das hilft deutlich beim Schrittkettenbau. Zu finden im Schrittketten Beispiel.
Dieses Beispiel besteht aus 3 Tasks:

  1. Taste entprellen
  2. Blinken in 2 Geschwindigkeiten, per Taste umschaltbar
  3. Zeigen der Loopdurchläufe pro Sekunde (ca 62000 auf einem Uno)
    Sicherlich könnte man dieses Beispiel noch etwas mehr straffen/optimieren....

TaskMacro.zip (3.6 KB)

1 Like

Hast Du die Makros schon mal ausprobiert?
Was soll das do - while(0) in taskSwitch?

Wenn taskSwitch() mark=LINE setzt, und case LINE ans Ende der Zeile, wie soll dann der Rest von taskPause (vor taskSwitch) mehr als einmal durchlaufen werden?
IMO sollte taskPause und taskWaitFor eher so aussehen:

#define taskPause(interval) timeStamp = millis(); taskSwitch(); if ((millis() - timeStamp) < (interval)) return;
#define taskWaitFor(condition)  taskSwitch(); if (!(condition)) return;

Ich vermute außerdem, daß jede Task in genau 1 Funktion liegen muß, in der mark und der switch definiert ist. Aufrufe der Makros in anderen Funktionen dürften nicht compilieren.

Ich vermute außerdem, daß jede Task in genau 1 Funktion liegen muß, in der mark und der switch definiert ist.

Eine Task befindet sich in einer Funktion
Eingerahmt von taskBegin() und taskEnd()
Nur der Speicherverbrauch setzt der Anzahl Funktionen/Tasks Grenzen.
Und, keine Datei, welche eine (oder mehrere) Task beinhaltet, darf mehr als 32kZeilen haben.
(könnte man mit unsigned int auf 64kZeilen erhöhen.)

Aufrufe der Makros in anderen Funktionen dürften nicht compilieren.

Wenn in der Funktion kein taskBegin() und taskEnd() in der richtigen Reihenfolge steckt, dann machen die TaskKontroll Dinger keinen Sinn, und werden Errors werfen.

Hast Du die Makros schon mal ausprobiert?

Natürlich!
:wink: Du auch? :wink:

Was soll das do - while(0) in taskSwitch?

Weil man sonst nicht den Einsprungspunkt für den nächsten Durchlauf hin bekommt.
(zumindest ich nicht, bisher)
Dieser Einsprungspunkt ist für jeden Taskwechsel unabdingbar nötig.
Der Compiler optimiert die tote Schleife weg.
Sie dient also nur dazu Syntaxfehler zu vermeiden.

IMO sollte taskPause und taskWaitFor eher so aussehen:

Das wird nicht funktionieren.
Denn dann wird kein Wiedereintrittspunkt für die Prüfung generiert. Die Prüfung muss ja öfter wiederholt werden, da bietet sich eine Schleife an. Ein if würde ja nur einmal prüfen.

Wenn taskSwitch() mark=LINE setzt, und case LINE ans Ende der Zeile, wie soll dann der Rest von taskPause (vor taskSwitch) mehr als einmal durchlaufen werden?

Die Frage verstehe ich nicht.....
Eigentlich kann ich dazu nur sagen: Es funktioniert!

PS:
Und Danke für den Kommentar/Nachfrage!

Und ja!
Ich treibe da bösen Schindluder mit dem switch/case Konstrukt.
Sonst bekommt man keine Schleifen aufgebrochen, ohne jeder Task einen eigenen Stack zu spendieren. Und das, das kann z.B. RTOS viel besser als ich.

Die Funktion der ProtoThreads ist vielleicht noch am ehesten mit diesen TaskMacros zu vergleichen.

Sehe ich vermutlich richtig: taskBegin und taskEnd allein nutzt nix:

void blink() {
   taskBegin();
   while(1)   
   {
      digitalWrite(13,!digitalRead(13));
      delay(100);
   }    
   Serial.println("Hier hin oder gar aus blink wieder raus kommt er nie");  
   taskEnd(); 
}

Wenn man es richtig verwendet, funktioniert es wohl.

  • geht nur innerhalb einer
    ** **void** **
    Funktion
  • taskBegin und taskEnd dürfen je Funktion nur einmal in richtiger Reihenfolge als Paar auftreten
  • dazwischen dürfen taskPause taskWaitFor taskSwitch stehen (auf jeweils eigenen Zeilen in der gleichen Funktion)
  • diese führen gegebenenfalls ein return aus. Es geht also nicht nach taskEnd(); weiter

Wir haben mal vor Urzeiten c-Makros geschrieben, die helfen sollten, etwas zu programmieren das wie FORTRAN aussah ( #define END } und sowas )

Ich mags nicht

  • weil es was anderes macht als wie es aussieht
  • weil Fehlermeldungen nur verständlich sind, wenn man die Makrodefinition kennt und versteht

Aber hübsch ist es schon, jedenfalls für Leute die auch gut ohne diese Makros auskämen :wink:

Sehe ich vermutlich richtig: taskBegin und taskEnd allein nutzt nix:

Richtig!
Sonst bräuchte jede Task ihren eigenen Stack.
Preemptives Multitasking, wäre das dann.

Wenn man es richtig verwendet, funktioniert es wohl.

Du hast die Bedingungen korrekt erkannt.
Ich werde diese als Anregung für die Erweiterung der Doku nehmen.

  • geht nur innerhalb einer void Funktion

Da habe ich lange dran geknackst...
Aber keinen eleganten Weg gefunden.

Aber hübsch ist es schon, jedenfalls für Leute die auch gut ohne diese Makros auskämen

Danke, das geht runter, wie Honig!

Es geht also nicht nach taskEnd(); weiter

Doch schon...
Unter Umständen...
z.B. wenn sich in der Task keine Endlosschleife befindet.

Erst mal: ich habe das Ampel-Beispiel jetzt ausprobiert, und es funktioniert tatsächlich. Bis hierhin sieht die Bibliothek wirklich brauchbar aus :slight_smile:

Mich hätte jetzt aber brennend interessiert, was der Präprozessor draus macht - wie komme ich da dran? Vielleicht macht der ja noch mehr unentdeckte Schweinereien?

Es sieht tatsächlich so aus, als ob der Compiler ohne Schleife den case LINE wegoptimiert. Igitt igitt, aber gerade noch erklärbar.

Mein Vorschlag für taskPause funktioniert durchaus, da dürften beide Varianten den gleichen Code erzeugen.

Bei taskWaitFor hingegen tritt das Problem auf, unter welchen Umständen der nachfolgende Code ausgeführt wird. Folgt darauf kein weiterer taskSwitch, wird die Bedingung bei jedem Durchlauf erneut geprüft und der nachfolgende Code so lange ausgeführt, als die Bedingung wahr ist. Wird die Bedingung absichtlich oder versehentlich false, bleibt die Task hängen. Deshalb würde ich sicherheitshalber ein weiteres taskSwitch einfügen, wenn die Bedingung das erste mal true war, damit sie nicht weiterhin geprüft wird. Leider kriege ich das per Makro nicht hin, da case LINE ja schon vergeben ist :frowning:

Ähnlich würde ich auch bei taskEnd ein taskSwitch einfügen, damit das Ende des switch (ab dem letzten taskSwitch) nicht endlos wiederholt wird. Das dürfte dann auch das Problem mit taskWaitFor beheben. Oder wait=0 setzen, damit es bei taskBegin weitergeht - dann kann man sogar die while(1) Schleife zwischen taskBegin und taskEnd einsparen.

Damit reduzieren sich meine Änderungsvorschläge hierauf:

#define taskEnd() wait = 0; }

Was die void Funktion betrifft, die könnte man mit taskBegin kombinieren, ähnlich ISR:

TASK() //incl. taskBegin
...
ENDTASK() //incl. taskEnd

Geschweifte Klammern je nach Geschmack hinzufügen.

Mich hätte jetzt aber brennend interessiert, was der Präprozessor draus macht - wie komme ich da dran?

Kein Problem... :wink:
Build Meldungen aktivieren.
Dann zeigts dir das Build Verzeichnis.
Im Build Verzeichnis findet sich ein Ordner namens preproc.
Da drin findest du es aufgedröselt.

Man könnte ja auch noch in beginTask ein neues delay() Makro hinzufügen bzw. aktivieren, und in endTask wieder löschen. Aber bei dem dafür notwendigen Aufwand erscheint mir taskPause dann doch eleganter...

combie:
Build Meldungen aktivieren.
Im Build Verzeichnis findet sich ein Ordner preproc
Da findest du es aufgedröselt.

Danke für dieses Puzzle, aber ich hab's noch geschafft :wink:

Seltsamerweise funktioniert es jetzt auch ohne die Schleife in beginTask. Sehr undurchsichtig :frowning:

Hallo combie,

schön für die Mühen, aber ist das nicht ein umgedrehtes Intervall?
Sieht alles nach negativer Logik aus.
Und sollten Multitasking Aufgaben nicht eher vom OS verwaltet und ausgeführt werden statt vom Userprogramm?

Kannst aber gern weitermachen. Bin gespannt was noch bei rauskommt. :slight_smile:

Je länger ich mich mit den Makros befasse, desto mehr bin ich davon begeistert :slight_smile:

Vor allem Anfänger dürften davon profitieren, die von den komplizierten Lösungen wie BlinkWithoutDelay abgeschreckt sind. Aber auch Fortgeschrittene können umfangreicheren Code damit sehr übersichtlich hinschreiben, besser als mit verschachtelten oder scheinbar unmotiviert aufeinanderfolgenden umfangreichen Abfragen. Die Effizienz könnte sogar höher sein als mit der sonst üblichen Technik, zumindest habe ich in dieser Richtung keine ernsthaften Bedenken.

Mich persönlich stört die while-Schleife zwischen taskBegin und taskEnd, und die läßt sich einfach vermeiden, wenn man in taskEnd mark=0 setzt. Eine Frage ist dabei, ob diese Schleife ein besseres Gefühl dafür vermittelt, wie der Code tatsächlich abläuft. Läßt man die Schleife weg, weil die Task nur einmal ausgeführt werden soll, dann hört sie tatsächlich garnicht auf, sondern wiederholt den letzten Teil (ab dem letzten case label) endlos. Dieses Verhalten empfinde ich jedenfalls wenig intuitiv.

Weitere Verbesserungsmöglichkeiten sind mir schon eingefallen, allerdings noch keine Lösungen dazu. Ich sollte die wohl einzeln zur Debatte stellen.

Das Timing mit taskPause ist etwas unpräzise, wie man am Ampel Beispiel sehen kann. Ersetzt man dort in piepser() den letzten taskSwitch durch TaskPause(500), dann müßte der letzte Piep eigentlich nach dem Umschalten der Ampel (alle 2000ms) erfolgen - tut er aber meist nicht! Für zeitgenaues Loggen oder die Erzeugung von Impulsen ist das eher ungeeignet.

Vielleicht könnten dafür zwei Makros verwendet werden, mit taskPause oder taskDelay für unexaktes Warten als direkter Ersatz für delay(), und taskResume o.ä. das relativ zum letzten timeStamp wartet? Die Implementierung wäre relativ simpel

#define taskResume(interval) timestamp+=interval; while (millis()-timestamp < (interval)) taskSwitch();

Nur der Name gefällt mir noch nicht.

Nett das du es mal wieder versuchst mit Tasks.

Ich hab es damals mal versucht, es ist aber leider untergegangen:

http://forum.arduino.cc/index.php?topic=165552.0

Das Projekt habe ich seit dem nicht weiter verfolgt. Die Erkenntnisse sind dann mit in die LCDMenuLib geflossen.

Jomelo:
Nett das du es mal wieder versuchst mit Tasks.

Ich hab es damals mal versucht, es ist aber leider untergegangen:

Projektvorstellung: simpleThreads auf dem Arduino mit Hilfe von Makros - Deutsch - Arduino Forum

Das Projekt habe ich seit dem nicht weiter verfolgt. Die Erkenntnisse sind dann mit in die LCDMenuLib geflossen.

Hi..

Hmm.. das hatte ich bisher nicht gefunden....
Das war wohl vor meiner Zeit hier :wink:

Dein TaskDingen verfolgt ja einen ganz anderen Ansatz. Den werde ich mir auch mal genau anschauen...

Mir drehte es sich eher darum, einen 1:1 DropIn für delay() und nebenläufige Fäden trotz lang laufender Schleifen zu bekommen.

Lange habe ich darüber gegrübelt, sowas wie einen TaskKontrollBlock einzuführen, aber dann verworfen. Das hätte mehr Möglichkeiten geschaffen, wäre aber auch um Größenordnungen komplexer geworden. Auch für den Anwender, und genau das wollte ich nicht.

Das Timing mit taskPause ist etwas unpräzise,

Ja ...
Dass das Ganze kein Echtzeitbetriebsystem wird, dank des kooperativen Charakters auch nie werden können wird, darf einen nicht verwundern.
Will man es exakter, wird man die Hardwaretimer bzw. ein RTOS bemühen müssen.

Im Ampel-Test war noch ein Denkfehler drin. Bei 5 Pieps im Abstand von 500ms kommt der letzte ja bereits nach 2000ms, also etwa zeitgleich mit der Weiterschaltung der Ampel, und nicht erst nach 2500ms. Tatsächlich kommt der letzte Piep sauber nach dem letzten Ampel-Zustand, wenn das Intervall auf 501ms hochgesetzt wird. So schlimm ist es mit dem Zeitversatz also doch nicht :slight_smile:

Bei meinen angedachten Erweiterungen geht es nicht um hochkomplizierte Funktionen aus Echtzeit-Systemen, sondern was sich mit vergleichbar einfachen Mitteln an Standard-Aufgaben sonst noch erledigen lassen kann.

Anfänger dürften davon profitieren, die von den komplizierten Lösungen wie BlinkWithoutDelay abgeschreckt sind

Das Problem bei Makros ist, dass der Compiler was anderes sieht als was der Anfänger geschrieben hat, und eventuelle Fehlermeldungen dadurch noch viel unverständlicher werden als sie für Anfänger so schon sind.

Wem BlinkWithoutDelay zu komplex ist, dem ist nicht zu helfen der wird auch mit den Multitasking Makros Probleme haben :confused:

Wenn man weiss, dass diese Makros dazu dienen, heimlich aus der umgebenden Funktion zu verschwinden um beim nächsten Mal genau dort wieder weiter zu machen, und man damit gedanklich zurecht kommt, schön. Damit kann man sicher übersichtlich AblaufSteuerungen definieren.

Wenn man weiss, dass diese Makros dazu dienen, heimlich aus der umgebenden Funktion zu verschwinden um beim nächsten Mal genau dort wieder weiter zu machen,

Das hast du schön formuliert.
.... "heimlich" .... "verschwinden" ....
:wink:

Was gefällt euch an Jomelos simpleThread nicht?

Hallo,

ich hatte ja schon einmal versucht zu fragen, bekam aber keine Antwort. Im Grunde ist das ja die negative Logik von deinem Intervall Projekt. Womit ich nicht klar komme vom Verständnis her ist folgendes.
Man möchte doch das die loop so schnell wie möglich durchlaufen wird.
Da widerspricht es doch das man hier feste Zeiten für andere Dinge frei gibt.
Statt so schnell wie möglich sein Ding fertig zu machen und dann kann das nächste dran kommen.

ich hatte ja schon einmal versucht zu fragen, bekam aber keine Antwort.

Sorry, ich hatte deine Frage nicht verstanden.
Auch jetzt noch nicht wirklich.

Aber, ich versuchs mal....

Man möchte doch das die loop so schnell wie möglich durchlaufen wird.

Das stimmt!
Findet bei den ganzen Beispielen ja auch mehr als 100.000 mal pro Sekunde statt.

Da widerspricht es doch das man hier feste Zeiten für andere Dinge frei gibt.

Nöö...
Denn die "freie" Zeit geht ja an loop(), im Grunde so, wie es das Interval Macro auch macht.

Ich halte es also nicht für inverse Logik, sondern eher für die gleiche.

Hmmm.
War es das, was du wissen wolltest?

Nachtrag:
Ah....
Ich glaube, jetzt... kommts mir...

Zu Anfang sagte ich:

taskSwitch() Gibt die Rechenzeit an andere Tasks ab. Es wird im nächsten Durchlauf an dieser Stelle weiter gearbeitet.

Der erste Teil ist wohl missverständlich.
Die Rechenzeit wird nicht direkt an andere Tasks abgegeben, sondern der Programmfluss wird per Return an die aufrufende Funktion übergeben, und loop() kann dann andere Tasks aufrufen, bzw. zusätzliche Aufgaben erledigen. Das gilt natürlich auch für die anderen Taskkontrol Dinger..