xTaskNotify and ulTaskNotifyTake issues

Hello community,

This is my first post on this forum so I hope i will make everything correct and understandable.

I have a project using ESP32, freeRTOS, oled, touchbuttons...

Since this is my first hw and/or sw project (learning by doing), I learnt a lot during the previous couple of weeks but am still struggling with some issues. However, for most issues there is already a question asked and most likely a solution available online, hence why I haven't asked anything here yet.

But, as the saying goes "there's always a first time" so I will jump into it:

I have created a program with several tasks divided on both cores of the ESP32. For simplicity I have a code snippet with only 3 tasks to show my issue.

What happens in the code:

Loop:
includes a routine for buttonpress (touch detect) check. if the touchpad is activated, the program shall switch from task 1 to task 2 then task3 and than back to task 1 again.

While switching between the tasks, i would like to suspend the previous task before i go to the next one and with "xTaskNotify" i would like to call the new task.

Now for the strange part:
During boot up, in the setup section i call xTaskNotify for task1. This is executed perfectly and I am able to see "Task 1 entry" in the serial monitor.
Another touchbutton activation works well too. I suspend task1, send xTaskNotify and see "Task 2 entry" and the "for loop" i have included into this task. The same for task3 - so far so good.

But now the strange thing happens. If task3 is active at the moment and I would like to change to task1 again, this works without any issues, BUT if i want to change from task1 to task2 or from task2 to task3, with the same method as before - the program dives in directly into the "for loop" of the given task, without execution of the "Task x entry" part. I tried moving the if condition after the "for" part, moving the suspend command, adding vTaskDelay command... but nothing helped.

So to sum up, the first loop task1 -> task2 -> task3 works flawlesly, but after that, my task2 and task3 entries are not executed.

Can anyone suggest why or what I should be doing differently?

Thanks!

BR
RoB

uint8_t mode = 5, longtouchtime = 350, touchdebounce = 200, touch1begin =0, touch2begin = 0;
unsigned long touch1now = 0, touch1last = 0, touch1timer = 0, touch1longtime = 0;
bool touch1active = false, longtouchactive = false, relaisstate = false;

void task1( void *pvParameters );
void task3( void *pvParameters );
void task2( void *pvParameters );

TaskHandle_t xtask1 = NULL;
TaskHandle_t xtask2 = NULL;
TaskHandle_t xtask3 = NULL;

void setup() {
  Serial.begin(115200);
  Serial.println("Basic Demo - ESP32-Arduino-CAN");
  
  touch1begin = touchRead(T3);
  Serial.println(touch1begin);

  xTaskCreatePinnedToCore(
    task2
    ,  "task2"   // A name just for humans
    ,  1000  // 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.
    ,  &xtask2 
    ,  1);
  
  xTaskCreatePinnedToCore(
    task3
    ,  "task3"   // A name just for humans
    ,  2000  // 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.
    ,  &xtask3 
    ,  1);

  xTaskCreate(task1, "task1", 2000, NULL, 1, &xtask1 );
  //xTaskCreate(task2, "task2", 2000, NULL, 1, &xtask2 );
  //xTaskCreate(task3, "task3", 2000, NULL, 1, &xtask3 );
  
  vTaskSuspend(xtask2);
  vTaskSuspend(xtask3);

  xTaskNotifyGive( xtask1 );
}

void task1(void *pvParameters){
  
  while(1){
    if (ulTaskNotifyTake(pdTRUE, portMAX_DELAY) != 0){
      Serial.println("task1 entry");
      mode = 0;
    }
    vTaskSuspend(NULL);
  }
}

void task2(void *pvParameters){
  
  if (ulTaskNotifyTake(pdTRUE, portMAX_DELAY) != 0){
    Serial.println("task 2 entry");
    
    for(;;){
      Serial.println("task 2 repeat");
      vTaskDelay(pdMS_TO_TICKS( 200 ));
    }
    vTaskSuspend(NULL);
  }
}

void task3(void *pvParameters){
  
  if (ulTaskNotifyTake(pdTRUE, portMAX_DELAY) != 0){
    Serial.println("task 3 entry");
    
    for(;;){
      Serial.println("task 3 repeat");
      vTaskDelay(pdMS_TO_TICKS( 200 ));
    }
    vTaskSuspend(NULL);
  } 
}


void loop() {
  
  if (touchRead(T3) < (touch1begin-5)){
  touch1now = millis();
    if((touch1now - touch1last) > touchdebounce) {
      if ((touchRead(T3) < (touch1begin-5)) && touch1active == false && !longtouchactive) {
        touch1longtime = millis();
        touch1active = true;
        Serial.println("Button pressed");
      }
      touch1last = millis();
    }
  } else if (touch1active == true && !(touchRead(T3) < (touch1begin-5))){
    touch1active = false;
    longtouchactive = false;
    Serial.println("Button release");
    if (mode !=4 || mode !=5){
      if (mode == 0){
        Serial.println(mode);
        vTaskResume (xtask2);
        xTaskNotifyGive( xtask2 );
        mode=1;
      }else if (mode == 1){
        Serial.println(mode);
        vTaskSuspend (xtask2);
        vTaskResume (xtask3);
        xTaskNotifyGive( xtask3 );
        mode=2;
      }else if (mode == 2){
        Serial.println(mode);
        vTaskSuspend (xtask3);
        vTaskResume (xtask1);
        xTaskNotifyGive( xtask1 );
      }
    }
  }else if (longtouchactive == true && !(touchRead(T3) < (touch1begin-5))){
    longtouchactive = false;
  }
}

when you "suspend" a task, you stop it's execution at whatever point it was executing. When you later "resume" the task, it picks up from where it left of, it doesn't start at the beginning of the task.

both task2 and task3 have infinite loops() with their if statements. task1 is entirely an infinte loop.

because of the interior infinite loops in task2 and task3 their if conditions will never be re-executed.

Wow!

This literally destroyed a whole day of work, which is kind of good for the learning curve :frowning:

Is there any way to switch between tasks or to switch tasks "off" if not needed and switch them back "on" if needed?

what i'm trying to do is following:

start program:
page one on display appears showing task 1 (task 2 and task 3 not needed so can be "switched off")
button click
page two replaces page one and shows task 2 (task 1 and task 3 not needed so can be "switched off")
button click
page three replaces page two and shows task 3 (task 1 and task 2 not needed so can be "switched off")
button click
go back to page 1 (task 2 and task 3 not needed so can be "switched off")

and so on...

Thanks!

BR
RoB

a common approach is to use a semaphore. the task loops on the semaphore (e.g while (semaphore())).

the semaphore() blocks if not taken (not sure what the correct term is). so when the semaphore is taken it blocks, when it is free, it does't and allows the loop to execute. at the end of the loop, the semaphore may be taken.

some other task (or interrupt) frees the semaphore when there is something for the task to do.

#include "sdkconfig.h"
#include "esp32/ulp.h"
#include "driver/rtc_io.h"
#include "esp_system.h" //This inclusion configures the peripherals in the ESP system.
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/timers.h"
#include "freertos/event_groups.h"
#include <Adafruit_NeoPixel.h>
#include "AudioAnalyzer.h"
////
/* define event group and event bits */
EventGroupHandle_t eg;
#define evtDo_AudioReadFreq       ( 1 << 0 ) // 1
////
TickType_t xTicksToWait0 = 0;
////
QueueHandle_t xQ_LED_Info;
////
const int NeoPixelPin = 26;
const int LED_COUNT = 24; //total number of leds in the strip
const int NOISE = 10; // noise that you want to chop off
const int SEG = 6; // how many parts you want to separate the led strip into
const int Priority4 = 4;
const int TaskStack40K = 40000;
const int TaskCore1  = 1;
const int TaskCore0 = 0;
const int AudioSampleSize = 6;
const int Brightness = 180;
const int A_D_ConversionBits = 4096; // arduino use 1024, ESP32 use 4096
////
Analyzer Audio = Analyzer( 5, 15, 36 );//Strobe pin ->15  RST pin ->2 Analog Pin ->36
// When we setup the NeoPixel library, we tell it how many pixels, and which pin to use to send signals.
Adafruit_NeoPixel leds = Adafruit_NeoPixel( LED_COUNT, NeoPixelPin, NEO_GRB + NEO_KHZ800 );
////
int FreqVal[7];//create an array to store the value of different freq
////
void ULP_BLINK_RUN(uint32_t us);
////
void setup()
{
  ULP_BLINK_RUN(100000);
  eg = xEventGroupCreate();
  Audio.Init(); // start the audio analyzer
  leds.begin(); // Call this to start up the LED strip.
  clearLEDs();  // This function, defined below, de-energizes all LEDs...
  leds.show();  // ...but the LEDs don't actually update until you call this.
  ////
  xQ_LED_Info = xQueueCreate ( 1, sizeof(FreqVal) );
  //////////////////////////////////////////////////////////////////////////////////////////////
  xTaskCreatePinnedToCore( fDo_AudioReadFreq, "fDo_ AudioReadFreq", TaskStack40K, NULL, Priority4, NULL, TaskCore1 ); //assigned to core
  xTaskCreatePinnedToCore( fDo_LEDs, "fDo_ LEDs", TaskStack40K, NULL, Priority4, NULL, TaskCore0 ); //assigned to core
  xEventGroupSetBits( eg, evtDo_AudioReadFreq );
} // setup()
////
void loop() {} // void loop
////
void fDo_LEDs( void *pvParameters )
{
  int iFreqVal[7];
  int j;
  leds.setBrightness( Brightness ); //  1 = min brightness (off), 255 = max brightness.
  for (;;)
  {
    if (xQueueReceive( xQ_LED_Info, &iFreqVal,  portMAX_DELAY) == pdTRUE)
    {
      j = 0;
      //assign different values for different parts of the led strip
      for (j = 0; j < LED_COUNT; j++)
      {
        if ( (0 <= j) && (j < (LED_COUNT / SEG)) )
        {
          set(j, iFreqVal[0]); // set the color of led
        }
        else if ( ((LED_COUNT / SEG) <= j) && (j < (LED_COUNT / SEG * 2)) )
        {
          set(j, iFreqVal[1]); //orginal code
        }
        else if ( ((LED_COUNT / SEG * 2) <= j) && (j < (LED_COUNT / SEG * 3)) )
        {
          set(j, iFreqVal[2]);
        }
        else if ( ((LED_COUNT / SEG * 3) <= j) && (j < (LED_COUNT / SEG * 4)) )
        {
          set(j, iFreqVal[3]);
        }
        else if ( ((LED_COUNT / SEG * 4) <= j) && (j < (LED_COUNT / SEG * 5)) )
        {
          set(j, iFreqVal[4]);
        }
        else
        {
          set(j, iFreqVal[5]);
        }
      }
      leds.show();
    }
    xEventGroupSetBits( eg, evtDo_AudioReadFreq );
  }
  vTaskDelete( NULL );
} // void fDo_ LEDs( void *pvParameters )
////
void fDo_AudioReadFreq( void *pvParameters )
{
  int64_t EndTime = esp_timer_get_time();
  int64_t StartTime = esp_timer_get_time(); //gets time in uSeconds like Arduino Micros
  for (;;)
  {
    xEventGroupWaitBits (eg, evtDo_AudioReadFreq, pdTRUE, pdTRUE, portMAX_DELAY);
    EndTime = esp_timer_get_time() - StartTime;
    // log_i( "TimeSpentOnTasks: %d", EndTime );
    Audio.ReadFreq(FreqVal);
    for (int i = 0; i < 7; i++)
    {
      FreqVal[i] = constrain( FreqVal[i], NOISE, A_D_ConversionBits );
      FreqVal[i] = map( FreqVal[i], NOISE, A_D_ConversionBits, 0, 255 );
      // log_i( "Freq %d Value: %d", i, FreqVal[i]);//used for debugging and Freq choosing
    }
    xQueueSend( xQ_LED_Info, ( void * ) &FreqVal, xTicksToWait0 );
    StartTime = esp_timer_get_time();
  }
  vTaskDelete( NULL );
} // fDo_ AudioReadFreq( void *pvParameters )
////
//the following function set the led color based on its position and freq value
//
void set(byte position, int value)
{
  // segment 0, red
  if ( (0 <= position) && (position < LED_COUNT / SEG) ) // segment 0 (bottom to top), red
  {
    if ( value == 0 )
    {
      leds.setPixelColor( position, 0, 0, 0 );
    }
    else
    {
      // increase light output of a low number
      // value += 10;
      // value = constrain( value, 0, 255 ); // keep raised value within limits
      leds.setPixelColor( position, leds.Color( value , 0, 0) );
    }
  }
  else if ( (LED_COUNT / SEG <= position) && (position < LED_COUNT / SEG * 2) ) // segment 1 yellow
  {
    if ( value == 0 )
    {
      leds.setPixelColor(position, leds.Color(0, 0, 0));
    }
    else
    {
      leds.setPixelColor(position, leds.Color( value, value, 0)); // works better to make yellow
    }
  }
  else if ( (LED_COUNT / SEG * 2 <= position) && (position < LED_COUNT / SEG * 3) ) // segment 2 pink
  {
    if ( value == 0 )
    {
      leds.setPixelColor(position, leds.Color(0, 0, 0));
    }
    else
    {
      leds.setPixelColor(position, leds.Color( value, 0, value * .91) ); // pink
    }
  }
  else if ( (LED_COUNT / SEG * 3 <= position) && (position < LED_COUNT / SEG * 4) ) // seg 3, green
  {
    if ( value == 0 )
    {
      leds.setPixelColor(position, leds.Color( 0, 0, 0));
    }
    else //
    {
      leds.setPixelColor( position, leds.Color( 0, value, 0) ); //
    }
  }
  else if ( (LED_COUNT / SEG * 4 <= position) && (position < LED_COUNT / SEG * 5) ) // segment 4, leds.color( R, G, B ), blue
  {
    if ( value == 0 )
    {
      leds.setPixelColor(position, leds.Color( 0, 0, 0));
    }
    else //
    {
      leds.setPixelColor(position, leds.Color( 0, 0, value) ); // blue
    }
  }
  else // segment 5
  {
    if ( value == 0 )
    {
      leds.setPixelColor(position, leds.Color( 0, 0, 0)); // only helps a little bit in turning the leds off
    }
    else
    {
      leds.setPixelColor( position, leds.Color( value, value * .3, 0) ); // orange
    }
  }
} // void set(byte position, int value)
////
void clearLEDs()
{
  for (int i = 0; i < LED_COUNT; i++)
  {
    leds.setPixelColor(i, 0);
  }
} // void clearLEDs()
//////////////////////////////////////////////
/*
  Each I_XXX preprocessor define translates into a single 32-bit instruction. So you can count instructions to learn which memory address are used and where the free mem space starts.

  To generate branch instructions, special M_ preprocessor defines are used. M_LABEL define can be used to define a branch target.
  Implementation note: these M_ preprocessor defines will be translated into two ulp_insn_t values: one is a token value which contains label number, and the other is the actual instruction.

*/
void ULP_BLINK_RUN(uint32_t us)
{
  size_t load_addr = 0;
  RTC_SLOW_MEM[12] = 0;
  ulp_set_wakeup_period(0, us);
  const ulp_insn_t  ulp_blink[] =
  {
    I_MOVI(R3, 12),                         // #12 -> R3
    I_LD(R0, R3, 0),                        // R0 = RTC_SLOW_MEM[R3(#12)]
    M_BL(1, 1),                             // GOTO M_LABEL(1) IF R0 < 1
    I_WR_REG(RTC_GPIO_OUT_REG, 26, 27, 1),  // RTC_GPIO2 = 1
    I_SUBI(R0, R0, 1),                      // R0 = R0 - 1, R0 = 1, R0 = 0
    I_ST(R0, R3, 0),                        // RTC_SLOW_MEM[R3(#12)] = R0
    M_BX(2),                                // GOTO M_LABEL(2)
    M_LABEL(1),                             // M_LABEL(1)
    I_WR_REG(RTC_GPIO_OUT_REG, 26, 27, 0),// RTC_GPIO2 = 0
    I_ADDI(R0, R0, 1),                    // R0 = R0 + 1, R0 = 0, R0 = 1
    I_ST(R0, R3, 0),                      // RTC_SLOW_MEM[R3(#12)] = R0
    M_LABEL(2),                             // M_LABEL(2)
    I_HALT()                                // HALT COPROCESSOR
  };
  const gpio_num_t led_gpios[] =
  {
    GPIO_NUM_2,
    // GPIO_NUM_0,
    // GPIO_NUM_4
  };
  for (size_t i = 0; i < sizeof(led_gpios) / sizeof(led_gpios[0]); ++i) {
    rtc_gpio_init(led_gpios[i]);
    rtc_gpio_set_direction(led_gpios[i], RTC_GPIO_MODE_OUTPUT_ONLY);
    rtc_gpio_set_level(led_gpios[i], 0);
  }
  size_t size = sizeof(ulp_blink) / sizeof(ulp_insn_t);
  ulp_process_macros_and_load( load_addr, ulp_blink, &size);
  ulp_run( load_addr );
} // void ULP_BLINK_RUN(uint32_t us)
//////////////////////////////////////////////

Here

EventGroupHandle_t eg;
#define evtDo_AudioReadFreq       ( 1 << 0 ) // 1

I use an event group as one means of task control.

Here if (xQueueReceive( xQ_LED_Info, &iFreqVal,  portMAX_DELAY) == pdTRUE)
I use a message queue to control another task.

You are creating tasks with stack of only 2K. I recommend you use

 Serial.print(uxTaskGetStackHighWaterMark( NULL ));
        Serial.println();
        Serial.flush();

to get the stack used by the task and to default each task to 10K memory use instead of 2K on an ESP32.

the underlying mechanism to control the tasks is probably a semaphore.

but they need to be the condition of a while loop

while (xQueueReceive( xQ_LED_Info, &iFreqVal,  portMAX_DELAY) == pdTRUE)  {
    ...
}

Hi!

Thanks for the reply guys.
I had the time to look into it with following conclusion:

EventGropuHandle

EventGroupHandle is pretty neat if one wants to save a few lines of code, but otherwise it can be directly compare with xTaskNotify, at least for my application. However, the downside I recognized is:
If I do this:

...
for(;;){
   if (xEventGroupWaitBits (...)){
   do something...;
   }
}

the for loop is done only once upon receival of the correct event bit as the bit is set back immediately after. Ofcourse I could use pdFALSE and not set it back but how do I set it back if I want to change to another task.

But if i do the following:

...
if (xEventGroupWaitBits (...)){
   for(;;){
   do something...;
   }
}

upon receiving of the correct bit, the task will be running within the for loop indefinitely even if I change from task 1 to task 2. In other words, task1 never goes to blocked mode.

What I need is best of both: the task shall run indefinitely as long as I am within the task, but if I change the task (e.g. from task1 to task2), task1 shall get blocked (the for loop shall stop) and task 2 shall be running.

Queue

As far as I understood, the task waiting for the queue is blocked until there is something in the queue. When the queue is empty, it is blocked again, correct?
However, I just don't understand yet, how this would be applicable to my application with the buttons.

Semaphores

I looked briefly into the semaphores and it seems, that it is exactly what I need but I'm not sure I have understood correctly so I will try to explain in terms of my application from above:

Press of a button gives a semaphore to task 1. The way I understand it, is that if I want to change to task2 and deactivate (or block) task 1, i need to change the code of task 1 to give the semaphore to task 2 and the same for task 3. Is my assumption correct or is there a way to have a button press loop which is the main distributor of semaphore and takes it back from one task and gives it to another?

I will try semaphores during this weekend, but in the meantime I changed the code slightly to use EventGroupHandle and vTaskSuspend and vTaskResume which at least at the moment is working well. However, as I want to avoid any memory leaks, watchdog or stack issues, I will need to find a proper solution with (I think) semaphores.

As for Your comments, I have a few questions:

gcjr:
the underlying mechanism to control the tasks is probably a semaphore.

but they need to be the condition of a while loop

while (xQueueReceive( xQ_LED_Info, &iFreqVal,  portMAX_DELAY) == pdTRUE)  {

...
}




As far as I understood, this would be okay, if there is a constant stream of queue comming along, but in my case, after a push of a button, a simple "if" would do instead of the "while", correct?

Idahowalker:
You are creating tasks with stack of only 2K. I recommend you use

 Serial.print(uxTaskGetStackHighWaterMark( NULL ));

Serial.println();
        Serial.flush();



to get the stack used by the task and to default each task to 10K memory use instead of 2K on an ESP32.

I had a look at the high water mark and it seems that my tasks are consuming not more then 1K but, at some point after 300x switching between tasks, i receive a stack owerflow, hence why I used 2K.
What is the reason for the 10K if You could elaborate?
I know what it does but why is the "Serial.flush()" part important for the code above?

Thanks!

BR
RoB

Press of a button gives a semaphore to task 1. The way I understand it, is that if I want to change to task2 and deactivate (or block) task 1, i need to change the code of task 1 to give the semaphore to task 2 and the same for task 3. Is my assumption correct or is there a way to have a button press loop which is the main distributor of semaphore and takes it back from one task and gives it to another?

semaphores are a tool that needs to be used intelligently. they are intended to provide access to a shared resource by multiple entities. sometimes multiple resources are required and deadlocks can occur (Diner's Problem)

semaphores can be used to "block" a task, hence suspend it. while many tasks could be blocked by a single semaphore, but once a semaphore is freed, it's random which task asks for it first and runs. And of course many tasks can run simultaneously. In your case you're trying to block all but one.

so presumably, each task has an infinite loop (e.g. for(;:wink: or while(1)) that continually checks a semaphore, blocking if it is not available.

unfortunately i can't remember how exactly to do this and it depends on the semaphore implementation

you want the task to be blocked until there is something to do. it attempts to "take" a semaphore and is blocked because it is unavailable. A subsequent attempt to re-"take" would block the task until some other entity "gives" the semaphore (not sure of this). This concept works if you consider the task that blocks on a semaphore as a "consumer" and a "producer" "gives" the semaphore when something is ready to be consumed.

in your case, the top of the loop blocks on "taking" the semaphore and if it wants to continue, it must then "give" the semaphore. If you want to switch tasks, that task doesn't give the semaphore back, instead it gives to a different semaphore which a second task is waiting for.