ESP32 RTOS tasks

Basierend auf diesen Thread

wollte ich mal einen Sketch machen mit 3 Tasks,
Task A und B sollten sich den Core selber wählen.
Task C fixiere ich auf 0

Sobald ich im Task B die eine der zwei Testausgaben aktiviere, stürzt das Program/rebootet der ESP.

Warum - und wie kann ich das beheben?

//

void printInfo(char c, int core)
{
  Serial.printf("This task %c runs on core %d\n", c, core);
}

void taskA(void * parameter)
{
  for (;;)
  {
    printInfo('A', xPortGetCoreID());
    Serial.println("A");
    delay(1100);
  }
}

void taskB(void * parameter)
{
  for (;;)
  {
    //printInfo('B', xPortGetCoreID());  // krachbumbang
    //Serial.printf("This task %c runs on core %d\n", 'B', xPortGetCoreID());  //krachbumbang
    Serial.println("B");
    delay(1200);
  }
}

void taskC(void * parameter)
{
  for (;;)
  {
    printInfo('C', xPortGetCoreID());
    Serial.println("C fix auf 1");
    delay(1300);
  }
}

void setup() {
  Serial.begin(115200);
  xTaskCreatePinnedToCore(
    taskA
    ,  "TaskA"   // A name just for humans
    ,  1024  // This stack size can be checked & adjusted by reading the Stack Highwater
    ,  NULL
    ,  2  // Priority, with 3 (configMAX_PRIORITIES - 1) being the highest, and 0 being the lowest.
    ,  NULL
    ,  tskNO_AFFINITY);

  xTaskCreatePinnedToCore(
    taskB
    ,  "TaskB"   // A name just for humans
    ,  1024  // This stack size can be checked & adjusted by reading the Stack Highwater
    ,  NULL
    ,  3  // Priority, with 3 (configMAX_PRIORITIES - 1) being the highest, and 0 being the lowest.
    ,  NULL
    ,  tskNO_AFFINITY);

  xTaskCreatePinnedToCore(
    taskC
    ,  "TaskC"   // A name just for humans
    ,  1024  // This stack size can be checked & adjusted by reading the Stack Highwater
    ,  NULL
    ,  9  // Priority, with 3 (configMAX_PRIORITIES - 1) being the highest, and 0 being the lowest.
    ,  NULL
    ,  1);  // core fix zugewiesen

}

void loop() {

}

Zusatzfrage: geschieht die (automatische) Zuordnung der Tasks zu Cores dynamisch zur Laufzeit und könnte sich somit ändern? oder wird das einmalig festgelegt?

ESP meldet den Grund!

Debug exception reason: Stack canary watchpoint triggered (TaskB) 

Und danach macht FreeRTOS einen SoftwareReset.

Abhilfe:

  xTaskCreatePinnedToCore(
    taskB
    ,  "TaskB"   // A name just for humans
    ,  2048  // This stack size can be checked & adjusted by reading the Stack Highwater
    ,  NULL
    ,  3  // Priority, with 3 (configMAX_PRIORITIES - 1) being the highest, and 0 being the lowest.
    ,  NULL
    ,  tskNO_AFFINITY);

1 Like

Lösung ja. Aber warum passiert das? Warum braucht TaskB mehr stack als TaskA, der unterschiedliche delay (?!?)

Weiß nicht, so genau .....
Serial::printf nutzt intern vsnprintf
Und beide legen Buffer auf den Stack an.
Einmal 64 Byte und einmal 80(zumindest in den alten AVR Libc war das so, beim ESP kann ich das nicht unmittelbar prüfen)

Zudem, wenn ich mich recht erinnere, laufen Interrupts auch in dem Task Kontext, in dem sie zufällig aufgerufen werden.

Der Stack muss also alle Buffer aufnehmen, die Push Orgie der in der Task aktiven Funktionen und der ISR Routinen

Wenn ein kompletter Registersatz gerettet werden soll, gehen dafür schon 512 Bytes drauf. Plus Rücksprungadressen und Flags.

Da sind 1028 Byte Stack eben nicht besonders viel.

1 Like

wenn man nichts weis, muss man es selber ausprobieren:
zu meiner Zusatzfrage sehe ich aktuell, dass z.B. Task A durchaus auch manchmal auf Core1 durchgeführt wird,

void printInfo(char c, int core)
{
  Serial.printf("%c runs on core %d\n", c, core);
}

void taskA(void * parameter)
{
  (void)parameter;
  for (;;)
  {
    printInfo('A', xPortGetCoreID());
    Serial.println('A');
    vTaskDelay(300);
  }
}

void taskB(void * parameter)
{
  (void)parameter;
  for (;;)
  {
    printInfo('B', xPortGetCoreID());  // krachbumbang
    Serial.println('B');
    vTaskDelay(1200);
  }
}

void taskC(void * parameter)
{
  (void)parameter;
  for (;;)
  {
    printInfo('C', xPortGetCoreID());
    Serial.println("C fixed        1");
    vTaskDelay(1300);
  }
}

void taskD(void * parameter)
{
  (void)parameter;
  for (;;)
  {
    printInfo('D', xPortGetCoreID());
    Serial.println("D fix          0");
    vTaskDelay(1400);
  }
}

void setup() {
  Serial.begin(115200);
  xTaskCreatePinnedToCore(
    taskA               // Pointer to the task entry function. Tasks must be implemented to never return (i.e. continuous loop), or should be terminated using vTaskDelete function.
    ,  "TaskA"          // A descriptive name for the task. This is mainly used to facilitate debugging. Max length defined by configMAX_TASK_NAME_LEN - default is 16
    ,  1024             // The size of the task stack specified as the number of bytes. This stack size can be checked & adjusted by reading the Stack Highwater
    ,  NULL             // Pointer that will be used as the parameter for the task being created.
    ,  2                // Priority, with 3 (configMAX_PRIORITIES - 1) being the highest, and 0 being the lowest.
    ,  NULL             // Used to pass back a handle by which the created task can be referenced.
    ,  tskNO_AFFINITY); // If the value is tskNO_AFFINITY, the created task is not pinned to any CPU, and the scheduler can run it on any core available.

  xTaskCreatePinnedToCore(
    taskB
    ,  "TaskB"   // A name just for humans
    ,  2048      // This stack size can be checked & adjusted by reading the Stack Highwater
    ,  NULL
    ,  2         // Priority, with 3 (configMAX_PRIORITIES - 1) being the highest, and 0 being the lowest.
    ,  NULL
    ,  tskNO_AFFINITY);

  xTaskCreatePinnedToCore(
    taskC
    ,  "TaskC"   // A name just for humans
    ,  1024      // This stack size can be checked & adjusted by reading the Stack Highwater
    ,  NULL
    ,  9         // Priority, with 3 (configMAX_PRIORITIES - 1) being the highest, and 0 being the lowest.
    ,  NULL
    ,  1);       // core fix zugewiesen

  xTaskCreatePinnedToCore(
    taskD
    ,  "TaskD"   // A name just for humans
    ,  1024      // This stack size can be checked & adjusted by reading the Stack Highwater
    ,  NULL
    ,  9         // Priority, with 3 (configMAX_PRIORITIES - 1) being the highest, and 0 being the lowest.
    ,  NULL
    ,  0);       // core fix zugewiesen

}

void loop() {
}

gibt

A runs on core 0
A
B runs on core 0
B
A runs on core 1
A
A runs on core 0
A
C runs on core 1
C fixed        1
A runs on core 0
A
D runs on core 0
D fix          0
A runs on core 0
A
B runs on core 0
B
A runs on core 1

Jetzt fehlt mir noch das Problem für eine Lösung durch mehrere Tasks auf zwei Cores :wink:

Mir fehlt der Sinn des ganzen Threads. Ich erkenne nirgends, dass überhaupt irgendwas gleichzeitig läuft. Und mit einem RealTimeOperatingSystem sollte das doch, egal mit wie wenigen Core, möglich sein, dass es einem eine Ausgabe wie
A runs onC runs o ncore 0
hinschmiert.

Warum sollte man das wollen?
Meiner Ansicht nach sollte man das eher verhindern wollen!

Ein kleiner Test: (von vorher kopiert und modifiziert)



void taskA(void * parameter)
{
  for (;;)
  {
    Serial.println("taskA");
  }
}

void taskB(void * parameter)
{
  for (;;)
  {
    Serial.println("taskB");
  }
}

void taskC(void * parameter)
{
  for (;;)
  {
    Serial.println("taskC");
  }
}

void setup() {
  Serial.begin(115200);
  xTaskCreatePinnedToCore(
    taskA
    ,  "TaskA"   // A name just for humans
    ,  1380  // This stack size can be checked & adjusted by reading the Stack Highwater
    ,  NULL
    ,  1  // Priority, with 3 (configMAX_PRIORITIES - 1) being the highest, and 0 being the lowest.
    ,  NULL
    ,  tskNO_AFFINITY);

  xTaskCreatePinnedToCore(
    taskB
    ,  "TaskB"   // A name just for humans
    ,  2048  // This stack size can be checked & adjusted by reading the Stack Highwater
    ,  NULL
    ,  1  // Priority, with 3 (configMAX_PRIORITIES - 1) being the highest, and 0 being the lowest.
    ,  NULL
    ,  tskNO_AFFINITY);

  xTaskCreatePinnedToCore(
    taskC
    ,  "TaskC"   // A name just for humans
    ,  1024  // This stack size can be checked & adjusted by reading the Stack Highwater
    ,  NULL
    ,  1  // Priority, with 3 (configMAX_PRIORITIES - 1) being the highest, and 0 being the lowest.
    ,  NULL
    ,   tskNO_AFFINITY);  // core fix zugewiesen

}

void loop() 
{
  Serial.println("loop");
}

Ausgabe:
(wie man sieht, zerhackt)

taskB
taskAloop
taskB
taskCtaskA



taskCtaskAtaskB
loop
taskAtaskB

taskC

taskB

taskB

Jetzt glücklich?

das umgeht man mit dem Stream Buffer API oder anders?

Da bin ich ungeübt.

Generell gilt, für alle Ressourcen:

Zwei Köche verderben den Brei!

So auch bei der Seriellen.
Geregelt wird das üblicher weise mit Semaphoren/Mutex

Hier vielleicht informativ:

Und hier auch ein paar Betrachtungen:

1 Like

herzchen für Combie.
Semaphore scheint mal ein Weg gewesen zu sein.

// ESP32 - fun with RTOS
// Serial print protected by semaphore (based on an idea of https://forum.arduino.cc/t/freertos-zusammenspiel-von-tasks/641707/21 )

SemaphoreHandle_t xSemaphore = NULL;

void printInfo(char c, int core)
{
  Serial.printf("%c runs on core %d\n", c, core);
}

void taskA(void * parameter)
{
  (void)parameter;
  for (;;)
  {
    if ( xSemaphoreTake( xSemaphore, ( TickType_t ) 10 ) == pdTRUE )
    {
      // If not available, wait 10 ticks and try again
      // If task has it, do some exclusive stuff here;
      printInfo('A', xPortGetCoreID());
      Serial.println('A');
      // After finishing, release the semaphore
      xSemaphoreGive( xSemaphore ); // Now free or "Give" the resource to other tasks which might want it too.
    }
    vTaskDelay(300);
  }
}

void taskB(void * parameter)
{
  (void)parameter;
  for (;;)
  {
    if ( xSemaphoreTake( xSemaphore, ( TickType_t ) 10 ) == pdTRUE )
    {
      // If not available, wait 10 ticks and try again
      // If task has it, do some exclusive stuff here;
      printInfo('B', xPortGetCoreID());
      Serial.println('B');
      // After finishing, release the semaphore
      xSemaphoreGive( xSemaphore ); // Now free or "Give" the resource back to other tasks which might want it too.
    }
    vTaskDelay(1200);
  }
}

void taskC(void * parameter)
{
  (void)parameter;
  for (;;)
  {
    if ( xSemaphoreTake( xSemaphore, ( TickType_t ) 10 ) == pdTRUE )
    {
      // If not available, wait 10 ticks and try again
      // If task has it, do some exclusive stuff here;
      printInfo('C', xPortGetCoreID());
      Serial.println("C fixed        1");
      // After finishing, release the semaphore
      xSemaphoreGive( xSemaphore ); // Now free or "Give" the resource back to other tasks which might want it too.
    }

    vTaskDelay(1300);
  }
}

void taskD(void * parameter)
{
  (void)parameter;
  for (;;)
  {
    if ( xSemaphoreTake( xSemaphore, ( TickType_t ) 10 ) == pdTRUE )
    {
      // If not available, wait 10 ticks and try again
      // If task has it, do some exclusive stuff here;
      printInfo('D', xPortGetCoreID());
      Serial.println("D fixed        0");
      // After finishing, release the semaphore
      xSemaphoreGive( xSemaphore ); // Now free or "Give" the resource back to other tasks which might want it too.
    }
    vTaskDelay(1400);
  }
}

void setup() {
  Serial.begin(115200);
  // create semaphore
  if ( xSemaphore == NULL )                // Ensure that the Semaphore has not already been created.
  {
    xSemaphore = xSemaphoreCreateMutex();  // A mutex semaphore to manage the resource
    if ( ( xSemaphore ) != NULL )
      xSemaphoreGive( ( xSemaphore ) );    // Make the resource available for exclusive use, by "Giving" the Semaphore.
  }

  xTaskCreatePinnedToCore(
    taskA               // Pointer to the task entry function. Tasks must be implemented to never return (i.e. continuous loop), or should be terminated using vTaskDelete function.
    ,  "TaskA"          // A descriptive name for the task. This is mainly used to facilitate debugging. Max length defined by configMAX_TASK_NAME_LEN - default is 16
    ,  2048             // The size of the task stack specified as the number of bytes. This stack size can be checked & adjusted by reading the Stack Highwater
    ,  NULL             // Pointer that will be used as the parameter for the task being created.
    ,  2                // Priority, with 3 (configMAX_PRIORITIES - 1) being the highest, and 0 being the lowest.
    ,  NULL             // Used to pass back a handle by which the created task can be referenced.
    ,  tskNO_AFFINITY); // If the value is tskNO_AFFINITY, the created task is not pinned to any CPU, and the scheduler can run it on any core available.

  xTaskCreatePinnedToCore(
    taskB
    ,  "TaskB"   // A name just for humans
    ,  2048      // This stack size can be checked & adjusted by reading the Stack Highwater
    ,  NULL
    ,  2         // Priority, with 3 (configMAX_PRIORITIES - 1) being the highest, and 0 being the lowest.
    ,  NULL
    ,  tskNO_AFFINITY);

  xTaskCreatePinnedToCore(
    taskC
    ,  "TaskC"   // A name just for humans
    ,  2048      // This stack size can be checked & adjusted by reading the Stack Highwater
    ,  NULL
    ,  9         // Priority, with 3 (configMAX_PRIORITIES - 1) being the highest, and 0 being the lowest.
    ,  NULL
    ,  1);       // core fix zugewiesen

  xTaskCreatePinnedToCore(
    taskD
    ,  "TaskD"   // A name just for humans
    ,  2048      // This stack size can be checked & adjusted by reading the Stack Highwater
    ,  NULL
    ,  9         // Priority, with 3 (configMAX_PRIORITIES - 1) being the highest, and 0 being the lowest.
    ,  NULL
    ,  0);       // core fix zugewiesen
}

void loop() {
}

Von der Implementierung von Serial in Arduino her fehlt mir ein Serial.flush() vor der Rückgabe des Semaphors. Oder ist das jetzt in println() eingebaut?

Nachtrag und Lösung: Wenn immer nur 1 Task aktiv sein kann, dann landen natürlich auch seine Ausgaben am Stück im Ausgabepuffer. Mit Multitasking hat das dann aber nichts mehr zu tun.

imho nein. println erweitert nur den print um den \r\n
der flush() ist Teil des hardwareSerial.

Ich sehe aber auch ohne flush() bisher keine Überschneidungen mehr.

Welchen Sinn soll das machen?

Oder anders:
Es reicht doch, wenn die Ausgaben in der richtigen Reihenfolge in den Ringbuffer der seriellen kommen.

Denn:
Im Buffer mischt sich nix mehr.

Natürlich!
Die anderen Tasks laufen ungestört weiter!

Konkurrierende Zugriffe müssen ausgeschlossen werden!
Da führt kein Weg drum rum.

Die Alternative wäre, es mit Queues abzuhandeln.
Das verlagert das Problem aber nur...
Denn auch die müssen irgendwo "ordentlich" zusammen geführt werden.

Richtig.

Das macht normale Arduino-Programmierung so schön einfach. Da braucht man sich um die Probleme, die durch Multitasking entstehen, nicht zu kümmern :slight_smile:

Die gibt es doch auch bei der Behandlung von Interrupts, und bei Zugriffen auf gemeinsam benutzte (volatile) Variablen.

Sie laufen zwar, tun aber nichts. Wenn sie was tun sollen, sieht die Sache schon anders aus.

P.S.: Ich möchte das nicht weiter diskutieren, es führt erfahrungsgemäß zu nichts.

Mit den üblichen Arduinos, machen wir es uns meist Kooperativ. Das entschärft die Sache erheblich.
Zudem nur 1 Core....

Aber:
Sobald man mehr als einen endlichen Automaten baut, ist man auch sofort in der Ressourcen Verteilungszwicke.
Es ist vielleicht etwas einfacher, schlichter, aber auch grundhässlich!
z.B. Das ausrollen von Schleifen
Bis zur Unkenntlichkeit verstümmelt!
In einem Multitasking System schreibt man eine zählende Schleife, wenn man eine braucht. Fertig.
In typischem AVR Arduino muss man dran schreiben "Dieses emuliert eine Schleife", damit der Leser schnallt was da passiert.
Das ist zwar machbar, aber wirklich schön ist das dann nicht.

Doch, sie tun!
Es bleiben nur die Tasks stehen, welche auf die Ressource warten.

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.