Forum: Mikrocontroller und Digitale Elektronik Coroutinen für microController


von Jochen W. (jochen_w335)


Lesenswert?

Ahoi,

ich arbeite schon länger an einer Bibliothek namens CoCo, Coroutinen für 
microController auf Basis von C++ 20. Das verhält sich wie ein 
Multitasking-Betriebssystem, jedoch nur kooperatives Multitasking, kein 
preemptives Multitasking. Für zeitkritische Aufgaben reichen oft auch 
Interrupts, so dass man damit eigentlich gut "bedient" ist, daher will 
ich es mal kurz vorstellen. LEDs blinken lassen macht man so:

    Coroutine task1(Loop &loop) {
        while (true) {
            debug::toggleRed();
            co_await loop.sleep(200ms);
        }
    }
    Coroutine task1(Loop &loop) {
        while (true) {
            debug::toggleGreen();
            co_await loop.sleep(350ms);
        }
    }

    int main() {
        task1(drivers.loop);
        task2(drivers.loop);

        drivers.loop.run();
    }

Das Main-Programm wird normal ausgeführt, task1 unterbricht jedoch beim 
ersten co_await (hängt sich in die Event-Loop) so dass es mit task2 
weitergeht, was auch bei co_await unterbricht. Danach wird die Event 
Loop ausgeführt. Diese weckt die beiden Coroutinen nach den jeweiligen 
Zeiten auf, die die LEDs umschalten und sich dann wieder schlafen legen. 
Der Stack von task1 und task2 wird vom Compiler automatisch entweder 
statisch oder auf dem Heap angelegt, da muss man sich nicht drum 
kümmern, denn die Coroutinen "leben" ja dauerhaft weiter. "drivers" ist 
eine Struktur, die man für jede Kombination aus Mikrocontroller und 
Dev-Board anlegt und die in diesem Fall die Event-Loop enthählt 
(Windows, STM32 oder nRF52). Hier ein github-Link: 
https://github.com/Jochen0x90h/coco-loop/blob/main/test/LoopTest.cpp

Monochome Displays gehen auch schon: 
https://github.com/Jochen0x90h/coco-mono-display sowie Funkübertragung 
mit nRF5284. Es gibt eine ganze Reihe von coco-Bibliotheken, die man 
sich mit conan als Abhängigkeit ins Projekt einbinden kann. Startpunkt 
wäre also https://github.com/Jochen0x90h/coco

Würde mich mal interessieren was ihr davon haltet ;)

: Verschoben durch Moderator
von Michael B. (laberkopp)


Lesenswert?

Jochen W. schrieb:
> was ihr davon haltet

Nix.

uC sind event-driven.

Denk an Windows.

Events erfolgen durch Interrupts.

Da nicht jeder Interrupt voll abgearbeitet werden soll bevor der nächste 
Interrupt kommen darf, macht eine Queue Sinn, Message-Warteschlange in 
Windows.

Die loop (Arduino Jargon) nimmt dann den nächsten Auftrag aus der queue.

Das sollte das Prozessschema sein.

Ein Blinker wird also vom timer-interupt gesteuert. Nur das notigste:
1
void LEDblinkt(void)
2
{
3
   LED=!LED;
4
}
5
6
void loop(void) 
7
{ 
8
   switch(dequeue())
9
   {
10
   case M_Time:
11
      LEDblinkt();
12
      break
13
   }
14
}
15
16
void setup(void)
17
{
18
   initTimer(1000);
19
}
20
21
ISR(TIMER1_OVF_vect)
22
{
23
   enqueue(M_Time);
24
}

von Jochen W. (jochen_w335)


Lesenswert?

Ok das Beispiel war noch zu einfach da die Blink-Funktion ja keinen 
Zustand hat. Hier ist ein komplexeres Beispiel, ein VCP (virtueller 
COM-Port über USB): 
https://github.com/Jochen0x90h/coco-usb/blob/main/test/UsbSerialTest.cpp

    Coroutine echo(Device &device, Buffer &buffer) {
        while (true) {
            // wait until USB device is connected
            co_await device.untilReady();

            while (device.ready()) {
                // receive data from host
                co_await buffer.read();

                // send data back to host
                co_await buffer.write();
            }
        }
    }


Hier wartet die Coroutine bis USB verbunden ist, liest dann und schreibt 
zurück. Das passiert nebenläufig z.B. neben der Behandlung des 
Control-Endpoint. Besonders interessant für komplexere Protokolle wo man 
auf die Antwort der Gegenstelle warten muss bevor es weitergeht.

(wie bekommt man Syntax Higlight hin? muss man [c] vor jede einzelne 
Zeile schreiben?)

von Harald K. (kirnbichler)


Lesenswert?

Jochen W. schrieb:
> (wie bekommt man Syntax Higlight hin? muss man [ c] vor jede einzelne
> Zeile schreiben?)

Nein. Vor die erste Zeile des Codeabschnitts, und nach der letzten kommt 
ein [/c].

Dann sieht's so aus:
1
    Coroutine echo(Device &device, Buffer &buffer) {
2
        while (true) {
3
            // wait until USB device is connected
4
            co_await device.untilReady();
5
6
            while (device.ready()) {
7
                // receive data from host
8
                co_await buffer.read();
9
10
                // send data back to host
11
                co_await buffer.write();
12
            }
13
        }
14
    }

: Bearbeitet durch User
von Jochen W. (jochen_w335)


Lesenswert?

Komisch jetzt geht es auf einmal.

Jedenfalls ist die Idee von CoCo die Event Loop aus dem Beispiel von 
Michael B. in eine Library zu verpacken und dann statt Funktionen 
aufzurufen (LEDblinkt()) Coroutinen weiterlaufen zu lassen (resume() 
wird auf dem Coroutine-Handle aufgerufen wobei das Handle vom Typ 
std::coroutine_handle ist).

von Veit D. (devil-elec)


Lesenswert?

Hallo,

auch wenn ich keinen STM habe, ich finde es sehr gut. Respekt.

von Torsten R. (Firma: Torrox.de) (torstenrobitzki)


Lesenswert?

Jochen W. schrieb:

> Würde mich mal interessieren was ihr davon haltet ;)

Ich denke, coroutinen sind deutlich Ressourcen sparender, als threading. 
Ich muss geraden mit Zephyr arbeiten und wollte zum Debuggen alle 
Optimierungen ausschalten. Danach brauchte ich nur noch einen halben 
Tag, um für alle vom System gestarteten Threads die Stacks zu erhöhen, 
damit die Software nicht gleich beim Start in einen stack overflow 
läuft.

Es wird aber sicher noch Jahrhunderte dauern, bis die ersten Chip 
Vendors den Vorteil erkennen werden ;-)

von 900ss (900ss)


Lesenswert?

Ich finde Coroutinen auch sinnvoll. Allerdings würde ich es etwas 
generischer machen, unabhängig von dem jeweils verwendeten uc. 
Stattdessen ein I/F zur HW-Resourcen. Treiber für Timer, UART u.s.w. 
kann dann jeder für sich schreiben und einbinden. Dann wäre es auf 
"jedem" uc lauffähig. Oder habe ich da was übersehen? Ich hab es nur 
kurz überflogen.

von Christoph M. (mchris)


Lesenswert?

Warum das jetzt aus dem Projektordner in die Allgemeinheit geschoben 
wurde, ist unklar.

Jochen W. schrieb:
> Würde mich mal interessieren was ihr davon haltet ;)
Wäre als Arduino-Library interessant.

von Motopick (motopick)


Lesenswert?

Woanders macht Mann das so:
1
main() {
2
int i;
3
while(1) { // endless loop for multitasking framework
4
costate { // task 1
5
. . . // body of costatement
6
}
7
costate { // task 2
8
... // body of costatement
9
}
10
}
11
}

von Torsten R. (Firma: Torrox.de) (torstenrobitzki)


Lesenswert?

Motopick schrieb:
> Woanders macht Mann das so:

Da hatte Mann aber auch noch keine Geräte, die mit Batterien betrieben 
wurden...

von Rbx (rcx)


Lesenswert?

Torsten R. schrieb:
> Es wird aber sicher noch Jahrhunderte dauern, bis die ersten Chip
> Vendors den Vorteil erkennen werden ;-)

Es gab auch schon mal Lisp-Maschinen
https://de.wikipedia.org/wiki/Lisp-Maschine

Darüber hinaus parallelisiert Haskell erstklassig, aus verschiedenen 
Gründen. Schreibe ich nur deswegen, weil ich den Ansatz der "Coroutinen" 
für C++ ganz gut finde - und denke, es hilft vielleicht, wenn man mehr 
bei C++ bleibt bei der Betrachtung.

von Bana A. (bananen_bieger)


Lesenswert?

> Würde mich mal interessieren was ihr davon haltet ;)

Gaehn... machen wir schon seit 40 Jahren. Man bringt die Coroutine dann 
halt dort unter wo's passt.

Das Text-LCD-Display beschreiben zB im 10ms Timer Tick, weil das Display 
eh alles etwas langsamer, mit etwas Zeit dazwischen haben will. Also 
jeden Tick einen Character schreiben. 3 Refreshes pro Sekunde ist etwa 
das, was man einem Betrachter zumuten kann.

Eine Umwandlung eines Int oder Float Datentyps nach String fuer den 
Display im Idle Loop. Ein Digit pro Durchgang. Nein, ein Printf() gibt's 
nicht. Ist um Groessenordnungen zu klotzig.

Ein Regelungs routine im runtergeteilten Timer Tick. zB alle 100ms, 
jeden 10. Timer Tick.
Die Messwerte kann man falls das Regelsystem das zulaesst im freerunning 
ADC  mit Kanalwechsel im Interrupt ansaugen. Allenfalls startet man den 
Messzyklus auch mit dem Regel-Tick. Der Messzyklus kann auch mehrere 
Samples auf den mehreren Kanaelen messen, und auch gleich die 
Tiefpassung mit den Vorwerten machen.

usw. Alter Hut.

von Peter D. (peda)


Lesenswert?

Ich benutze schon länger einen Scheduler, ursprünglich auf dem 80C51 
entwickelt. Man kann ihm Callbacks übergeben, die dann nach der 
angegebenen Zeit einmalig oder periodisch ausgeführt werden. Damit 
lassen sich z.B. komplexe Ampelsteurungen programmieren, ohne daß es 
unübersichtlich wird. Man stellt einen Callback rein und muß sich nicht 
weiter darum kümmern. Callbacks können auch wieder gelöscht werden, z.B. 
für Timeouts.
Der Aufruf erfolgt in der Mainloop über ein Timerflag. Damit umgeht man 
Probleme mit mehreren Instanzen, wie bei Interrupts. Es ist also 
erlaubt, daß Callbacks weitere Callbacks einstellen oder entfernen 
können, ohne dafür Interrupts sperren zu müssen.
Die Callbacks werden in einer sortierten Liste angelegt. Somit hat man 
nicht dutzende Zähler, sondern nur einen einzigen für den nächsten 
Callback in der Liste. Periodische Callbacks sortieren sich einfach 
wieder selber in die Liste ein.
Besonders vorteilhaft für µCs mit wenig RAM ist es, daß nur Platz für 
die gleichzeitig aktiven Callbacks angelegt werden muß.
Eine Blink-LED trägt z.B. ein LED-Toggle als periodischen Callback ein.

von Torsten R. (Firma: Torrox.de) (torstenrobitzki)


Lesenswert?

Falls jemand wissen möchte, was coroutinen sind (Im wesentlichen, die 
Möglichkeit, eine Funktion zu verlassen und sie an der verlassenen 
Stelle, später weiter laufen zu lassen):

https://en.cppreference.com/w/cpp/language/coroutines

von Vincent H. (vinci)


Lesenswert?

Ich find das ziemlich cool, man sieht nur leider mal wieder dass das 
µc.net Forum schlichtweg nicht die richtige Anlaufstelle für solche 
Projekte ist. Vermutlich verstehen die meisten hier deinen Code auch 
nicht, anders kann ich mir nicht erklären weshalb gleich eine Hand voll 
Antworten kommen die Coroutinen mit Timern vergleichen.

Ich hoffe nur du hast dich mit dem "Framework" drum herum nicht 
übernommen. Eine sinnvolle Coroutine Library für Mikrocontroller zu 
schreiben ist schon kompliziert genug, ohne dann auch noch zig Devices 
mit Treibern usw. zu supporten. Als potenzieller Nutzer würde ich mir 
hier mehr Dokumentation fürs "eigentliche Produkt" wünschen. Ich hab 
C++20 Couroutines bisher nur grob überflogen und dann auf Grund der 
enormen Komplexität gleich wieder beiseite gelegt. Ich hab jetzt das 
README des eigentlichen 'coco' Repos gelesen und weiß nun weder ob die 
Coroutinen Heap benötigen, noch hab ich ein schnelles Snippet gesehen 
mit dem ich anfangen könnte.

Aber was nicht ist kann ja noch werden, sonst wie gesagt schon mal sehr 
cool!

von Peter D. (peda)


Lesenswert?

Vincent H. schrieb:
> Vermutlich verstehen die meisten hier deinen Code auch
> nicht ...

Ja, so geht es mir. Mit C++ habe ich so meine Probleme. Ich verliere 
schnell den Überblick, wenn man tausende Dateien öffnen muß. Ehe man 
sich bis zu den untersten Ebenen durchgekämpft hat, hat man längst 
vergessen, was man eigentlich verstehen wollte.
Ich habe daher nur überblickt, was auf den ersten Ebenen steht. Zu den 
eigentlichen Implementationen der Coroutinen bin ich noch nicht 
durchgedrungen.

von Torsten R. (Firma: Torrox.de) (torstenrobitzki)


Lesenswert?

Peter D. schrieb:

> ... Ich verliere
> schnell den Überblick, wenn man tausende Dateien öffnen muß. Ehe man
> sich bis zu den untersten Ebenen durchgekämpft hat, hat man längst
> vergessen, was man eigentlich verstehen wollte.


Das ist total normal. Sobald die Probleme größer werden und die 
komplette Lösung einfach nicht mehr in den Kopf passt, muss man etas 
anders vorgehen. Dann muss man die Software an definierten Stellen in 
Stücke schneiden und nur noch Teile der Software betrachten.

Wenn ich diese Schnittstelle dann gut beschreibe und die Implementierung 
der Schnittstelle gut getestet habe, dann muss ich halt nicht mehr genau 
verstehen, wie `schedule( task_a, next )` implementiert ist, wenn ich 
mich darauf verlassen kann, das die Funktion das macht, was sie machen 
soll.

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Wo hält eine Coroutine eigentlich ihren Status?  Einschlägige Dokus 
sagen "Heap", und damit wären C++ Coroutinen nicht wirklich gut für 
(kleine) µC geeignet.

: Bearbeitet durch User
von J. S. (jojos)


Lesenswert?

Bei Embedded hinken die Implementierungen der neueren Features oft 
hinterher, und auch unterschiedlich bei verschiedenen Compilern. Noch 
schwieriger wird es wenn es die Runtime Libs betrifft, ich musste schon 
feststellen das ein aligned_alloc was es schon seit C11 gibt lange 
Probleme machte.
Wie es sich mit dem Heap verhält kann man auf Systemen testen die einen 
Memory Trace anbieten. Würde ich mal testen, habe aber gerade keine 
Langeweile.

: Bearbeitet durch User
von Torsten R. (Firma: Torrox.de) (torstenrobitzki)


Lesenswert?

Johann L. schrieb:
> Wo hält eine Coroutine eigentlich ihren Status?  Einschlägige Dokus
> sagen "Heap", und damit wären C++ Coroutinen nicht wirklich gut für
> (kleine) µC geeignet.

https://en.cppreference.com/w/cpp/language/coroutines#:~:text=Coroutine%20state%20is%20allocated%20dynamically,operator%20new%20will%20be%20used.

von Jochen W. (jochen_w335)


Lesenswert?

Johann L. schrieb:
> Wo hält eine Coroutine eigentlich ihren Status?  Einschlägige Dokus
> sagen "Heap", und damit wären C++ Coroutinen nicht wirklich gut für
> (kleine) µC geeignet.

Meistens enthalten Coroutinen dann ja Endlosschleifen, also beenden sich 
nie. Der Heap fragmentiert dann ja auch nicht bzw. man könnte den 
operator new überladen und einfach fortlaufend Speicher rausgeben den 
man nicht wieder freigeben kann. Nach der Initialisierung ändert sich 
der Speicherfüllstand dann auch nicht mehr. Vielleicht kann man mit dem 
[ [noreturn]] Attribut an der aufrufenden Funktion dafür sorgen, dass 
die Coroutine auf dem Stack der aufrufenden Funktion angelegt wird. 
Müsste man mal genauer untersuchen.

von Jochen W. (jochen_w335)


Lesenswert?

Vincent H. schrieb:
> Aber was nicht ist kann ja noch werden, sonst wie gesagt schon mal sehr
> cool!

Klar ist alles viel mehr geworden als ursprünglich gedacht. Einstieg ist 
aber 
https://github.com/Jochen0x90h/coco-loop/blob/main/test/LoopTest.cpp wo 
man mit dem Prinzip experimentieren kann oder 
https://github.com/Jochen0x90h/coco-uart/blob/main/test/UartSendTest.cpp 
für Spaß mit der UART. Die "Treiber" setzen in separaten Libraries auf 
coco-loop (Event Loop) auf, paar sind schon vorhanden oder kann man sich 
selber schreiben. Ich selbst nutze es inzwischen für das erste Projekt 
auf Arbeit, daher supporte ich zumindest das was ich gerade brauche und 
vielleicht finden sich ja Mitstreiter die es auch nutzen oder sich 
inspirieren lassen und was eigenes machen

von Veit D. (devil-elec)


Lesenswert?


von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Jochen W. schrieb:
> Johann L. schrieb:
>> Wo hält eine Coroutine eigentlich ihren Status?  Einschlägige Dokus
>> sagen "Heap", und damit wären C++ Coroutinen nicht wirklich gut für
>> (kleine) µC geeignet.
>
> Meistens enthalten Coroutinen dann ja Endlosschleifen,
> also beenden sich nie.

Sie verlassen aber ihren Kontext und kehren wieder dahin zurück.  Der 
Kontext muss also irgendwo gespeichert werden, und zwar nicht auf dem 
Stack.

von Michael B. (laberkopp)


Lesenswert?

Johann L. schrieb:
> Sie verlassen aber ihren Kontext und kehren wieder dahin zurück.  Der
> Kontext muss also irgendwo gespeichert werden, und zwar nicht auf dem
> Stack.

Der komplette Stack GEHÖRT zum Kontext, zusätzlich zu den Registern.

Ein irrer Speicherverbrauch, elegant versteckt hinter einfachen 
Funktionen.

Ein Grund, warum aktuelle Software so gross und langsam ist.

Wer unbedingt wartende Routinen schreiben will weil er das irgendwie für 
einfacher hält
1
void receiveserial(void)
2
{
3
    co_await device.untilReady();
4
    b1=device.read();
5
    co_await device.untilReady();
6
    b2=device.read();
7
}
kann das im kooperativen Multitasking
1
void receiveserial(void)
2
{
3
    while(device.notready()) yield();
4
    b1=device.read();
5
    while(device.notready()) yield();
6
    b2=device.read();
7
}
8
void yield(void)
9
{
10
   // system queue message Bearbeitung, darunter:
11
   if(queue.size>0) switch(queue[0].msg)
12
   {
13
   case M_processstupidly:
14
       receiveserial(); 
15
       break;
16
   default:
17
      // alle anderen sinnvollen messages
18
   }
19
}
20
void loop(void) // siehe mein erster Beitrag
21
{ 
22
   yield();
23
}
und sieht gleich sehr gut, wie yield rekursiv aufgerufen wird und der 
ganze stack erhalten bleibt.

Macht man so spätestens seit Windows 1.0, aber selten weil es meist 
bessere Lösungen gibt wie state machines.

von Jürgen (temp1234)


Lesenswert?

Ich hab mir deine Lib gerade mal auf github angesehen. Du hast da eine 
Menge Arbeit geleistet aber ich denke dass das ehr nichts für die 
Allgemeinheit ist. Das liegt sicher einmal an der Masse des Codes, der 
Komplexität die viele überfordern wird und an der Verlässlichkeit, dass 
das Projekt auch noch in 5 Jahren existiert. Die Coroutinen an sich sind 
sicher nicht schlecht und ich ziehe diese Art der Programmierung einem 
rtos vor (wenn möglich). Im letzten Jahrtausend hatte Adam Dunkels mit 
seinen Protothreads einen ähnlichen Ansatz in reinem C veröffentlicht. 
So was ähnliches in C++ verwende ich auch hin und wieder.
Ganz eingetaucht bin ich nicht in deinen Code, das ist mir zu 
umfangreich. Im realen Leben schlagen sich viele ja auch mit noch einer 
Menge anderer Libs herum die sie nicht so einfach nach oder umentwickeln 
wollen oder können. Und immer häufiger sind die Beispiele für die 
allermeisten Sachen die über uart, spi oder i2c angebunden sind nur noch 
als Arduino-Libs verfügbar. Da passt dann eine neue Gesamt-Lib für alles 
nicht mehr dazu. Solche großen Libs die als Unterbau für alles genutzt 
werden können haben es sowieso sehr schwer. Ich denke das nur am mbed. 
Die Leute haben da sicher vielen guten Code geschrieben, schade dass das 
nun irgendwie alles für die Tonne ist.

von Andras H. (andras_h)


Lesenswert?

Michael B. schrieb:
> Nix.

Muss ich leider auch teilweise zustimmen. Grob gesagt (aber bitte nicht 
Wort wörtlich verstehen): Multitasking braucht man nur dann, wenn man 
selber nicht programmieren kann. Theoretisch, kann man alles in kleinen 
Stücke aufteilen. Der uC wird nicht all zu lange blockiert. Es reicht 
eine while schleife mit einfache Funktionsaufrufen aus. Das klappt in 
der Regel ganz gut, wenn man Funktionen mit delayms oder delayus 
vermeidet und anstelle dann State Machines programmiert. Bzw Polling 
nimmt anstelle auf etwas zu warten.

Allerdings gibt es Use-Cases wo Multitasking doch Vorteile hat. Zum 
Beispiel wenn man Zeitlich bestimme Events sonst nicht schnell genug 
bearbeiten kann. (Ja man kann darüber diskutieren wieso soetwas nicht in 
ein interrupt landet). Oder anderes Beispiel, wenn man nicht 
programmieren kann. Ich würde behaupten, wenn man alleine auf ein uC 
programmiert, dann kommt man relativ weit auch ohne Multitasking.

Wenn wir dann noch weitergehen, und nicht nur die Zeitliche beachten, 
sondern auch Speicherschutz. Dann kann man auch Tasks relativ gut 
voneinander schützten. Separate Stack, Memory Protection. Dann noch ein 
kleiner schritt und man ist bei VMs angekommen :)

von J. S. (jojos)


Lesenswert?

Andras H. schrieb:
> Muss ich leider auch teilweise zustimmen. Grob gesagt (aber bitte nicht
> Wort wörtlich verstehen): Multitasking braucht man nur dann, wenn man
> selber nicht programmieren kann.

Sehr arrogant und kurzsichtig, da hast du es sicher noch nicht mit 
komplexen Embedded Systemen zu tun gemacht. Embedded sind nicht nur 
Blinkys die noch ein paar Tasten verarbeiten müssen.
Wenn mir jemand so etwas in einem Vorstellungsgespräch erzählt ist das 
sehr schnell zu Ende.

: Bearbeitet durch User
von Peter D. (peda)


Lesenswert?

Muß das nochmal durchdenken.

: Bearbeitet durch User
von Jochen W. (jochen_w335)


Lesenswert?

Jürgen schrieb:
> Ich hab mir deine Lib gerade mal auf github angesehen. Du hast da eine
> Menge Arbeit geleistet aber ich denke dass das ehr nichts für die
> Allgemeinheit ist. Das liegt sicher einmal an der Masse des Codes, der
> Komplexität die viele überfordern wird und an der Verlässlichkeit, dass
> das Projekt auch noch in 5 Jahren existiert.

Klar da ist natürlich was dran daher stelle ich das ja mal der 
öffentlichen Diskussion und sollten welche mitmachen wollen dann erhöht 
das ja die Chance, dass es in 5 Jahren noch existiert. Mit Arduino ist 
auch richtig, es wurde ja auch schon vorgeschlagen das als Arduino-Libs 
zu verpacken. Das wäre dann eine andere "Darreichungsform", müsste mir 
mal ansehen ob das geht (bzw. wäre super wenn jemand, der viel 
Arduino-Erfahrung hat, das ansehen würde).

von Jochen W. (jochen_w335)


Lesenswert?

Johann L. schrieb:
> Jochen W. schrieb:
>> Johann L. schrieb:
>>> Wo hält eine Coroutine eigentlich ihren Status?  Einschlägige Dokus
>>> sagen "Heap", und damit wären C++ Coroutinen nicht wirklich gut für
>>> (kleine) µC geeignet.
>>
>> Meistens enthalten Coroutinen dann ja Endlosschleifen,
>> also beenden sich nie.
>
> Sie verlassen aber ihren Kontext und kehren wieder dahin zurück.  Der
> Kontext muss also irgendwo gespeichert werden, und zwar nicht auf dem
> Stack.

Ich meine wenn der Kontext auf dem Heap ist, man aber nur in der 
Initialisierungsphase ein paar Coroutinen startet, die Endlosschleifen 
enthalten, dann passieren zur Laufzeit keine Allokationen mehr und es 
kann keinen "out of Memory" geben.

von J. S. (jojos)


Lesenswert?

Kooperativ und Präemptiv haben beide Vor- und Nachteile, das sollte 
einem schon bewusst sein.
Präemptiv mit Tasks/Threads braucht eben mehr Speicher durch die eigenen 
Stacks je Thread und kostet Rechenzeit für die Kontextwechsel, dafür 
kann man Threads einrichten die wichtige Dinge mit Vorrang ausführen.
Bei Kooperativ darf keine Funktion blockieren, das würde die anderen 
stören. Trotzdem kann man das gut skalieren und es hat weniger Overhead 
wenn die Funktionen über Ereignisse ausgelöst werden.
Und man kann auch beide kombinieren wenn es das System hergibt.
Mit CoRoutinen vermeidet man lange unübersichtliche Dispatcher, ich 
finde die schon interessant. Sicherlich kommt man auf kleinen Systemen 
auch ohne aus, aber das Problem fängt immer dann an wenn der 
Funktionsumfang wächst und immer mehr angebaut wird. In der c't gab es 
mal ein schönes Bild, im meine zu Windows98, wo ein schöner Palast 
sichtbar war, der aber auf einem fragilen Fundament stand. Da ist eine 
solide Basis besser wenn man etwas erweitern möchte.

: Bearbeitet durch User
von Jürgen (temp1234)


Lesenswert?

Jochen W. schrieb:
> es wurde ja auch schon vorgeschlagen das als Arduino-Libs
> zu verpacken

Ich will nicht gerade sagen dass ich Arduino liebe. Der code vom core 
ist aber aus meiner Sicht recht gut und durchdacht. Das was da drunter 
die Hardware abstrahiert gefällt mir (z.B. für STM32) auch nicht. Da 
geht's mir wie dir und ich bediene die Register lieber direkt ohne die 
Libs von ST. Selten benutze ich ESPxxx und wenn mit Arduino. Das ist mir 
da lieber als das Expressif SDK direkt. Wenn es in dem Umfeld was nützen 
soll, dann reicht eine absolut abgespeckte Version die nur die 
coroutinen beinhaltet und sich mit dem normalen i2s,spi,uart u.s.w. 
benutzen lässt.

von Cyblord -. (cyblord)


Lesenswert?

Andras H. schrieb:
> Multitasking braucht man nur dann, wenn man
> selber nicht programmieren kann.

Das kann so evt. für sehr kleine Systeme gelten. Allgemein natürlich 
nicht.

Es wäre absurd, sich bei jeder Multitasking Anwendung erst mal selbst 
noch ein Task-System zu programmieren.

Wer aber natürlich meint, er brauche auf einem 8 Bit Controller für ein 
LED Blinken und eine Taster-Abfrage ein fertiges RTOS mit Multitasking, 
der sollte in der Tat an seinen Fähigkeiten arbeiten.

von Veit D. (devil-elec)


Lesenswert?

Michael B. schrieb:
> Johann L. schrieb:
>> Sie verlassen aber ihren Kontext und kehren wieder dahin zurück.  Der
>> Kontext muss also irgendwo gespeichert werden, und zwar nicht auf dem
>> Stack.
>
> Der komplette Stack GEHÖRT zum Kontext, zusätzlich zu den Registern.
>
> Ein irrer Speicherverbrauch, elegant versteckt hinter einfachen
> Funktionen.
>
> Ein Grund, warum aktuelle Software so gross und langsam ist.

Das hat hier nichts mit groß und langsam zu tun. Coroutinen sind nicht 
langsam. Sie sind genauso schnell wie "normale" Funktionsaufrufe. Das 
ist ja genau deren Vorteil.

Mit deinem yield() Vorschlag versteckst du auch nur Funktionsaufrufe. 
Das ist auf keinen Fall besser wie Coroutinen.

Was man nicht aus dem Bick verlieren darf. Die C++ Entwicklung wird 
nicht primär für µC gemacht.

von Jochen W. (jochen_w335)


Lesenswert?

Jürgen schrieb:
> Wenn es in dem Umfeld was nützen
> soll, dann reicht eine absolut abgespeckte Version die nur die
> coroutinen beinhaltet und sich mit dem normalen i2s,spi,uart u.s.w.
> benutzen lässt.

Vielleicht kam es nicht richtig rüber aber die abgespeckte Version gibt 
es schon, das wäre dann nur coco https://github.com/Jochen0x90h/coco
Das enthält das Build-System (CMake + conan), die Coroutinen, die 
Queue-Klasse (IntrusiveMpscQueue.hpp), die Umschaltung der von den 
Herstellern bereitgestellten Header (#include 
<coco/platform/platform.hpp> um z.B. stm32c031xx.h zu erhalten wenn man 
den uC im Build-System ausgewählt hat) und noch HAL-artige 
Hilfsfunktionen und Klassen (die man noch auslagern könnte aber erstmal 
nicht stören da nur Header).

Wer nur die Event-Loop haben will nimmt coco-loop und baut sich alle 
Treiber selber.

von Michael B. (laberkopp)


Lesenswert?

Veit D. schrieb:
> Das hat hier nichts mit groß und langsam zu tun. Coroutinen sind nicht
> langsam. Sie sind genauso schnell wie "normale" Funktionsaufrufe. Das
> ist ja genau deren Vorteil.
> Mit deinem yield() Vorschlag versteckst du auch nur Funktionsaufrufe.
> Das ist auf keinen Fall besser wie Coroutinen.

Doch, weil der Programmierer noch weiss, was für einen Unsinn in der Not 
er treibt.

Bei Coroutinen ist der Unsinn gut versteckt und er glaubt an die beste 
Erfindung seit dem Rad die er möglichst immer überall verwenden sollte.

Die Coroutine ist zwar nicht merklich langsamer als yield, aber 
speicherintensiv für JEDE hängende Routine. Das können schnell einige 
sein, wenn der Programmiere den Programmierstil geil findet.

Bei yield weiss er, dass er das nur in der Not verwenden sollte, da 
hängt das Programm höchstens in einer Routine.

: Bearbeitet durch User
von Veit D. (devil-elec)


Lesenswert?

Hallo,

das mag im Groben und Ganzen fast stimmen. Aber so wie du es sagst, so 
abwertend, so stimmt es einfach nicht.

> Die Coroutine ist zwar nicht merklich langsamer als yield, aber
> speicherintensiv für JEDE hängende Routine.

Coroutinen sind nicht langsamer als yield(). Können sie auch gar nicht 
sein. Weil man in yield() auch nur Funktionen aufruft. Coroutinen sind 
auch nur Funktionsaufrufe. Die Links hatte ich nicht umsonst gezeigt.

Diskutiere sachlich richtig nach besten Wissen und Gewissen.

von Jochen W. (jochen_w335)


Lesenswert?

Michael B. schrieb:
> und sieht gleich sehr gut, wie yield rekursiv aufgerufen wird und der
> ganze stack erhalten bleibt.

Probier mal Dein Beispiel mit receiveserial1() und receiveserial2(), 
also man bearbeitet 2 serielle Schnittstellen parallel. Dann sieht man 
gleich, dass das gar nicht funktioniert weil z.B. receiveserial1() 
yield() aufruft aber wenn von da aus receiveserial2() gestartet wird 
dann bleibt receiveserial1() hängen oder wird rekursiv nochmal 
aufgerufen, also fängt nochmal von vorne an. Das müsste schnell einen 
Stack Overflow geben.

von Michael B. (laberkopp)


Lesenswert?

Jochen W. schrieb:
> Probier mal Dein Beispiel mit receiveserial1() und receiveserial2(),
> also man bearbeitet 2 serielle Schnittstellen parallel. Dann sieht man
> gleich, dass das gar nicht funktioniert

Ja, daher nur ein yield zu einer Zeit.

von Jochen W. (jochen_w335)


Lesenswert?

Michael B. schrieb:
> Ja, daher nur ein yield zu einer Zeit.

Also kooperatives Singletasking ;)

von Hans-Georg L. (h-g-l)


Lesenswert?

Für mich ist das auch Neuland ;-)

Diesen (englischen) Vortrag fand ich gut:

https://www.youtube.com/watch?v=kIPzED3VD3w

Jetzt frage ich mich :
Schleppe ich mir damit automatisch dynamische Speicherverwaltung und 
Exceptions in mein embedded System oder geht es auch irgendwie statisch 
?

Also Coroutinen werden einmalig angelegt und löschen sich nicht selbst,
bleiben im Speicher und können, wenn benötigt, einfach zyklisch wieder 
aktiviert werden ?

von Jochen W. (jochen_w335)


Lesenswert?

Hans-Georg L. schrieb:
> Jetzt frage ich mich :
> Schleppe ich mir damit automatisch dynamische Speicherverwaltung und
> Exceptions in mein embedded System oder geht es auch irgendwie statisch ?

Exceptions sind eine andere Nummer und haben mit Coroutinen nichts zu 
tun. Benutze ich momentan nicht.

> Also Coroutinen werden einmalig angelegt und löschen sich nicht selbst,
> bleiben im Speicher und können, wenn benötigt, einfach zyklisch wieder
> aktiviert werden ?

Ja, wenn sie einmalig auf dem Heap angelegt werden dann bleiben sie da 
und es findet keine weitere dynamische Speicherverwaltung statt solange 
man nicht als Reaktion auf ein Ereignis eine neue Coroutine startet. 
Angeblich kann der Compiler sogar die dynamischen Allokationen unter 
bestimmten Bedingungen wegoptimieren aber das müsste man mit dem 
Compiler Explorer mal genauer untersuchen.

von Hans-Georg L. (h-g-l)


Lesenswert?

wenn ich mir das hier durchlese:
https://en.cppreference.com/w/cpp/language/coroutines

Jochen W. schrieb:
> Hans-Georg L. schrieb:
>> Jetzt frage ich mich :
>> Schleppe ich mir damit automatisch dynamische Speicherverwaltung und
>> Exceptions in mein embedded System oder geht es auch irgendwie statisch ?
>
> Exceptions sind eine andere Nummer und haben mit Coroutinen nichts zu
> tun. Benutze ich momentan nicht.
>
Each coroutine is associated with the promise object, manipulated from 
inside the coroutine. The coroutine submits its result or exception 
through this object. Promise objects are in no way related to 
std::promise.

Deshalb meine Vermutung das ich evtl. Exceptions mit der Lib 
einschleppe.

>> Also Coroutinen werden einmalig angelegt und löschen sich nicht selbst,
>> bleiben im Speicher und können, wenn benötigt, einfach zyklisch wieder
>> aktiviert werden ?
>
> Ja, wenn sie einmalig auf dem Heap angelegt werden dann bleiben sie da
> und es findet keine weitere dynamische Speicherverwaltung statt solange
> man nicht als Reaktion auf ein Ereignis eine neue Coroutine startet.

When a coroutine reaches a suspension point

the return object obtained earlier is returned to the caller/resumer, 
after implicit conversion to the return type of the coroutine, if 
necessary.
When a coroutine reaches the co_return statement, it performs the 
following:

calls promise.return_void() for co_return; co_return expr; where expr 
has type void
or calls promise.return_value(expr) for co_return expr; where expr has 
non-void type
destroys all variables with automatic storage duration in reverse order 
they were created.
calls promise.final_suspend() and co_awaits the result.
Falling off the end of the coroutine is equivalent to co_return;, except 
that the behavior is undefined if no declarations of return_void can be 
found in the scope of Promise. A function with none of the defining 
keywords in its function body is not a coroutine, regardless of its 
return type, and falling off the end results in undefined behavior if 
the return type is not (possibly cv-qualified) void.
When the coroutine state is destroyed either because it terminated via 
co_return or uncaught exception, or because it was destroyed via its 
handle, it does the following:

calls the destructor of the promiseobject.
calls the destructors of the function parameter copies.
calls operator delete to free the memory used by the coroutine state.
transfers execution back to the caller/resumer.

Also darf man nie co_return aufrufen. ?
Wie bekommt man dann aber das Resultat zurück ?

> Angeblich kann der Compiler sogar die dynamischen Allokationen unter
> bestimmten Bedingungen wegoptimieren aber das müsste man mit dem
> Compiler Explorer mal genauer untersuchen.

The call to operator new can be optimized out (even if custom allocator 
is used) if The lifetime of the coroutine state is strictly nested 
within the lifetime of the caller, and the size of coroutine frame is 
known at the call site. In that case, coroutine state is embedded in the 
caller's stack frame (if the caller is an ordinary function) or 
coroutine state (if the caller is a coroutine).

Dann wären die Daten wieder auf dem Stack ?

Vielleicht könnte man den Compiler mit "placement new" überlisten.

Ich will aber nicht weiter herum meckern ;-)
Eine schöne Library von dir Daumen hoch !

von Jochen W. (jochen_w335)


Lesenswert?

Hans-Georg L. schrieb:
> Also darf man nie co_return aufrufen. ?
> Wie bekommt man dann aber das Resultat zurück ?

Ja um new/delete zu vermeiden sollte co_return nie aufgerufen werden, 
also die Coroutine ewig "leben". Coroutinen sind also weniger da um ein 
Resultat zurückzugeben sondern sind Prozesse, die auf etwas warten und 
dann etwas tun und das in einer Endlosschleife.

Beispielsweise habe ich einen virtuellen COM-Port wo die LED für 100ms 
leuchten soll sobald man was sendet. Das mache ich mit einer Coroutine 
wie folgt:
1
Barrier<> sendLedBarrier;
2
3
Coroutine sendLed(Loop &loop) {
4
    while (true) {
5
        co_await sendLedBarrier.untilResumed();
6
        debug::setRed(true);
7
        co_await loop.sleep(100ms);
8
        debug::setRed(false);
9
    }
10
}

Die Coroutine wird gestartet und wartet an der Barrier, das ist wie eine 
Eisenbahnschranke wo man erstmal warten muss. Wenn Daten gesendet werden 
dann dürfen alle, die an der Schranke warten, weiterlaufen (in dem Fall 
wartet nur die eine Coroutine):
1
// receive from usb host and send to RS485 device
2
Coroutine send(Rs485 &rs485, Drivers::UsbEndpoint &usb) {
3
    Drivers::Rs485Buffer rs485Buffer(rs485);
4
    Drivers::UsbBuffer usbBuffer(usb);
5
    while (true) {
6
        co_await rs485.untilReady();
7
        co_await usb.untilReady();
8
        while (rs485.ready() && usb.ready()) {
9
            // receive from usb host (OUT transfer)
10
            co_await usbBuffer.read();
11
12
            // blink send indicator LED
13
            sendLedBarrier.doAll();
14
15
            // send to RS485 device
16
            co_await rs485Buffer.writeData(usbBuffer);
17
        }
18
    }
19
}

Beide Coroutinen beenden sich nie, daher ist das mit dem new/delete auch 
kein Problem.

Um noch einen Double-Buffer zu erzeugen kann man die send Coroutine 
einfach zwei mal starten, dann empfängt immer mindestens einer Daten 
während der andere sendet. Genauso mit receive.

von Richard W. (richardw)


Lesenswert?

Ich finde es löblich dass du dich für modernes C++ auf kleinen 
Mikrocontrollern entschieden hast.

Coroutinen fand ich immer akademisch interessant, in der Praxis finde 
ich aber Multithreading auf einem Mikrocontroller schwer zu debuggen. 
Ein Kollege der einen Fetisch für Coroutinen hat, hat diese ähnlich wie 
du implementiert, auch mit C++ und wir haben das ein Weilchen in einem 
Projekt benutzt. Das war auch wirklich schön und wie aus dem Lehrbuch, 
wurde aber von den anderen Kollegen rundweg abgelehnt und schließlich 
durch eine primitive Superloop ersetzt. Schade.

Ich persönlich mag ereignisgetriebene Single Stack Systeme in 
Kombination mit Zustandsautomaten am liebsten und verwende die in der 
Praxis am häufigsten.

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.