FreeRTOS – Zusammenspiel von Tasks

Moin in die Runde,

ich verliebe mich gerade in den ESP32, und möchte mit diesem meinen 5-Achsen Kamera Bot fertig stellen, der vormals an den Ressourcen eines Nano (oder Mega) gescheitert war. Da ich immer noch mit C++ hadere, war ich vor einem Jahr froh, dass mich Combie auf seinen TaskMakro hin wies. Damit gelang es mir, die vielen Teilfunktionen in nicht blockierende Tasks zu granulieren, die wunderbar zusammen spielten.

Jetzt habe ich von ihm den Tipp bekommen, dass die ESP32 Implementation in der Arduino IDE automatisch freeRTOS mitbringt (bei klassischen AVRs kann man es per Lib einbinden), welches beim Task Handling wesentlich mächtiger ist. Zudem ermöglicht es, den 2. Core des ESP32 vollwertig einzusetzen. Dafür gibt es spezifische Funktionen von Espressiv, die freeRTOS um Multicore Funktionen erweitert. Das macht es insgesamt nicht leichter, denn man stößt auf viele Beispiele, die nur auf anderen Entwicklungsplattformen Sinn machen. Ich möchte jedoch in der Arduino Umgebung bleiben.

Die Doku von freeRTOS liest sich für mich an vielen Stellen wie chinesisch. Als API Referenz wahrscheinlich verständlich, aber zum Lernen (für ambitionierte Stümper wie mich) völlig ungeeignet. Also sucht man nach Tutorials zu freeRTOS in der Arduino Welt – und wird auch fündig. Allen (gefundenen) Beispielen ist gemein, dass sie das wichtigste ausklammern – nämlich das Zusammenspiel mehrerer Tasks.

Es gelingt mir problemlos, zwei (oder mehr) Tasks zu erzeugen, die – unabhängig voneinander – auf einem oder beiden Cores – LEDs zum blinken bringen. Es gelingt mir ebenfalls, eine Task zu erstellen, die sich selbst löscht, wenn sie einmal gelaufen ist. Was mir fehlt, ist ein Real World Beispiel, in welchem eine Task Daten sammelt, die von einer anderen Task im Bedarfsfalle (und nur dann) konsumiert werden.

Ich vermute, dass ich beim freeRTOS evaluieren nicht alleine bin. Vielleicht hat ja jemand von euch einen Link zu einer Art „freeRTOS for Dummies“. Oder ein weiterführendes Beispiel. Ich scheue nicht den Lernaufwand und auch Englisch ist kein Problem.

Danke für allen Input,

Demo

Demokrit:
ich verliebe mich gerade in den ESP32, ...

Da ich immer noch mit C++ hadere, ...

Moin,
im ersten Fall dürfte es eine schwierige weil komplizierte Beziehung werden, im zweiten stimme ich Dir aber aus voller Überzeugung zu. Aber es hilft halt nichts.

Solltest Du an freeRTOS scheitern, dann bleibt Dir C++ als Rückfallmöglichkeit, so viel kann ich Dir versprechen.

Solltest Du bei „freeRTOS for Dummies" nicht fündig werden, könnte ich ein paar graue Zellen von mir zum Mitgrübeln anbieten.

Ansonsten bin ich hier nur Mitleser, da ich bis zu combies Anmerkung nicht wußte, daß ich freeRTOS mit dabei habe. Ich bin gespannt :slight_smile:

Oder speziell Reader/Writer FreeRTOS variable length messages and data stream features

Aus meiner Erfahrung muss man APIs benutzen* um sie zu verstehen,
schreib dir 'einfach' selbst kleine Beispiele in einer Arduino Umgebung. :wink:

* benutzen im Sinne von "in eigenem Kode verwenden"

agmue:
Ansonsten bin ich hier nur Mitleser, da ich bis zu combies Anmerkung nicht wußte, daß ich freeRTOS mit dabei habe. Ich bin gespannt :slight_smile:

Ja, sobald man einen ESP32 im Board Manager auswählt, hat man freeRTOS (fast) vollumfänglich zur Verfügung (plus zusätzliche multicore Erweiterungen). Man muss nichts zusätzlich einbinden, und kann direkt beide Cores nutzen.

Auf einem Arduino kann man freeRTOS als Lib einbinden, und hat danach alle Möglichkeiten des Vanilla freeRTOS. Bei ausgesprochen schlankem Footprint.

Richtig scheitern kann man damit auch nicht, denn selbst ohne Verzahnung einzelner Tasks hat man - quasi ab Werk, beliebig viele Loops(). Die wahre Macht liegt aber wohl in Semaphores, Queues und anderem Teufelswerk, welches mir noch völlig fremd ist.

Demokrit:
... in Semaphores, Queues und anderem Teufelswerk, welches mir noch völlig fremd ist.

Alles sehr simple und logische Konstrukte, keineswegs Teufelswerk.
Benutze sie und du wirst das sicherlich auch sehen.

Whandall:
Aus meiner Erfahrung muss man APIs benutzen* um sie zu verstehen,
schreib dir 'einfach' selbst kleine Beispiele in einer Arduino Umgebung. :wink:

* benutzen im Sinne von "in eigenem Kode verwenden"

Bin gerade schon einen großen Schritt weiter gekommen, indem ich einfach globale Variablen nutze. Derzeit blinkt eine Task vor sich hin, während sie die Blinks zählt, eine andere gibt den Blink Zähler auf einem I2C Display aus, und eine 3. überwacht einen Taster, der den Zähler zurück setzt. Trial & Error hilft also durchaus weiter.

Ich muss gestehen, auch wenn ich mich schon geraume Zeit mit RTOS rumschlagen musste, sind mir die Details von FreeRTOS und ESP32 relativ fremd.

Aber die grundlegenden Konzepte und Ideen sind immer die gleichen.

, indem ich einfach globale Variablen nutze.

Allerdings muss man sie verriegeln, deine globalen Variablen.
Denn es gib ja bei einem 2 Kern µC immer 2 Tasks welche gleichzeitig (wirklich gleichzeitig, nicht quasi gleichzeitig, sondern das absolute gnadenlose echte Gleichzeitig) zugreifen können.
Auch kann mitten im Zugriff ein Taskswitch erfolgen.

Und dann ist man schon bei Mutex, Semaphore usw.
Ein Semaphor ist nichts anderes (in seiner einfachsten Form), als eine verriegelte globale Variable zum Zwecke andere Dinge vor konkurierenden Zugriffen zu schützen.

Der ESP und sein Freetos bringt das alles schon funktionsfähig mit.
Man muss es nur in der Doku finden und nutzen.

Das Thema ist komplex.
Auch lesenswert: Das Philosophen Problem
Das erklärt eigentlich ganz genau worum es sich dreht.
Wo der Hase im Pfeffer liegt.

Hier ein Beispiel, dass es auch ohne vorgefertigte Dinge(FreeRTOS) geht.
Da findet sich auch eine URL zum Hintergrund

combie:
Denn es gib ja bei einem 2 Kern µC immer 2 Tasks welche gleichzeitig zugreifen können.
Auch kann mitten im Zugriff ein Taskswitch erfolgen.

Die Interrupts je Kern kommen auch noch dazu.

Whandall:
Die Interrupts je Kern kommen auch noch dazu.

Das ist wahr.
Die DMA nicht zu vergessen, die fummelt auch noch im Ram rum, wenn man sie nutzt.

Im Falle des K210 gibt es noch zusätzliche Subsysteme wie die FFT, Audio und KI Einheit, welche selber, völlig asynchron zu den 2 Kernen den Speicher manipulieren.

combie:
Aber die grundlegenden Konzepte und Ideen sind immer die gleichen.
Allerdings muss man sie verriegeln, deine globalen Variablen.
Denn es gib ja bei einem 2 Kern µC immer 2 Tasks welche gleichzeitig (wirklich gleichzeitig, nicht quasi gleichzeitig, sondern das absolute gnadenlose echte Gleichzeitig) zugreifen können.
Auch kann mitten im Zugriff ein Taskswitch erfolgen.

Ja, das ist mir grundsätzlich bewusst. Derzeit trickse ich mich noch mit unterschiedlichen Prioritäten und (der freeRTOS Variante von) delays durch, muss aber jetzt die korrekten Konzepte lernen.

In meiner tatsächlichen Anwendung (die derzeit lernbedingt ruht) habe ich z.B. 3 Devices am I2C Bus. Ein LCD Display für Menüführung und Prozessausgaben, einen Wii Controller für eine 5-Achsen Steuerung (bei Handbedienung) sowie einen MCP23017 als Port Expander, da mir selbst beim ESP32 die Ports ausgegangen sind. Ideal wäre eine Separation dieser 3 Dinge in Form von Tasks. Problematisch ist dabei, dass diese sich nicht gegenseitig ins Gehege kommen dürfen, denn sie teilen sich nur einen I2C Bus, bei dem zeitgleiche Zugriffe beider Cores wahrscheinlich übel enden würden.

Das wird noch eine steile Lernkurve - aber die Sache scheint es wert.

Whandall:
FreeRTOS task communication and synchronisation with queues, binary semaphores, mutexes, counting semaphores and recursive semaphores

Super - der Link ist Gold wert.

Grundlagen der Betriebsysteme.

Ich habe nur kurz drübergeschaut, klingt aber ganz brauchbar, wenn auch etwas staubig.

Grundlagen der Betriebssysteme, Prof. Dr. Dieter Zöbel, Vera Christ

Entsprechende Informatik Vorlesung inklusive Skript in PDF Form. :wink:

Mir ist gerade aufgefallen, dass ich zwar geschrieben habe, dass meine ersten (Demo) Tasks mit nicht thread-sicheren globalen Variablen funktioniert, aber ich hätte sie auch posten können, um in der Diskussion zu bleiben.

Bei Mutexen und Semaphoren nähere ich mich einem minimalen Verständnis. Zu gering aber noch, um es hier und jetzt schon richtig zu machen.

Ich finde das hier schon beeindruckend genug:

#include <Wire.h>
#include <LiquidCrystal_I2C.h>

#define BUTTON_PIN 32   // use pin with pullup capabilities or add a resistor.

uint16_t blinkCounter = 0;
uint8_t buttonState = LOW;
bool buttonPressed = false;

LiquidCrystal_I2C lcd(0x27, 20, 4);

TaskHandle_t blinkHandle;

// This task will run 20 times and then self-destroy
void blinkTask( void * pvParameter ) {

  for ( int i = 0; i < 20; i++ ) {

    Serial.println("BlinkTask/ Core 0");

    // Turn the LED on
    digitalWrite(LED_BUILTIN, HIGH);

    // Pause the task for 500ms
    vTaskDelay(500 / portTICK_PERIOD_MS);

    // Turn the LED off
    digitalWrite(LED_BUILTIN, LOW);

    blinkCounter++;
    vTaskDelay(1000 / portTICK_PERIOD_MS);
  }
  // After 20 blinks, remove the task
  if (blinkHandle != NULL) {
    Serial.println("Ending BlinkTask");
    vTaskDelete( blinkHandle );
  }
}

// Handle a button with debounce
void buttonTask( void * pvParameter) {
  for (;;) {              // infinite loop
    Serial.println("Button task/ Core 0");
    buttonState = digitalRead(BUTTON_PIN);
    vTaskDelay(5 / portTICK_PERIOD_MS);   // 5 ms debounce
    buttonPressed = (buttonState == HIGH) ? false : true;
  }
}

// Output on LCD
void lcdTask( void * pvParameter) {
  for (;;) {              // infinite loop
    Serial.println("LCD task/ Core 1");
    lcd.setCursor (14, 2);
    lcd.print(blinkCounter);
    lcd.setCursor (10, 3);
    lcd.print( buttonPressed ? F("pressed ") : F("released"));
    vTaskDelay(100 / portTICK_PERIOD_MS);
  }
}

void setup() {

  Serial.begin(115200);
  delay(1000);
  pinMode(LED_BUILTIN, OUTPUT);
  pinMode(BUTTON_PIN, INPUT_PULLUP);

  // LCD Begin
  lcd.init();
  lcd.backlight();
  lcd.setCursor (0, 1);
  lcd.print(F("Let's multitask"));
  lcd.setCursor (0, 2);
  lcd.print(F("Blink Counter: "));
  lcd.setCursor (0, 3);
  lcd.print(F("Button: "));

  xTaskCreatePinnedToCore(
    blinkTask,        // Task function.
    "BlinkTask",      // String with name of task, used for debug.
    1000,             // Stack size in bytes on ESP, words on other
    NULL,             // Parameter passed as input of the task
    0,                // Priority of the task.
    &blinkHandle,     // Task handle. Can be used for removal, suspension, enable,.. the task
    0 );              // MCU Core 0

  xTaskCreatePinnedToCore(
    buttonTask,       // Task function.
    "ButtonTask",     // String with name of task.
    1000,             // Stack size in bytes.
    NULL,             // Parameter passed as input of the task
    0,                // Priority of the task.
    NULL,             // Task handle.
    0);               // MCU Core 0

  xTaskCreatePinnedToCore(
    lcdTask,          // Task function.
    "LcdTask",        // String with name of task.
    5000,             // Stack size in bytes.
    NULL,             // Parameter passed as input of the task
    2,                // Priority of the task.
    NULL,             // Task handle.
    1);               // MCU Core 1
}

void loop() {
  delay(10000);        // Do something realy stupid here, just to show that it doesn't matter
                       // On the ESP32 the loop() runs on core 1 with priority 1 by default
}

Sorry für die englischen Kommentare, aber ich bin zu alt, um mich noch zu ändern.

Das Beispiel lässt sich einfachst auch auf Single-Core Arduinos portieren. freeRTOS Lib einbinden, und die ESP32-spezische Task Definition "xTaskCreatePinnedToCore" durch den freeRTOS Standard "xTaskCreate" ersetzen UND den letzten Parameter (den MCU Core) rausschmeißen. Dann sollte es auch auf einem Nano laufen. Vielleicht noch etwas an den Prioritäten schrauben. Damit zu spielen macht sowieso Sinn.

Mittlerweile ist mir noch aufgefallen, dass der ESP32 ja sogar einen 3. Kern enthält. Den Low-Power Kern. Vielleicht kann man den ja auch noch irgendwie beschäftigen? :slight_smile:

Vielleicht kann man den ja auch noch irgendwie beschäftigen? :slight_smile:

Der läuft auch im Deepsleep weiter...
Dürfte der größte Vorteil sein.

Sorry für die englischen Kommentare, aber ich bin zu alt, um mich noch zu ändern.

Das ist mir nur recht.
Weiter machen!

Danke für das einfache Beispiel, das probiere ich mal :slight_smile:

Demokrit:
...
Problematisch ist dabei, dass diese sich nicht gegenseitig ins Gehege kommen dürfen, denn sie teilen sich nur einen I2C Bus, bei dem zeitgleiche Zugriffe beider Cores wahrscheinlich übel enden würden.

In der Tat - wobei "übel" einfach nur bedeutet: geht nicht.

Die naheliegende Lösung ist dann ein Task, der nix weiter macht als I2C zu bedienen. Der bekommt von Deinen anderen Tasks jeweils die Aufträge zugespielt, arbeitet die nacheinander ab und verteilt die Ergebnisse wieder zurück an den jeweils anfragenden Job.

Ein durchaus anspruchsvolle Aufgabe; leider fehlt mir momentan die Zeit, um da mal ein Gerüst zu bauen.

Gruß Walter

Solange man nicht unbedingt eine definierte Reihenfolge einhalten muss, tun es auch die üblichen Verriegelungen, damit sich nichts gegenseitig ins Essen spuckt.

wno158:
Ein durchaus anspruchsvolle Aufgabe; leider fehlt mir momentan die Zeit, um da mal ein Gerüst zu bauen.

Gruß Walter

Hallo Walter,
aber das mit der Zeit ist doch überhaupt kein Problem: Schau, der Tag hat 24 Stunden. Und reichen die mal nicht aus, ja dann nimmste einfach noch die Nacht dazu, dann hast Du noch mindestens 8 - 12 Stunden mehr. :o

LG Stefan

Möglicherweise führt ja der Corona-Sommer 2020 - bedingt durch anstehende Selbstisolation - zu einem Quantensprung bei der Entwicklung :slight_smile:

Man sollte zum ESP32 in der Arduino Umgebung noch erwähnen, dass dieser IMMER unter freeRTOS läuft - also auch, wenn man damit gar nix weiter machen will. setup() und loop() laufen dabei immer auf Core 1 und mit Priorität 1.

Vor mir liegen zwei Testaufbauten. Ein kleiner, nur zum RTOS lernen, und ein sehr komplexer, mit 5 Motoren und voller Ausstattung. Bei dem komplexen Aufbau hatte ich bislang ein (lässliches) Problem. Wenn jemand während des synchronisierten Betriebs von 5 Motoren den Encoder (welcher interruptgetrieben ist) betätigte, kamen die Motoren für den Bruchteil einer Sekunde zum Stocken (eher hör- als sichtbar). Lagert man jedoch - quasi als minimale freeRTOS Implementation - alle nicht-motorrelevanten Programmteile auf den Core 0 aus, so geschieht dies nicht. Das ist schon mal was.

Bei meiner I2C Problematik fällt auf, dass sich Display (mit LCDMenuLib2) und Wii-Controller (mit Lib Nintendo_Extension_Ctrl ) nicht ins Gehege kommen. Anders sieht es aus, wenn auch noch der Port-Expander (MCP23017) mitspielt. Letzteren benötige ich eigentlich nur während der Initialisierung nach Power Up. An ihm hängen End- und Limit Switche der 5 Achsen. Wenn ich für den eine sich selbst löschende Task bastle (die ggf. vor dem Löschen eine LCD -und Wii-Task aktiviert), müsste eigentlich alles weitere ohne Kollision klappen.

Ich bin einen entscheidenden Schritt weiter gekommen, und habe das Beispiel entsprechend abgeändert:

Die Button Task wird direkt nach Definition schlafen geschickt (suspended). In diesem Modus fordert sie keinerlei Aufmerksamkeit vom Prozessor (gut sichtbar an der Serial Konsole).

Nachdem die LED 20x geblinkt hat, aktiviert die BlinkTask die Button Task und löscht sich selbst. Erst jetzt reagiert der Button auf Betätigung.

Dies alles noch ohne Mutexes oder Semaphores, aber mit Bordmitteln von freeRTOS. Ich begreife langsam, welche Power man damit für Entwicklungen erhält.

#include <Wire.h>
#include <LiquidCrystal_I2C.h>

#define BUTTON_PIN 32   // use pin with pullup capabilities or add a resistor.

uint16_t blinkCounter = 0;
uint8_t buttonState = LOW;
bool buttonPressed = false;

LiquidCrystal_I2C lcd(0x27, 20, 4);

TaskHandle_t blinkHandle;
TaskHandle_t xButtonHandle;

// This task will run 20 times and then self-destroy
// Before self destruction it will activate ButtonTask
void blinkTask( void * pvParameter ) {

  for ( int i = 0; i < 20; i++ ) {

    Serial.println("BlinkTask/ Core 0");

    // Turn the LED on
    digitalWrite(LED_BUILTIN, HIGH);

    // Pause the task for 500ms
    vTaskDelay(500 / portTICK_PERIOD_MS);

    // Turn the LED off
    digitalWrite(LED_BUILTIN, LOW);

    blinkCounter++;
    vTaskDelay(1000 / portTICK_PERIOD_MS);
  }
  // After 20 blinks, activate ButtonTask
  if (xButtonHandle != NULL) {
    Serial.println("Activating ButtonTask");
    vTaskResume( xButtonHandle );
  }
  // Now go and kill yourself
  if (blinkHandle != NULL) {
    Serial.println("Ending BlinkTask");
    vTaskDelete( blinkHandle );
  }
}

void buttonTask( void * pvParameter) {
  for (;;) {              // infinite loop
    Serial.println("Button task/ Core 0");
    buttonState = digitalRead(BUTTON_PIN);
    vTaskDelay(5 / portTICK_PERIOD_MS);   // 5 ms debounce
    buttonPressed = (buttonState == HIGH) ? false : true;
  }
}

void lcdTask( void * pvParameter) {
  for (;;) {              // infinite loop
    Serial.println("LCD task/ Core 1");
    lcd.setCursor (14, 2);
    lcd.print(blinkCounter);
    lcd.setCursor (10, 3);
    lcd.print( buttonPressed ? F("pressed ") : F("released"));
    vTaskDelay(100 / portTICK_PERIOD_MS);
  }
}

void setup() {

  Serial.begin(115200);
  delay(1000);
  pinMode(LED_BUILTIN, OUTPUT);
  pinMode(BUTTON_PIN, INPUT_PULLUP);

  // LCD Begin
  lcd.init();
  lcd.backlight();
  lcd.setCursor (0, 1);
  lcd.print(F("Let's multitask"));
  lcd.setCursor (0, 2);
  lcd.print(F("Blink Counter: "));
  lcd.setCursor (0, 3);
  lcd.print(F("Button: "));


  xTaskCreatePinnedToCore(
    blinkTask,        // Task function.
    "BlinkTask",      // String with name of task, used for debug.
    1000,             // Stack size in bytes on ESP, words on other
    NULL,             // Parameter passed as input of the task
    0,                // Priority of the task.
    &blinkHandle,     // Task handle. Can be used for removal, suspension, enable,.. the task
    0 );              // MCU Core 0

  xTaskCreatePinnedToCore(
    buttonTask,       // Task function.
    "ButtonTask",     // String with name of task.
    1000,             // Stack size in bytes.
    NULL,             // Parameter passed as input of the task
    0,                // Priority of the task.
    &xButtonHandle,   // Task handle.
    0);               // MCU Core 0

  // Use the handle to suspend the created task right after creation.
  vTaskSuspend( xButtonHandle );

  xTaskCreatePinnedToCore(
    lcdTask,          // Task function.
    "LcdTask",        // String with name of task.
    5000,             // Stack size in bytes.
    NULL,             // Parameter passed as input of the task
    2,                // Priority of the task.
    NULL,             // Task handle.
    1);               // MCU Core 1
}

void loop() {
  delay(10000);        // Do something realy stupid here, just to show that it doesn't matter
  // On the ESP32 the loop() runs on core 1 with priority 1 by default
}

Noch ein Tipp: Die Doku auf den Espressiv Seiten zum Thema freeRTOS sind wesentlich verständlicher als die der offiziellen freeRTOS Site. Dies dürfte auch für den Einsatz auf AVRs gelten.