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?
vTaskDelete(NULL); kommt in die Taskfunktion, und dazu ein Sperrflag dass Du abfragst bevor Du einen neuen Task erzeugst.
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.
Olli Z. schrieb: > 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.
Olli Z. schrieb: > und mit xQueueReceive(..., 0) auf neue Messages prüfe und die ggf. > übernehme und die aktuelle Aktion abbreche Der Vollständigkeit halber, und als Suchbegriff: Dieses Paradigma nennt sich Aktor-Modell. Da gibt es auch Libraries und ganze Programmiersprache für.
Luca E. schrieb: > 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:
1 | while (1) |
2 | {
|
3 | if (xQueueReceive(ledQueue, &newLedConfig, pdMS_TO_TICKS(10)) == pdPASS) { |
4 | ESP_LOGI(TAG, "Switch LED mode from %d to %d", ledConfig.mode, newLedConfig.mode); |
5 | ledConfig = newLedConfig; |
6 | }
|
7 | |
8 | switch (ledConfig.mode) { |
9 | ...
|
10 | case LED_MODE_BLINK: |
11 | LedSetDuty(ledConfig.duty1); // schalte LED ein |
12 | if (xQueueReceive(ledQueue, &newLedConfig, pdMS_TO_TICKS(ledConfig.period1_ms)) == pdPASS) { |
13 | ESP_LOGI(TAG, "Switch LED mode from %d to %d", ledConfig.mode, newLedConfig.mode); |
14 | ledConfig = newLedConfig; |
15 | break; |
16 | }
|
17 | LedSetDuty(ledConfig.duty2); // schalte LED aus |
18 | if (xQueueReceive(ledQueue, &newLedConfig, pdMS_TO_TICKS(ledConfig.period2_ms)) == pdPASS) { |
19 | 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?
:
Bearbeitet durch User
Niklas G. schrieb: > 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 ;-)
:
Bearbeitet durch User
Olli Z. schrieb: > 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. Olli Z. schrieb: > Da habe ich zusätzlich 10ms eingebaut damit der Watchdog nicht meckert Sollte eigentlich nicht nötig sein, xQueueReceive yielded ja auch. Der Task darf da prinzipiell beliebig lange drin festhängen Olli Z. schrieb: > 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".
Niklas G. schrieb: > 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.
Niklas G. schrieb: > Olli Z. schrieb: >> 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.
:
Bearbeitet durch User
Nochmal konkret, mein Ansatz 1: LED Controller Task mit Queue-Polls:
1 | static void led_controller_task(void *pvParameters) |
2 | {
|
3 | ESP_LOGI(TAG, "Controller task started"); |
4 | |
5 | LedConfig_t ledConfig = { .mode = LED_MODE_OFF }; |
6 | LedConfig_t newLedConfig; |
7 | |
8 | while (1) |
9 | {
|
10 | if (xQueueReceive(ledQueue, &newLedConfig, pdMS_TO_TICKS(10)) == pdPASS) { |
11 | ESP_LOGI(TAG, "Switch LED mode from %d to %d", ledConfig.mode, newLedConfig.mode); |
12 | ledConfig = newLedConfig; |
13 | }
|
14 | |
15 | switch (ledConfig.mode) { |
16 | case LED_MODE_OFF: |
17 | LedSetDuty(0); |
18 | break; |
19 | |
20 | case LED_MODE_ON: |
21 | if ( ! ledConfig.duty1) { |
22 | ledConfig.duty1 = 255; |
23 | }
|
24 | LedSetDuty(ledConfig.duty1); |
25 | break; |
26 | |
27 | case LED_MODE_BLINK: |
28 | if ( ! ledConfig.duty2) { |
29 | ledConfig.duty2 = 0; |
30 | }
|
31 | if ( ! ledConfig.period2_ms) { |
32 | ledConfig.period2_ms = ledConfig.period1_ms; |
33 | }
|
34 | LedSetDuty(ledConfig.duty1); |
35 | if (xQueueReceive(ledQueue, &newLedConfig, pdMS_TO_TICKS(ledConfig.period1_ms)) == pdPASS) { |
36 | ESP_LOGI(TAG, "Switch LED mode from %d to %d", ledConfig.mode, newLedConfig.mode); |
37 | ledConfig = newLedConfig; |
38 | break; |
39 | }
|
40 | LedSetDuty(ledConfig.duty2); |
41 | if (xQueueReceive(ledQueue, &newLedConfig, pdMS_TO_TICKS(ledConfig.period2_ms)) == pdPASS) { |
42 | ESP_LOGI(TAG, "Switch LED mode from %d to %d", ledConfig.mode, newLedConfig.mode); |
43 | ledConfig = newLedConfig; |
44 | break; |
45 | }
|
46 | break; |
47 | |
48 | case LED_MODE_FLASH: |
49 | break; |
50 | |
51 | case LED_MODE_FADE: |
52 | uint8_t step_size = 5; |
53 | int step_pause = (ledConfig.period1_ms / (ledConfig.duty2 - ledConfig.duty1)) * step_size; |
54 | ESP_LOGI(TAG, "duty1 %d, duty2 %d, step_pause %d", ledConfig.duty1, ledConfig.duty2, step_pause); |
55 | for (int i = ledConfig.duty1; i <= ledConfig.duty2; i += step_size) |
56 | {
|
57 | LedSetDuty(i); |
58 | if (xQueueReceive(ledQueue, &newLedConfig, pdMS_TO_TICKS(step_pause)) == pdPASS) { |
59 | ESP_LOGI(TAG, "Switch LED mode from %d to %d", ledConfig.mode, newLedConfig.mode); |
60 | ledConfig = newLedConfig; |
61 | ESP_LOGI(TAG, "Fade aborted"); |
62 | break; |
63 | }
|
64 | }
|
65 | ESP_LOGI(TAG, "fade done"); |
66 | break; |
67 | |
68 | case LED_MODE_PULSE: |
69 | break; |
70 | |
71 | }
|
72 | |
73 | vTaskDelay(pdMS_TO_TICKS(10)); |
74 | }
|
75 | }
|
Olli Z. schrieb: > . 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. Olli Z. schrieb: > RTOS+Queues ist genau dafür konzipiert. Ja, es gibt auch RTOS die genau dafür konzipiert sind, z.B. PXROS. Olli Z. schrieb: > So gesehen ist es eher eine "State-Machine on Steroids" ;-) In der Tat, aber die State Machine bekommt hier eben eigene Rechenzeit auch außerhalb der Zustandswechsel, über den Thread eben.
Niklas G. schrieb: 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.
:
Bearbeitet durch User
Olli Z. schrieb: > Dann hätte ich quasi "best of breed"? Ja so macht man das. Olli Z. schrieb: > Das ist ja im Grunde "Polling Kannst du nicht einfach die 10 durch portMAX_DELAY ersetzen?
Besser wäre wohl abhängig von der letzten Aktion zu warten:
1 | TickType_t waitTicks = 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.
Olli Z. schrieb: > Ist man noch in einem Effekt, kann hier z.B. "10" stehen wenn das die > Zeit bis zum nächsten errechneten Iterationsschrit sein soll. Ja stimmt so geht es. Besser aber die aktuelle Systemzeit abfragen und die verbleibende Zeit errechnen, sonst kann das Blinkmuster "weglaufen"
Also z.B. so exemplarisch für einen BLINK Prozess:
1 | TickType_t nextToggle = startTick + pdMS_TO_TICKS(500); |
2 | |
3 | while (1) { |
4 | TickType_t now = xTaskGetTickCount(); |
5 | if (now >= nextToggle) { |
6 | LedSetDuty(...); // toggle |
7 | nextToggle += pdMS_TO_TICKS(ledConfig.period_ms); |
8 | }
|
9 | |
10 | TickType_t wait = nextToggle - now; |
11 | xQueueReceive(ledQueue, &newCfg, wait); |
12 | }
|
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?
:
Bearbeitet durch User
Kleine Randnotiz für's Debuggen, auch hier gilt ledConfig.period_ms < 10 = 0 wegen Integerdivision (ggf. abfangen) https://github.com/espressif/esp-idf/blob/master/components/freertos/FreeRTOS-Kernel/include/freertos/projdefs.h#L46 Beitrag "Re: ESp32 Multithreading"
:
Bearbeitet durch User
Alexander schrieb: > Kleine Randnotiz für's Debuggen, auch hier gilt ledConfig.period_ms < 10 > = 0 Ja, Danke, das kannte ich bereits, da bin ich schonmal drüber gestoplert, hatte ich schon wieder fast vergessen! Die kleinste Tick-Einheit ist 10.
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.