FreeRTOS (ESP 32)

Hi,

ich setz mich grad mit dem Thema FreeRTOS auseinander und hab ein paar Verständnisfragen.

Folgendes Beispiel-Szenario (rein fiktiv):

// Dieser Task soll alle x Sekunden prüfen, ob ein WiFi-Verbindung besteht und falls nein, diese neu aufbauen 
xTaskCreatePinnedToCore(
    keepWiFiAlive,
    "Keep Wifi Alive",
    5000,
    NULL,
    2,
    NULL,
    CONFIG_ARDUINO_RUNNING_CORE
  ); 

void keepWifiAlive {
  for (;;) {
      if (WiFi.status() == WL_CONNECTED) {
        vTaskDelay(10000 / portTICK_PERIOD_MS);
        continue;
      }
      WiFi.begin(ssid, password);
      unsigned long startAttemptTime=millis();
      while (WiFi.status() != WL_CONNECTED && millis() - startAttemptTime < WIFI_TIMEOUT_MS) {
        
      }
      if (WiFi.status() != WL_CONNECTED) {
        vTaskDelay(300000 / portTICK_PERIOD_MS);
        continue;
      }
    } else {
      vTaskDelay(10000 / portTICK_PERIOD_MS);
      continue;
    }
  }
}

// Dieser hier soll alle x Sekunden prüfen, ob der ESP mit dem MQTT-Server verbunden ist, und falls nein, die Verbindung herstellen
xTaskCreatePinnedToCore(
    keepMQTTAlive,
    "Keep MQTTAlive",
    5000,
    NULL,
    2,
    NULL,
    CONFIG_ARDUINO_RUNNING_CORE
  ); 

void keepMQTTAlive(void * parameters) {
  for (;;) {
    // usw.
  }
}

Wie geht der ESP damit um, wenn zwei Tasks sich zeitlich überschneiden?
Regelt er das über die Prio, oder ? Oder wäre es besser, z.B. aus den oberen zwei Tasks einen zu machen, der zuerst prüft, ob die WiFi-Verbindung steht und falls ja danach dann direkt ob die MQTT-Verbindung besteht?

Oben habe ich xTaskCreatePinnedToCore() verwendet, weil ich das aus einem Beispiel so übernommen habe. Wäre es grundsätzlich besser, xTaskCreate() zu verwenden und somit dem ESP selber entscheiden zu lassen, welchen Core er nimmt? Denn woher weiß ich denn, welcher Core die bessere Wahl ist? Und man kann als Parameter statt dem Core ja auch tskNO_AFFINITY übergeben --> Ist das dann das gleiche Resultat wie xTaskCreate()?

Wie kann ich "berechnen", wie groß die Stacksize sein soll?
Von was macht man das Abhängig? z.B. im oberen "keepWiFiAlive" - Task? Wie kommt der Autor dieser Funktion (hab ich aus einem Tutorial) gerade auf die 5000? Warum nicht 4000 oder 6000?

Was passiert, wenn ich hier einen zu großen Wert nehme?

Viele Grüße und n guten Start in die Woche!

Du kannst davon ausgehen, dass sich die Tasks überschneiden. Die Tasks dürfen natürlich nicht voneinander abhängen. Sollten sie das trotzdem tun, musst Du die Ausführung mittel entsprechender Mittel (Mutex oder andere Semaphoren) gegeneinander schützen.
In Deinem Spezialfall würde ich die beiden Tasks zu einem zusammenführen.

Soweit ich weiss, verwendet der Arduino Core von ESP einen Core für die internen Aufgaben (Netzwerk, USB, etc.) und den anderen exklusiv für den Arduino-Code. Somit würde ich Tasks immer so erzeugen, wenn Du darin Arduino-Code (bzw. entsprechende Bibliotheken) ausführen willst. Und ja, tskNO_AFFINITY entspricht dem Aufruf von xTaskCreate() in diesem Zusammenhang.

Der Wert wird so gross gewählt, dass alle Stack-Variablen des Codes darin Platz finden. Alle lokalen Variablen werden auf dem Stack alloziert, aber auch Aufrufparameter und Rücksprungadressen werden dort abgelegt.
Wenn der Wert zu gross ist, belegt Dein Task einfach mehr RAM als notwendig wäre. Solange Du genug RAM für die ganze Anwendung hast, sollte das kein Problem sein.

Hi,

vielen Dank für die umfangreiche Antwort :blush:

Meinst du mit "so erzeugen" taskCreate() oder taskCreatePinnedToCore().

Und wann ist es "Arduino Code" und wann nicht? Woran erkenn ich das? In meinem Fall hab ich drei Tasks. Einen der überwachen soll, ob eine WiFi Verbindung besteht, einen ob einen Verbindung zum MQTT Server besteht und noch einen, der alle x Sekunden prüft, ob eine Variable Namens "Alarm" den Wert true hat und falls ja einen Buzzer ertönen lässt.

Welche dieser Tasks ist Arduino Code?
Welchen würdest du auf welchem Kern laufen lassen oder einfach alle mit taskCreate() ohne pinnedToCore?

Grüßle
Daniel

Wenn es sich überhaupt nicht um Tasks kümmert :slight_smile:

Letzteres.

Wenn es Arduino Bibliotheken verwendet. Wenn der Code sich auf die ESP Entwicklungsumgebung beschränkt, kannst Du frei über die Cores verfügen (soweit ich das verstanden habe).

Wenn es in der Arduino IDE nicht mehr kompiliert (start vereinfacht ausgedrückt).

Bei dieser Aufgabenstellung würde ich überhaupt keine Tasks verwenden. Damit handelst Du Dir nur unnötig Probleme ein. Das lässt sich problemlos in einem normalen loop() ausführen.

Da Du wahrscheinlich Arduino Bibliotheken einsetzen wirst (die sind meist viel benutzerfreundlicher als die Alternativen, für MQTT ist mir keine Bibliothek für die ESP-Umgebung bekannt), dürften es alle sein.

Wenn Du sie als Tasks laufen lassen willst/musst, dann würde ich sie über den obigen Aufruf nur an den Arduino-Core knüpfen.

1 Like

Hi,

Dankeschön, jetzt bin ich wieder ein bisschen schlauer. Die FreeRTOS Sache ist für mich einfach noch Neuland aber gerade für die Überwachung der WiFi-Verbindung denke ich, ist sie vorteilhaft. So kann ich alles hauptsächliche im Loop lassen und die WiFi Geschichte parallel laufen lassen.

Der ESP8266 hat sich automatisch wieder verbunden, falls erforderlich. Der ESP32 leider nicht.

Im Thema duty cycle messen eines PWM-Signals bei 20kHz habe ich eingebettet in ein Arduino-Programm FreeRTOS verwendet.

Kannst ja mal schauen, ob Dir das irgendwie hilft.

Programme mit main() und for (;;) habe ich in setup()´ und loop()´ geändert und mit ein paar kleinen weiteren Anpassungen in der Arduino-IDE zur Funktion gebracht.

1 Like

Würde ich nicht empfehlen. Die Nebenläufigkeit ist eine schwierig zu beherrschende Sache. Wenn Du das in einen Task verschiebst, darfst Du im loop() auf keine Netzwerk-Sachen zugreifen. Ich denke, das dürfte Dich zu stark einschränken. Lass das mit den Tasks lieber sein und mache alles explizit im loop().

Hi,

in dem Fall bleib ich bei den Tasks.
Im Loop hab ich sowieso nichts mehr mit WiFi zu tun. Und für sowas wie die Wifi Verbindung aufrecht zu erhalten finde ich Tasks ideal.
Denn jeder neue Verbindungsversuch würde meinen Loop ja für eine gewisse Zeit blockieren. Und wenn ich schon einen ESP32 verwende, der Multitasking kann, will ich das auch nutzen :wink:

Wird schon schiefgehen.

Grüßle und schönen Abend allen

Ich hoffe, dass dir klar ist, dass loop eine eigene FreeRTOS Task ist, mit einem arg eingeschränktem Stack Bereich.
Es ist also nicht unbedingt angesagt das zu überfluten.

Lesestoff:

Hi Combie,

im bin immer noch im Lernmodus, d.h. 100% klar ist mir noch nichts :wink:

Mir ist klar, dass auch der loop() nur ein Task ist.
Aber nach meinem Verständnis kann ich dank FreeRTOS parallel zum Loop noch weitere tasks laufen lassen, oder?

Wenn wir nun mal folgendes Beispiel nehmen:

Ich möchte folgendes im Loop machen:

  1. Prüfen ob der ESP32 mit der Station (also z.B. meinem Router verbunden ist)
  2. Falls nein für 5 Sekunden versuchen, die Verbindung herzustellen (mit einer While-Schleife)
  3. Die aktuelle Raumtemperatur (oder was auch immer) ermitteln
  4. noch 5 andere Sachen, z.B: Alarm schlagen, falls die Temperatur einen gewissen Wert überschreitet einen Buzzer ertönen lassen
  5. usw.

Nun würde doch - wenn ich Punkt 1. und 2. im Loop-Task lassen - der ganze Loop wegen der While-Schleife des Verbindungsaufbaus ins Stocken geraten, oder?

Denn den neuen Verbindungsversuuch würde ich ja so machen:

unsigned long pause = 10000;
  connTimer = millis();
  while (WiFi.status() != WL_CONNECTED && (millis() - connTimer < pause)) {
    Serial.print(".");
    delay(500);
  }

Darum dachte ich eben, ich lagere den neuen Verbindungsaufbau (also 1. und 2.) auf einen parallelen Task aus, damit das dann im Hintergrund abläuft und meinen Loop-Task nicht "stört".

Darauf gebracht hat mich dieses Video hier:

Aber ich bin euch dankbar, wenn ihr mich eines besseren belehrt und man das anders machen sollte.

Was meinst du mit überfluten?
Wie groß ist denn der Stack-Bereich des Loop-Tasks (ich nenn den jetzt einfach mal so, also void loop() mein ich in dem Fall?

Und wie könnte ich den versehentlich überfluten?

Ich frag deshalb da ich in Erinnerung hatte, dass ich bei irgendeinem Projekt schon mal das Problem hatte, dass der ESP immer mal wieder (nach ein paar Tagen) nicht mehr erreichbar war und ich ihn dann neu starten musste.... evtl. hing das ja damit zusammen.

Wär super, wenn du da noch einen Dummy-Anfänger gerechten Tipp hättest.

Grüßle

**Edit: **
Kann man sich irgendwie am Serial Monitor die aktuelle Stack Size-Ausschöpfung anzeigen lassen, also z.B. langsam zusehen, wie sie ansteigt?

Im arduino-esp32 Core gibt es ein paar Servicecalls dafür:

    uint32_t getHeapSize(); //total heap size
    uint32_t getFreeHeap(); //available heap
    uint32_t getMinFreeHeap(); //lowest level of free heap since boot
    uint32_t getMaxAllocHeap(); //largest block of heap that can be allocated at once

Alle gehören zum global deklarierten ESP-Objekt, werden also z.B. mit uint32_t heapsize = ESP.getFreeHeap(); aufgerufen.
Das ist allerdings der Heap, nicht das Stack. Das darf sich von der Gegenrichtung im freien Speicher ausdehnen; Wenn der freie Heap schrumpft, kann das auch an Stackerweiterungen liegen.
Beim ESP32 ist das wegen des relativ üppigen Speichers aber selten ein Problem - es sei denn, Du programmierst Rekursionen oder hast einen Speicherfresser eingebaut.

Und: keine Angst vor den tasks - ist ungewohnt für Arduinoprogrammierer, läuft aber sehr zuverlässig und bequem. Meine 0,02€...

Hi,

Danke für deine Antwort @Miq1

Ein paar Gegenfagen dazu:

Die dabei ausgegebenen Werte beziehen sich immer auf den gesamten Sketch, oder? ALso die Summe aller Tasks.

Oder kann man sich auch für jeden Task einzeln ausgeben lassen, wie es sich mit der freien Heap-Size verhält?

Und:
Heißt das quasi, solange die freie Heap Größe (also was mir getFreeHeap() ausgibt) über Null ist, ist alles ok? Oder ab wann gibts "Probleme" und wie könnten die aussehen?

Auf jeden Fall hast du mir schon ein ganzes Stück weitergeholfen.

LG

Der Heap ist für alle Tasks, richtig. Der Scheduler kann ja nicht hellsehen, deswegen muss man selbst angeben, wieviel die Task maximal brauchen darf - oder man lässt es, dann hat die Task theoretisch den kompletten Heap zur Verfügung.

Probleme äußern sich in der Regel durch Reboots, weil eine Speicheranforderung nicht erfüllt werden konnte. Wie geschrieben musst du da normalerweise aber einiges anstellen, bis das passiert.

Wenn eine Task beendet wird, wird beim xTaskDelete auch aller automatisch allokierte Speicher wieder dem Heap hinzugeschlagen. Eigene Allokierungen mit malloc oder new müssen aber explizit freigegeben werden.

OK, dann lass ich mir bei den nächsten Projekten mal interessehalber die Heap-Size ausgeben.
Ich bin gespannt :wink:

Eine Frage doch noch:

Was wäre denn ein typischer Speicherfresser?
Hast du da ein paar Beispiele?

Denn wenn ich z.B. einen char-array habe und diesen im Loop immer wieder mit einem anderen Wert befülle, "wächst ja nichts an" oder? Sprich: Was bringt die Stack Size zum überlaufen und wie kann man es vermeiden?

Achte aber bei mehreren Tasks auf die gemeinsame Nutzung von Variablen, wie das oben schon empfohlen wurde. Da du nie weißt, wann welche Task Zeit zugeteilt wird und bei zwei Cores auch zwei Tasks gleichzeitig laufen können, musst du gemeinsam genutzten Speicher mit Mutexen schützen, sonst sind die Ergebnisse nicht vorhersehbar.

Speicherfresser entstehen in aller Regel nur durch Programmierfehler - unendliche Rekursionen, immer wieder ausgeführte mallocs oder news etc.

1 Like

Heisst das, im loop() wird weder MQTT noch sonst eine Netwerk-Funktion verwendet? Wieso soll dann ein Task diese Verbindung aufrechterhalten?

Das ist genau, was ich versuchte. Du solltest das nicht machen. Ansonsten musst Du mittels Mutex (o.ä.) sicherstellen, dass sich die zwei Tasks nicht in die Quere kommen.

Nicht einfach so. Du müsstest das in Deine Programm einbinden. Und das solltest Du nicht für mehrere Tasks machen, sonst schreiben die sich unter Umständen in die Meldungen rein, es kommt also möglicherweise alles durcheinander.

Hi,

dann hatte ich das falsch verstanden:

Im separaten MQtt-Task soll geprüft werden, ob die MQTT-Verbindung noch steht und falls nein, diese wieder hergestellt werden.

Im separaten Wifi-Task soll geprüft werden, ob die WiFi-Verbindung noch steht und falls nein diese wieder herstellen.

ich werde diese beiden Tasks aber noch zu einem gemeinsamen zusammenfassen, dann kommen die sich schon mal nicht in die Quere.

Im Loop-Task soll neben anderen Dingen, die eben flüssig ablaufen sollen, wie z.B. die lückenlose Aufzeichnung von Temperaturdaten, ein bestimmter Wert an den MQTT-Broker übermittelt werden.

Von daher sollte das doch ok sein: Der (später) EINE separate Tasks kuckt immer im Hintergrund, ob wir verbunden sind bzw. verbindet, der andere (loop) macht was, FALLS wir verbunden sind.

So sollten die sich doch miteinander vertragen, oder?
Der Typ im verlinkten YouTube-Video macht das doch auch so ;-(
Und das hab ich auch meine ich schon an anderer Stelle im Netz mal gelesen.

Ist das so abwegig?

Oder Gegenfrage:
Was ist denn die smarteste Lösung, um die WiFi-Verbindung des ESP32 immer aufrecht zu erhalten ohne ständig im main-loop mit einem Verbindungsversuch unterbrechen zu müssen?
Denn der ist ja in der Regel ein while-loop mit ein paar Sekunden.