Forum: Mikrocontroller und Digitale Elektronik Überlegungen zu einem LED-Controller mit FreeRTOS und ESP32 (Lernprojekt)


von Olli Z. (z80freak)


Lesenswert?

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?

von Alexander (alecxs)


Lesenswert?

vTaskDelete(NULL); kommt in die Taskfunktion, und dazu ein Sperrflag 
dass Du abfragst bevor Du einen neuen Task erzeugst.

von Olli Z. (z80freak)


Lesenswert?

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.

von Alexander (alecxs)


Lesenswert?

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.

von Luca E. (derlucae98)


Lesenswert?

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.

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

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.

von Olli Z. (z80freak)


Lesenswert?

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
von Olli Z. (z80freak)


Lesenswert?

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
von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

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

von Olli Z. (z80freak)


Lesenswert?

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.

von Olli Z. (z80freak)


Lesenswert?

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
von Olli Z. (z80freak)


Lesenswert?

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
}

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

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.

von Olli Z. (z80freak)


Lesenswert?

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
von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

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?

von Olli Z. (z80freak)


Lesenswert?

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.

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

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"

von Olli Z. (z80freak)


Lesenswert?

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
von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

Olli Z. schrieb:
> Dann sollte das Timing einigermaßen stabil sein, richtig?

Ja genau!

von Alexander (alecxs)


Lesenswert?

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
von Olli Z. (z80freak)


Lesenswert?

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
Noch kein Account? Hier anmelden.