espressif / arduino-esp32

Arduino core for the ESP32
GNU Lesser General Public License v2.1
13.32k stars 7.36k forks source link

High Priority Task Appearing Not to Preempt Low Priority Task??? #8579

Open gfvalvo opened 1 year ago

gfvalvo commented 1 year ago

Board

ESP32 Dev Module, Adafruit ESP32 Feather Huzzah

Device Description

None

Hardware Configuration

Just the board connected to PC via USB.

Version

v2.0.11

IDE Name

Arduino IDE

Operating System

Windows 10

Flash frequency

80 MHz

PSRAM enabled

yes

Upload speed

921600

Description

I don't know if this is an Issue (bug) or simply a known limitation of FreeRTOS. It appears that when an Event Group bit is set with the intention of unblocking a high-priority task, that task does not immediately preempt the low-priority task that set the Event Group bit. Rather, it looks like the context switch does not occur until either the low-priority task is blocked or perhaps when the its Time Slice has expired.

In the MRE below, the Consumer Task has a higher priority than the Producer Task. So, when the Producer sets the Event Group bit, I would expect the Consumer Task to preempt it, do its printing, and then block itself again BEFORE the (lower priority) Producer Task resumes executing. Therefore, it seems that the printing sequence should be:

[338536][I][sketch_aug28a.ino:24] producerFunction(): Producer Function: Setting Event Bits
[338537][I][sketch_aug28a.ino:34] consumerFunction(): Consumer Task Received Event Bit
[338536][I][sketch_aug28a.ino:26] producerFunction(): Producer Function: Event Bits Set

However, what I'm seeing is this:

[338536][I][sketch_aug28a.ino:24] producerFunction(): Producer Function: Setting Event Bits
[338536][I][sketch_aug28a.ino:26] producerFunction(): Producer Function: Event Bits Set
[338537][I][sketch_aug28a.ino:34] consumerFunction(): Consumer Task Received Event Bit

However, the high-priority task DOES preempt if I use a FreeRTOS Notification instead of an Event Group bit.

Sketch

#include "Arduino.h"

void producerFunction(void *pvParameters);
void consumerFunction(void *pvParameters);

EventGroupHandle_t controlGroupHandle;

void setup() {
  Serial.begin(115200);
  vTaskDelay(2000);
  log_i("Starting");

  controlGroupHandle = xEventGroupCreate();
  xTaskCreatePinnedToCore(consumerFunction, "Consumer Task 1", 2000, NULL, 6, NULL, CONFIG_ARDUINO_RUNNING_CORE);
  vTaskDelay(500);
  xTaskCreatePinnedToCore(producerFunction, "Producer Task", 2000, NULL, 2, NULL, CONFIG_ARDUINO_RUNNING_CORE);
}

void producerFunction(void *pvParameters) {
  log_i("Producer Task Started\n");

  for (;;) {
    vTaskDelay(2000);
    log_i("Producer Function: Setting Event Bits");
    xEventGroupSetBits(controlGroupHandle, 1);
    log_i("Producer Function: Event Bits Set");
  }
}

void consumerFunction(void *pvParameters) {
  log_i("Consumer Task Started");
  for (;;) {
    xEventGroupWaitBits(controlGroupHandle, 1, pdTRUE, pdFALSE, portMAX_DELAY);
    log_i("Consumer Task Received Event Bit");
  }
}

void loop() {
}

Debug Message

See problem description above.

Other Steps to Reproduce

No response

I have checked existing issues, online documentation and the Troubleshooting Guide

SuGlider commented 1 year ago

@gfvalvo - Good point... It seems that the time slice must expire before the context switch occurs. Not sure about the reason. I can see that #define configUSE_PREEMPTION 1 is used in Arduino FreeRTOSConfig.h...

@me-no-dev - Do you have any clue about it?

AAJAY5 commented 1 year ago

Hey, I think High priority task does not switch to low priority task until vTaskDelay(). You can try below code snippest. also you have used Events so 2nd task blocked until event set. So, after setting bit from high priority task without delay/vTaskDelay it will not switch task.

#include "Arduino.h"

void producerFunction(void *pvParameters);
void consumerFunction(void *pvParameters);

EventGroupHandle_t controlGroupHandle;

void setup() {
  Serial.begin(115200);
  vTaskDelay(2000);
  log_i("Starting");

  controlGroupHandle = xEventGroupCreate();
  xTaskCreatePinnedToCore(consumerFunction, "Consumer Task 1", 2000, NULL, 2, NULL, CONFIG_ARDUINO_RUNNING_CORE);
  vTaskDelay(500);
  xTaskCreatePinnedToCore(producerFunction, "Producer Task", 2000, NULL, 2, NULL, CONFIG_ARDUINO_RUNNING_CORE);
}

void producerFunction(void *pvParameters) {
  log_i("Producer Task Started\n");

  for (;;) {
    vTaskDelay(2000);
    log_i("Producer Function: Setting Event Bits");
    xEventGroupSetBits(controlGroupHandle, 1);
    vTaskDelay(1);
    log_i("Producer Function: Event Bits Set");
  }
}

void consumerFunction(void *pvParameters) {
  log_i("Consumer Task Started");
  for (;;) {
    xEventGroupWaitBits(controlGroupHandle, 1, pdTRUE, pdFALSE, portMAX_DELAY);
    log_i("Consumer Task Received Event Bit");
  }
}

void loop() {
    vTaskDelete(NULL);
}

image

gfvalvo commented 1 year ago

Hey, I think High priority task does not switch to low priority task until vTaskDelay(). You can try below code snippest. also you have used Events so 2nd task blocked until event set. So, after setting bit from high priority task without delay/vTaskDelay it will not switch task.

Yes, I tested it with the delay before I posted my question and got the same behavior as you show. But, IMO, it's not correct. A higher-priority task always preempt when it becomes ready (unblocked) as a result of a Task Notification, data posting to a Queue, etc. Why would it be different when an Event Group bit is set?

AAJAY5 commented 1 year ago

I received same results by using notification...

#include "Arduino.h"

void producerFunction(void *pvParameters);
void consumerFunction(void *pvParameters);

EventGroupHandle_t controlGroupHandle;
TaskHandle_t task1,task2;

void setup() {
  Serial.begin(115200);
  vTaskDelay(2000);
  Serial.println("Starting");

  controlGroupHandle = xEventGroupCreate();
  xTaskCreatePinnedToCore(consumerFunction, "Consumer Task 1", 2000, NULL, 2, &task1, CONFIG_ARDUINO_RUNNING_CORE);
  vTaskDelay(500);
  xTaskCreatePinnedToCore(producerFunction, "Producer Task", 2000, NULL, 2, &task2, CONFIG_ARDUINO_RUNNING_CORE);
}

void producerFunction(void *pvParameters) {
  Serial.println("Producer Task Started\n");

  for (;;) {
    vTaskDelay(2000);
    Serial.println("Producer Function: Setting Event Bits");
    // xEventGroupSetBits(controlGroupHandle, 1);
    xTaskNotify(task1,1,eNoAction);
    Serial.println("Producer Function: Event Bits Set");
  }
}

void consumerFunction(void *pvParameters) {
  Serial.println("Consumer Task Started");
  uint32_t val;
  for (;;) {
    // xEventGroupWaitBits(controlGroupHandle, 1, pdTRUE, pdFALSE, portMAX_DELAY);
    xTaskNotifyWait(1,0,&val,portMAX_DELAY);
    Serial.println("Consumer Task Received Event Bit");
  }
}

void loop() {
    vTaskDelete(NULL);
}

image

gfvalvo commented 1 year ago

I received same results by using notification...

That's because your Producer and Consumer tasks have the same priority, so there'll be no preemption. Try the code below. Note the relative priority of the 3 tasks (1 Producer + 2 Consumers). You'll see that reflected in the order of the printouts.

#include "Arduino.h"

void producerFunction(void *pvParameters);
void consumerFunction(void *pvParameters);

TaskHandle_t consumer0TaskHandle;
TaskHandle_t consumer1TaskHandle;

void setup() {
    Serial.begin(115200);
    vTaskDelay(2000);
    Serial.println("Starting");

    uint32_t ConsumerTaskNumber = 0;
    xTaskCreatePinnedToCore(consumerFunction, "Consumer Task 0", 2000, &ConsumerTaskNumber, 2, &consumer0TaskHandle, CONFIG_ARDUINO_RUNNING_CORE);
    vTaskDelay(500);

    ConsumerTaskNumber = 1;
    xTaskCreatePinnedToCore(consumerFunction, "Consumer Task 1", 2000, &ConsumerTaskNumber, 6, &consumer1TaskHandle, CONFIG_ARDUINO_RUNNING_CORE);
    vTaskDelay(500);

    xTaskCreatePinnedToCore(producerFunction, "Producer Task", 2000, NULL, 4, NULL, CONFIG_ARDUINO_RUNNING_CORE);
}

void producerFunction(void *pvParameters) {
    Serial.println("Starting Producer Task");
    vTaskDelay(2000);
    Serial.printf("\nProducer Task: Sending Notifications\n");
    xTaskNotifyGive(consumer0TaskHandle);
    xTaskNotifyGive(consumer1TaskHandle);
    Serial.printf("Producer Task: Notifications Sent\n");

    for (;;) {
        vTaskDelay(1000);
    }
}

void consumerFunction(void *pvParameters) {
    uint32_t taskNumber = *reinterpret_cast<uint32_t*>(pvParameters);
    Serial.printf("Starting Consumer Task # %d\n", taskNumber);

    for (;;) {
        ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
        Serial.printf("Consumer Task %d Received Notification\n", taskNumber);
    }
}

void loop() {
}
AAJAY5 commented 1 year ago

Yes, I forgot to change during testing. After changing to 6 output as expected.

I received same results by using notification...

That's because your Producer and Consumer tasks have the same priority, so there'll be no preemption. Try the code below. Note the relative priority of the 3 tasks (1 Producer + 2 Consumers). You'll see that reflected in the order of the printouts.

#include "Arduino.h"

void producerFunction(void *pvParameters);
void consumerFunction(void *pvParameters);

TaskHandle_t consumer0TaskHandle;
TaskHandle_t consumer1TaskHandle;

void setup() {
  Serial.begin(115200);
  vTaskDelay(2000);
  Serial.println("Starting");

  uint32_t ConsumerTaskNumber = 0;
  xTaskCreatePinnedToCore(consumerFunction, "Consumer Task 0", 2000, &ConsumerTaskNumber, 2, &consumer0TaskHandle, CONFIG_ARDUINO_RUNNING_CORE);
  vTaskDelay(500);

  ConsumerTaskNumber = 1;
  xTaskCreatePinnedToCore(consumerFunction, "Consumer Task 1", 2000, &ConsumerTaskNumber, 6, &consumer1TaskHandle, CONFIG_ARDUINO_RUNNING_CORE);
  vTaskDelay(500);

  xTaskCreatePinnedToCore(producerFunction, "Producer Task", 2000, NULL, 4, NULL, CONFIG_ARDUINO_RUNNING_CORE);
}

void producerFunction(void *pvParameters) {
  Serial.println("Starting Producer Task");
  vTaskDelay(2000);
  Serial.printf("\nProducer Task: Sending Notifications\n");
  xTaskNotifyGive(consumer0TaskHandle);
  xTaskNotifyGive(consumer1TaskHandle);
  Serial.printf("Producer Task: Notifications Sent\n");

  for (;;) {
      vTaskDelay(1000);
  }
}

void consumerFunction(void *pvParameters) {
  uint32_t taskNumber = *reinterpret_cast<uint32_t*>(pvParameters);
  Serial.printf("Starting Consumer Task # %d\n", taskNumber);

  for (;;) {
      ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
      Serial.printf("Consumer Task %d Received Notification\n", taskNumber);
  }
}

void loop() {
}
SuGlider commented 1 year ago

All the testing I have done indicates that the context switching only happens at the end of the time slice (1ms in Arduino). I can also use a delayMicroseconds() which forces busy wait and no context switch happens. delay() uses vTaskDelay() and then FreeRTOS changes the context on its execution.

The FreeRTOS documentation says that when PREEMPTION is enabled, xEventGroupSetBits() should change the context at the end of its execution, preempting the low priority task, switching to a higher priority task that is waiting for that EventGroup. But it doesn't happen with Arduino...

My suggestion: Try a pure IDF application and check the FreeRTOS setting to make sure that it has configUSE_PREEMPTION enabled. In case the application works correctly, that may mean that something is not set as expected when the Arduino FreeRTOS Libraries are built.

gfvalvo commented 1 year ago

In case the application works correctly, that may mean that something is not set as expected when the Arduino FreeRTOS Libraries are built.

Like I said, it does work correctly when using Task Notifications instead of Event Group bits.

SuGlider commented 1 year ago

Like I said, it does work correctly when using Task Notifications instead of Event Group bits.

So this may be the necessary workaround.

gfvalvo commented 1 year ago

@VojtechBartoska: According to further investigation in another forum: https://www.esp32.com/viewtopic.php?f=19&t=35450#p119563 The issue also happens using ESP-IDF. So, their conclusion is: "This is an Espressif freertos port issue; changing over to the Amazon kernel fixes the behaviour."

SuGlider commented 1 year ago

Thanks @gfvalvo! The given conclusion sounds really interesting... I'll also follow this with ESP_Sprite.

VojtechBartoska commented 5 days ago

Hello, I have checked our internal tracking tool and the status is still open. I will double check it with ESP-IDF colleagues.