Ich beschäftige mich gerade etwas mit ESP32 und FreeRTOS und habe mir dazu ein Lernprojekt ausgedacht. Dazu möchte ich einen LED-Controller programmieren, der die Leuchtstärke einer LED (ich nehme zur Vereinfachung die Onboard eines Super-Mini Boards welche auf GPIO_NUM_13 liegt) per PWM ändern kann.
Dabei möchte ich verschiedene Lichtmuster erzeugen können:
ON/OFF/DIM
BLINK
FADE IN/OUT
PULSE
Um mehr mit dem Multitasking zu machen würde ich den Controller so umsetzen wollen das dieser im Hintergrund läuft und durch API-Befehle im Main-Code gesteuert wird. Dabei möchte ich ein Lichtmuster einstellen welches dann "im Hintergrund" läuft bis es fertig ist, oder eben durch ein anderes Muster ersetzt wird. Zudem möchte ich für bestimmte Effekte (z.B. Fade IN/OUT) erkennen können wann das Muster abgespielt ist um in meinem Main-Code darauf warten zu können. Also async/sync Muster.
Grundsätzlich wäre die LED ja die Ressource die es zu "schützen" gilt, sprich es darf immer nur ein Task darauf zugreifen.
Welchen Ansatz würdet ihr da wählen? Ich habe mir gedacht entweder Tasks zu erzeugen die sich nach durchlauf des Musters selbst beenden oder durch den Start eines neuen Musters beendet werden. Oder einen Daemon der über Queues und Semaphoren gesteuert wird.
Ein Task der intern keine Endlosschleife ausführt scheint immer noch ein gewisses Risiko zu sein, wenn dieser vom Task-Erzeuger nicht mit vTaskDelete() über das Handle sauber gelöscht wird? Das würde ja eher für eine Daemon-Variante sprechen?
Das heißt der Task killt sich selbst? Kann das gute gehen auf Dauer?... Vielleicht für Tasks die nur selten mal was tun und das ohne weitere Komminikation von aussen? Wenn ich aber alle paar Sekunden im Task was anderes tun will?
Ich fahre aktuell den Ansatz das ich im Task permanent iteriere (endlosschleife) mit einem kleinen vTaskDelay(10) damit mich der Watchdog nicht killt und mit xQueueReceive(..., 0) auf neue Messages prüfe und die ggf. übernehme und die aktuelle Aktion abbreche. Im Task ersetzte ich dann vTaskDelay(ticks) durch xQueueReceive(..., ticks) damit der Task an der Stelle blockiert aber jederzeit weitermacht wenn eine neue Anweisung eingeht.
Das macht den code sehr dynamisch aber auch complex.
Das ist die saubere Variante für Einmal-Tasks, wenn der Task nicht unterbrochen werden soll. Du könntest für jedes Programm einen eigenen Task starten. Wäre IMO einfacher.
Auf Variablen eines Tasks zugreifen erfordert atomaren Zugriff, das mūsste man dann ggf. mit portENTER_CRITICAL() / portEXIT_CRITICAL() sicherstellen.
vTaskDelay(10) damit mich der Watchdog nicht killt und mit
xQueueReceive(..., 0) auf neue Messages prüfe
Du musst die Queue nicht pollen.
Du kannst auch einfach mit xQueueReceive(..., portMAX_DELAY) den Task so lange blockieren, bis etwas in der Queue ist. Dann verschwendest du nicht sinnlos CPU Zeit.
Ich würde wahrscheinlich einen Task erstellen, der sich um die LED-Ansteuerung kümmert und mittels Queue das Muster vorgegeben bekommt.
Sinnvollerweise befindet sich diese Funktionalität in einem eigenen Modul, sodass man die Sonderfunktionen wie "Aktuelles Muster stoppen" einfach in Form einer C-Funktion realisiert, die dann Flags setzt auf die der Task zugreift.
Du musst die Queue nicht pollen.
Du kannst auch einfach mit xQueueReceive(..., portMAX_DELAY) den Task so
lange blockieren, bis etwas in der Queue ist. Dann verschwendest du
nicht sinnlos CPU Zeit.
Richtig, aber ich will ja im Task keine Subtasks starten oder einfach nur auf ein Ereignis warten, es dann stur ausführen und dann erst wieder bereit für das nächste sein, sondern auf die Anforderungsänderung sofort reagieren können. Um beim Beispiel der LED zu bleiben: Ich starte einen Blinker der die LED für jeweils 0,5s abwechselnd an und aus macht. Egal ob ich nun einen Zähler mitliefere der sagt wie lange oder wie oft das geschehen soll, oder er einfach endlos läuft, ich möchte jederzeit eine andere Anforderung stellen können. Das bedeutet das der Controller-Task nachdem er die LED eingeschaltet hat ja 0,5s "warten" muss bis zum nächsten Zyklus. Diese Wartezeit mache ich nicht mit vTaskDelay(pdMS_TO_TICKS(500)) sondern eben mit xQueueReceive(..., pdMS_TO_TICKS(500)). Das hat den Vorteil das wenn während der Wartezeit eine andere Anforderung gestellt wird, wird die Pause sofort unterbrochen und ich kann aus der aktuellen Funktion aussteigen, z.B. so:
ESP_LOGI(TAG,"Switch LED mode from %d to %d",ledConfig.mode,newLedConfig.mode);
20
ledConfig=newLedConfig;
21
break;
22
}
23
break;
Ich muss natürlich auch außerhalb einer Ausführung, wenn also gerade nichts gemacht wird, wie beim LED-ON oder LED-OFF, auch die Queue abfragen. Da habe ich zusätzlich 10ms eingebaut damit der Watchdog nicht meckert und der Task genügend CPU-Zeit für andere Dinge yielded.
Das alles macht den Code natürlich komplexer, aber dafür sehr asynchron. Die Queue habe ich nur mit einer Tiefe von 1 (Message) Initialisiert und nun mache ich dafür ein xQueueOverwrite() und ersetze somit den Inhalt der Queue durch einen anderen.
Ich würde wahrscheinlich einen Task erstellen, der sich um die
LED-Ansteuerung kümmert und mittels Queue das Muster vorgegeben bekommt.
Sinnvollerweise befindet sich diese Funktionalität in einem eigenen
Modul, sodass man die Sonderfunktionen wie "Aktuelles Muster stoppen"
einfach in Form einer C-Funktion realisiert, die dann Flags setzt auf
die der Task zugreift.
Genau so habe ich es gerade implementiert. Einen anderen Ansatz gäbe es noch das ganze zeitgesteuert zu machen, d.h. die Loop iteriert ständig und erst wenn es "an der Zeit" ist etwas zu ändern reagiert der switch/case. Damit ließe sich wohl ein präziseres Timing erreichen und womöglich wäre das einfacher?
Der Vollständigkeit halber, und als Suchbegriff: Dieses Paradigma nennt
sich Aktor-Modell. Da gibt es auch Libraries und ganze
Meinst Du? Ich denke das Actor-Model arbeitet genau anders als ich es wünsche, weil es eben eine Anforderung nach der anderen durchführt, ohne das von außen Einfluss genommen wird. Im Vergleich wäre das Actor-Model der Beamte und mein Model die Hausfrau ;-)
Diese Wartezeit mache ich nicht mit vTaskDelay(pdMS_TO_TICKS(500))
sondern eben mit xQueueReceive(..., pdMS_TO_TICKS(500)).
Die Tücke ist: Wenn der Task auf etwas anderes langsames (z.B. UART) warten soll aber dennoch auf die Queue sofort reagieren soll, was dann? Die Empfangsfunktion kann man ja schlecht durch xQueueReceive ersetzen.
Beim Actor-Model wird eben eine Anforderung nach der anderen
durchgeführt
Ich hatte es so verstanden, dass beim Aktor-Modell ein Aktor auch zwischendurch neue Anforderungen bekommen und bearbeiten kann und nicht zwischenzeitlich "blockiert". Dann wäre es ja nur ein simpler Batch-Thread oder "Future".
Die Tücke ist: Wenn der Task auf etwas anderes langsames (z.B. UART)
warten soll aber dennoch auf die Queue sofort reagieren soll, was dann?
Ja, in der Tat... das wäre ein Problem. Auch mein getimter Ansatz setzt voraus das eine Iteration sehr schnell passiert, idealerweise <= 1 Tick. Das ist ähnlich dem Problem bei ISRs.
Beim Actor-Model wird eben eine Anforderung nach der anderen
durchgeführt
Ich hatte es so verstanden, dass beim Aktor-Modell ein Aktor auch
zwischendurch neue Anforderungen bekommen und bearbeiten kann und nicht
zwischenzeitlich "blockiert". Dann wäre es ja nur ein simpler
Batch-Thread oder "Future".
Hm, vielleicht hatte ich es nicht richtig verstanden und nochmal eingelesen. Mein aktueller Code zeigt alle Anzeichen eines Actor-Models, RTOS+Queues ist genau dafür konzipiert. Der Trick bei dem Model ist wohl das es eben niemals DIREKT von außen beinflusst wird, sondern das nur über die Queue kommuniziert wird. So gesehen ist es eher eine "State-Machine on Steroids" ;-)
Also meine beiden Ansätze funktionieren und scheinen, trotz Unkenntnis der Design-Pattern und möglicher Libs, genau diesem Pattern zu entsprechen. Und scheinbar ist der Ansatz einzelne Tasks zu erzeugen und diese dann immer wieder zu managen (killen) nicht die bessere Wahl.
. Auch mein getimter Ansatz setzt voraus das eine Iteration sehr schnell
passiert, idealerweise <= 1 Tick
Ist halt auch recht ineffizient wenn die Tasks ständig "rotieren". Idealerweise sollte ein Task immer nur genau dann aufwachen wenn es wirklich was zu tun gibt und sonst ununterbrochen schlafen.
In allen Punkten volle Zustimmung. Dann sollte ich nur solange durch die loop laufen bis das aktuelle Pattern durchgespielt ist und dann auf einen portMAX_DELAY laufen und warten bis es wieder was zu tun gibt.
Bei einem LED-ON oder LED-OFF wäre das ja sofort der Fall. Bei den anderen Effekten nur dann, wenn ich diesen eine maximale Ausführungszeit oder Intervallzahl mitgebe und diese überschritten wird.
Dann hätte ich quasi "best of breed"?
Mein aktueller Ansatz ist ja der eines periodischen iterierens, ganz vereinfacht so ausgedrückt:
1
while(1){
2
xQueueReceive(ledQueue,&msg,0);
3
update_led_effect();
4
xQueueReceive(ledQueue,&msg,pdMS_TO_TICKS(10));// yield time for watchdog
5
}
Das ist ja im Grunde "Polling". Es verschwendet Ressourcen wenn nichts zu tun ist.
Besser wäre wohl abhängig von der letzten Aktion zu warten:
1
TickType_twaitTicks=portMAX_DELAY;
2
while(1){
3
xQueueReceive(ledQueue,&msg,waitTicks);
4
waitTicks=update_led_effect();
5
}
Also wenn ich LED-ON/OFF hatte oder mein Effekt fertig ist dann verwende "portMAX_DELAY" um auf das nächste Event zu warten. Ist man noch in einem Effekt, kann hier z.B. "10" stehen wenn das die Zeit bis zum nächsten errechneten Iterationsschrit sein soll.
Wenn ich nextToggle nicht basierend auf der aktuellen Zeit neu berechne, sondern die Ursprungsbasis nehme. Nur beim Moduswechsel, wenn ich also neu starte, sollte ich wieder mit now + period neu berechnen.
Dann sollte das Timing einigermaßen stabil sein, richtig?