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
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 | }
|
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?)
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
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).
Hallo, auch wenn ich keinen STM habe, ich finde es sehr gut. Respekt.
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 ;-)
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.
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.
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 | }
|
Motopick schrieb: > Woanders macht Mann das so: Da hatte Mann aber auch noch keine Geräte, die mit Batterien betrieben wurden...
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.
> 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.
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.
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
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!
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.
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.
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
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
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.
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.
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
Hallo, bestimmt interessant... https://www.grimm-jaud.de/index.php/blog/coroutinen https://www.heise.de/blog/C-20-Coroutinen-mit-cppcoro-4705161.html
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.
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.
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.
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 :)
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
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).
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.
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
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.
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.
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.
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.
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
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.
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.
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.
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 ?
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.
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 !
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.
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
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.