Semaphore problems in FreeRTOS+ESP32

Good afternoon everyone! I really hope you can help me out again!

I am using ESP32 and FreeRTOS. I have a task to improve the existing code to track possible pump failure, clogged pipelines, or microcontroller freezes. Let's get to the point.

Tasks 1 and 2 are located on different ESP cores and work in parallel. At startup, Task2 must check the upper liquid level and give permission to start Task1, that is, to turn on the pump, then Task2 monitors the critical liquid level in the container and, if this level is reached, will turn off the pump. After the pump is turned off, Task1 stops Task2 and they wait together for 10 seconds. After the time has elapsed, Task1 resumes Task2 and waits for it to start, this is repeated cyclically.

Using the capabilities of FreeRTOS, I would like to use a binary semaphore to track the emergency modes.
That is, when starting up, Task2 takes the semaphore, and if the level is normal, releases it for a short period of time so that Task1 can take it and start the pump, and then return the semaphore to Task2 so that it starts tracking the lower critical liquid level (distance_cm > 31.50).

Now, if Task2 does not return the semaphore for some time, then the liquid level is not normal else if (flag == true && distance_cm > 10.5), then some kind of accident has occurred and, accordingly, Task1 will report that it could not take the semaphore (since Task2 did not release it).

But the first time you run the ESP, you will see the following:

That is, Task1 starts first for some reason, without waiting for permission, which should not be the case since distance_cm = 20. However, during subsequent starts, Task2 still prevails and does not release the semaphore, preventing the Task1 pump from turning on.

How to make it so that even at the first startup, if the level is not normal, Task1 with the pump does not start, and signals if necessary "-----TASK 2 ERROR -----!!!!"?

I really hope for your help! Thank you for your time!

#include <EEPROM.h>

TaskHandle_t Task1Handle;
TaskHandle_t Task2Handle;

SemaphoreHandle_t xSemaphore = NULL;  

void Task1code(void *pvParameters);
void Task2code(void *pvParameters);

float distance_cm = 20; //The distance was measured by my sensor.
//If it is < 10.5 at the first start, then the pump is allowed to switch on, if it is > 10.5,
//then you need to wait up to 10 seconds, if during this time the water has
//not reached the required level (< 10.5), then an error message from TASK1
//(‘-----TASK 2 ERROR -----!!!!’) occurs, because TASK1 could not pick up the semaphore

bool flag = true;  //the flag that restores the TASK2 operation

void setup() 
{
  Serial.begin(9600);

  xSemaphore = xSemaphoreCreateBinary();  // Set the semaphore as binary

  xTaskCreatePinnedToCore(
    Task1code,    
    "Task1",      
    1024,         
    NULL,         
    2,            
    &Task1Handle, 
    0);           

  delay(500);

  xTaskCreatePinnedToCore(
    Task2code,   
    "Task2",      
    1024,         
    NULL,         
    2,           
    &Task2Handle, 
    1);           

  delay(500);
}

void Task1code(void *pvParameters) 
{
  for (;;)
  {
    xSemaphoreGive(xSemaphore);  
    vTaskDelay(200 / portTICK_PERIOD_MS);

    if (!flag) 
    {
      flag = true;
      vTaskResume(Task2Handle);
      Serial.println("   6)   Task2->Resume");
    }

    if (xSemaphoreTake(xSemaphore, (10000 * portTICK_PERIOD_MS))  == pdTRUE) 
    {
      vTaskDelay(50 / portTICK_PERIOD_MS);
      xSemaphoreGive(xSemaphore);  

      Serial.println("   2)   PUMP ___ON___");
      vTaskDelay(10000 / portTICK_PERIOD_MS); 
      Serial.println("   3)   PUMP !!!OFF!!!");
    
      vTaskSuspend(Task2Handle);
      Serial.println("   4)   Task2  ||  Suspend");
      
      vTaskDelay(10000 / portTICK_PERIOD_MS); 
    } 
    else 
    {
      Serial.println(" ");
      Serial.println(" ");
      Serial.println("-----TASK   2    ERROR -----!!!!");
      Serial.println(" ");
      Serial.println(" ");
      vTaskSuspend(Task2Handle);
      vTaskDelay(20000 / portTICK_PERIOD_MS);
    }
  }
}

void Task2code(void *pvParameters) 
{
  for (;;) 
  {
    Serial.println("TASK 2 TAKE SEMAPHORE");

    if (xSemaphoreTake(xSemaphore, (10000 * portTICK_PERIOD_MS)) == pdTRUE) 
    {
      Serial.println("MEASUREMENT LEVEL");
      vTaskDelay(350 / portTICK_PERIOD_MS);

      if (flag == true && distance_cm <= 10.5) 
      {
     
        Serial.println("   1)__________________________The upper level is normal_______________________");
        flag = false;
        xSemaphoreGive(xSemaphore);  // Release the semaphore
         vTaskDelay(200 / portTICK_PERIOD_MS);
        
      }

      else if (flag == true && distance_cm > 10.5) 
      {
        //If you are in this block, we will not return the semaphore until the level is normalised. If it does not return to normal, something bad has happened
        Serial.println("                       RESOLUTION FALSE");
        flag = false;
        vTaskDelay(5000 / portTICK_PERIOD_MS);
      }

      if (distance_cm > 31.50) 
      {
        Serial.println("-   -   -   -   -   -   -   -   -Emergency shutdown of the pump-   -   -   -   -   -   -   -   -  ");
        Serial.println("PUMP !!!OFF!!!");
        vTaskDelay(10000 / portTICK_PERIOD_MS);
      }

      Serial.print("   Distance cm:");
      Serial.println(distance_cm);
    } 
    else 
    {
      Serial.println(" ");
      Serial.println(" ");
      Serial.println("-----TASK   _1_    ERROR -----!!!!");
      Serial.println(" ");
      Serial.println(" ");
      vTaskDelay(20000 / portTICK_PERIOD_MS);
    }
  }
}


void loop() {
}

Start with only one task active and leave the other one suspended.

A single task without semaphores were easier to master. The use of flag is unclear and the distance never is updated..

1 Like

xSemaphoreTake
and
xSemaphoreGive

must always be called in pairs - within the same task!, first xSemaphoreTake that seizes the semaphore and then xSemaphoreGive that releases it. Between these two calls no other task can seize this semaphore.

Looking at Task1 code this is not the case (starts with xSemaphoreGive).

Also the following code makes no sense to me (why must it be executed while holding the semaphore?):

if (xSemaphoreTake(xSemaphore, (10000 * portTICK_PERIOD_MS)) == pdTRUE) { vTaskDelay(50 / portTICK_PERIOD_MS); xSemaphoreGive(xSemaphore);

1 Like

That's for mutexes, not so with semaphores.

1 Like

It seems like you've come up with a convoluted, overly-complicated way of implementing trivially simple functionality. This doesn't warrant using two tasks let alone two tasks each on their own core.

1 Like

I'm sorry, my mistake.

I always used a Read-Modify-Write instruction on 68K hardware. I do not know if that is available on the Arduino. There is also Atomic, not sure of its implementation on the Arduino.

RISC architecture does not normally support such instructions.

1 Like

That is an advantage of the CISC. Here is some good reading on the pseudo RISC implementation. https://five-embeddev.com/riscv-user-isa-manual/Priv-v1.12/a.html

std::atomic from the C++ STL is available on ESP32.

Hi! The flag is used to stop and resume Task2, because after the pump is turned off, there is no need to measure the level until the pump starts again. With this flag I synchronized both Task1,2 tasks and stop Task2 when it is not needed. I had to use this flag because of the logic of the operating system itself, I tried to do the code as follows, but it causes the ESP to restart, so I solved this issue with a kind of "crutch".

void Task1code(void *pvParameters) 
{
  for (;;)
  {

     vTaskResume(Task2Handle);

    xSemaphoreGive(xSemaphore);  
    vTaskDelay(200 / portTICK_PERIOD_MS);

    if (xSemaphoreTake(xSemaphore, (10000 * portTICK_PERIOD_MS))  == pdTRUE) 
    {
     
      vTaskDelay(50 / portTICK_PERIOD_MS);
      xSemaphoreGive(xSemaphore);  

      Serial.println("   2)   PUMP ___ON___");
      vTaskDelay(10000 / portTICK_PERIOD_MS); 
      Serial.println("   3)   PUMP !!!OFF!!!");
    
      vTaskSuspend(Task2Handle);
      Serial.println("   4)   Task2  ||  Suspend");
      
      vTaskDelay(10000 / portTICK_PERIOD_MS); 
    } 
  }
}

As for the distance. I've specifically removed all the unnecessary to make it clearer to you what I'm talking about, so I've removed all the logic for measuring distance, and I'm still testing the distance change in manual mode by entering the value .

See, before starting the pump, I need to check if the upper level is normal, and if so, Task2 releases the binary semaphore for a while so that Task1 can take it and start the pump. After the pump is turned on, I need to constantly monitor the lower critical level, so to start monitoring it, I need to release the semaphore of Task1 so that Task2 can pick it up again and start checking the lower critical level.

The lower critical level check is performed here:

*Those checks that are performed with flag in the if() body condition perform the upper level check once when the pump starts. Then they are not active during the critical level control.

So the algorithm turned out to be complicated. I would like to use semaphores to increase reliability. It seemed to me that in this way I could track both program hangs on one of the cores and, most importantly, if the semaphore was not released by Task2 (the upper liquid level is not normal and is more than 10.5), then the water did not return to the tank and, accordingly, the pipeline is clogged. So then the else body is triggered, which now simply notifies -----TASK 2 ERROR -----!!!!, and in the future will send a notification of a breakdown to the server.

Thank you very much for your help . :wink:

I will probably try to implement it in one Task.

Be sure to write, once again thank you very much!

Hi, I have solved the issue. According to the semaphore's logic, you must first give it to take it. So in Task2, I give it away or "create" it so that Task1 can take it. If the level is not normal, the semaphore is not given and the timer in Task1 will be triggered, which will lead to the execution of the else body. Here's an excerpt from the ESP documentation


on the semaphore section.

#include <EEPROM.h>

TaskHandle_t Task1Handle;
TaskHandle_t Task2Handle;

SemaphoreHandle_t xSemaphore = NULL;  

void Task1code(void *pvParameters);
void Task2code(void *pvParameters);

float distance_cm = 10; //The distance was measured by my sensor.
//If it is < 10.5 at the first start, then the pump is allowed to switch on, if it is > 10.5,
//then you need to wait up to 10 seconds, if during this time the water has
//not reached the required level (< 10.5), then an error message from TASK1
//(‘-----TASK 2 ERROR -----!!!!’) occurs, because TASK1 could not pick up the semaphore

bool flag = true;  //the flag that restores the TASK2 operation

void setup() 
{
  Serial.begin(9600);

  xSemaphore = xSemaphoreCreateBinary();  // Set the semaphore as binary

  xTaskCreatePinnedToCore(
    Task1code,    
    "Task1",      
    1024,         
    NULL,         
    2,            
    &Task1Handle, 
    1);           

  delay(500);

  xTaskCreatePinnedToCore(
    Task2code,   
    "Task2",      
    1024,         
    NULL,         
    2,           
    &Task2Handle, 
    1);           

  delay(500);
}

void Task1code(void *pvParameters) 
{
  for (;;)
  {
    //xSemaphoreGive(xSemaphore);  
    //vTaskDelay(200 / portTICK_PERIOD_MS);

    if (!flag) 
    {
      flag = true;
      vTaskResume(Task2Handle);
      Serial.println("   6)   Task2->Resume");
    }

    if (xSemaphoreTake(xSemaphore, (10000 * portTICK_PERIOD_MS))  == pdTRUE) 
    {
      Serial.println("   2)   PUMP ___ON___");
      vTaskDelay(10000 / portTICK_PERIOD_MS); 
      Serial.println("   3)   PUMP !!!OFF!!!");
    
      vTaskSuspend(Task2Handle);
      Serial.println("   4)   Task2  ||  Suspend");
      distance_cm = 20;
      vTaskDelay(10000 / portTICK_PERIOD_MS); 
    } 
    else 
    {
      Serial.println(" ");
      Serial.println(" ");
      Serial.println("-----TASK   2    ERROR -----!!!!");
      Serial.println(" ");
      Serial.println(" ");
      vTaskSuspend(Task2Handle);
      vTaskDelay(20000 / portTICK_PERIOD_MS);
    }
  }
}

void Task2code(void *pvParameters) 
{
  for (;;) 
  {
      Serial.println("MEASUREMENT LEVEL");
      vTaskDelay(350 / portTICK_PERIOD_MS);

      if (flag == true && distance_cm <= 10.5) 
      {
        Serial.println("   1)__________________________The upper level is normal_______________________");
        flag = false;
        xSemaphoreGive(xSemaphore);  // Release the semaphore   
      }

      else if (flag == true && distance_cm > 10.5) 
      {
        //If you are in this block, we will not return the semaphore until the level is normalised. If it does not return to normal, something bad has happened
        Serial.println("                       RESOLUTION FALSE");
        flag = false;
        vTaskDelay(5000 / portTICK_PERIOD_MS);
      }

      if (distance_cm > 31.50) 
      {
        Serial.println("-   -   -   -   -   -   -   -   -Emergency shutdown of the pump-   -   -   -   -   -   -   -   -  ");
        Serial.println("PUMP !!!OFF!!!");
        vTaskDelay(10000 / portTICK_PERIOD_MS);
      }

      Serial.print("   Distance cm:");
      Serial.println(distance_cm);
  }
}


void loop() {
}
1 Like

same conclusion after some experimenting trying to run 3 tasks.

Although I'll go a bit further and state; Calling just xSemaphoreCreateBinary() isn't enough, you must immediately release the created semaphore by using xSemaphoreGive() right after invoking create binary.

I had working semaphores after doing this. Different configurations might give different results.

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