Go Down

Topic: [Projekt] Multitasking (Read 4895 times) previous topic - next topic

combie

Jul 28, 2016, 11:53 am Last Edit: Aug 20, 2019, 10:28 pm by combie
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.

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:
Code: [Select]

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:
Code: [Select]

#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
Code: [Select]

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....

Alle sagen: Das geht nicht!
Einer wusste das nicht und probierte es aus.
Und: Es ging nicht.

DrDiettrich

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:
Code: [Select]
#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.

combie

#2
Jul 28, 2016, 04:29 pm Last Edit: Jul 28, 2016, 06:02 pm by combie
Quote
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.)

Quote
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.

Quote
Hast Du die Makros schon mal ausprobiert?
Natürlich!
;-) Du auch?  ;-)

Quote
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.


Quote
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.



Quote
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.






Alle sagen: Das geht nicht!
Einer wusste das nicht und probierte es aus.
Und: Es ging nicht.

michael_x

Sehe ich vermutlich richtig:  taskBegin und taskEnd allein nutzt nix:
Code: [Select]
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 ;)

combie

#4
Jul 28, 2016, 06:39 pm Last Edit: Jul 28, 2016, 06:44 pm by combie
Quote
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.

Quote
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.

Quote
- geht nur innerhalb einer void Funktion
Da habe ich lange dran geknackst...
Aber keinen eleganten Weg gefunden.

Quote
Aber hübsch ist es schon, jedenfalls für Leute die auch gut ohne diese Makros auskämen
Danke, das geht runter, wie Honig!


Quote
Es geht also nicht nach taskEnd(); weiter
Doch schon...
Unter Umständen...
z.B. wenn sich in der Task keine Endlosschleife befindet.

Alle sagen: Das geht nicht!
Einer wusste das nicht und probierte es aus.
Und: Es ging nicht.

DrDiettrich

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

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 :-(

Ä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:
Code: [Select]
#define taskEnd() wait = 0; }



Was die void Funktion betrifft, die könnte man mit taskBegin kombinieren, ähnlich ISR:
Code: [Select]
TASK() //incl. taskBegin
...
ENDTASK() //incl. taskEnd
Geschweifte Klammern je nach Geschmack hinzufügen.

combie

#6
Jul 28, 2016, 08:09 pm Last Edit: Jul 28, 2016, 08:34 pm by combie
Quote
Mich hätte jetzt aber brennend interessiert, was der Präprozessor draus macht - wie komme ich da dran?
Kein Problem... ;-)
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.
Alle sagen: Das geht nicht!
Einer wusste das nicht und probierte es aus.
Und: Es ging nicht.

DrDiettrich

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...

DrDiettrich

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 ;-)

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

Doc_Arduino

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.  :)
Tschau
Doc Arduino '\0'

Messschieber auslesen: http://forum.arduino.cc/index.php?topic=273445
EA-DOGM Display - Demos: http://forum.arduino.cc/index.php?topic=378279

DrDiettrich

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

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.

DrDiettrich

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
Code: [Select]
#define taskResume(interval) timestamp+=interval; while (millis()-timestamp < (interval)) taskSwitch();
Nur der Name gefällt mir noch nicht.

Jomelo

#12
Jul 29, 2016, 03:28 pm Last Edit: Jul 29, 2016, 03:29 pm by Jomelo
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.
while(!success){try++;}

combie

#13
Jul 29, 2016, 04:02 pm Last Edit: Jul 29, 2016, 04:16 pm by combie
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.
Hi..

Hmm.. das hatte ich bisher nicht gefunden....
Das war wohl vor meiner Zeit hier ;-)


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.


Quote
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.






Alle sagen: Das geht nicht!
Einer wusste das nicht und probierte es aus.
Und: Es ging nicht.

DrDiettrich

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 :-)

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.

Go Up