Arduino ESP32 FreeRTOS 4: How to use Binary Semaphore - Mutex - Counting semaphore - Critical section for resources management

1. Introduction
- This tutorial introduces the ways to control the resources access among tasks in multitasking operating system.
2. Resources management
2.1 Binary semaphore
- This is the simplest way to control the access of resources that only have 2 states: locked/unlocked or unavailable/available.
- The task that want to gains the resource will call xSemaphoreTake(). There are 2 cases:
+ If it is successful to access the resource it will keep the resource until it call xSemaphoreGive() to release resource so that other tasks can gain it.
+ If it is failed it will wait until the resource is released by another task.
- Binary semaphore will be applied to interrupt (ISR) processing where the ISR callback function will call xSemaphoreGiveFromISR() to delegate the interrupt processing to the task that call xSemaphoreTake() (when xSemaphoreTake() is called, the task will move to Block state and waiting interrupt event).
Note:
- Binary semaphore that we use in this demo is a little bit different comparing to the standard theory above because the task that call xSemaphoreTake() will not release semaphore.
- API functions that are called from ISR callback must have prefix "FromISR" (xSemaphoreGiveFromISR). They are designed for Interrupt safe API functions.
2.1.1 Demo
- We will re-use this demo,  but using FreeRTOS style and Binary semaphore to process the interrupt.
2.1.2 Hardware
Figure: ESP32 connect to button (input_pullup) and LED
Connections:
[ESP32 GIO12 - BUTTON - GND]
[ESP32 GIO14 - LED - GND]
2.1.3 Software
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/* LED pin */
byte ledPin = 14;
/* pin that is attached to interrupt */
byte interruptPin = 12;
/* hold the state of LED when toggling */
volatile byte state = LOW;
SemaphoreHandle_t xBinarySemaphore;

void setup() {
  Serial.begin(115200);
  pinMode(ledPin, OUTPUT);
  /* set the interrupt pin as input pullup*/
  pinMode(interruptPin, INPUT_PULLUP);
  /* attach interrupt to the pin
  function blink will be invoked when interrupt occurs
  interrupt occurs whenever the pin change value */
  attachInterrupt(digitalPinToInterrupt(interruptPin), ISRcallback, CHANGE);
  /* initialize binary semaphore */
  xBinarySemaphore = xSemaphoreCreateBinary();
  /* this task will process the interrupt event 
  which is forwarded by interrupt callback function */
  xTaskCreate(
    ISRprocessing,           /* Task function. */
    "ISRprocessing",        /* name of task. */
    1000,                    /* Stack size of task */
    NULL,                     /* parameter of the task */
    4,                        /* priority of the task */
    NULL);  
}

void loop() {
}

/* interrupt function callback */
void ISRcallback() {
  /* */
  BaseType_t xHigherPriorityTaskWoken = pdFALSE;
  /* un-block the interrupt processing task now */
  xSemaphoreGiveFromISR( xBinarySemaphore, &xHigherPriorityTaskWoken );
}

/* this function will be invoked when additionalTask was created */
void ISRprocessing( void * parameter )
{
  Serial.println((char *)parameter);
  /* loop forever */
  for(;;){
    /* task move to Block state to wait for interrupt event */
    xSemaphoreTake( xBinarySemaphore, portMAX_DELAY );
    Serial.println("ISRprocessing is running");
    /* toggle the LED now */
    state = !state;
    digitalWrite(ledPin, state);
  }
  vTaskDelete( NULL );
}
2.2 Mutex
It is like a key that associated with the resource. The task holds the key, will lock the resource, process it then unlock and give back the key so that other tasks can use it. This mechanism is similar to binary semaphore except that the task that take the key have to release the key.
Note: Suppose that we have 2 tasks: the low priority task and the high priority task. These tasks are waiting for the key and the low priority task has chance to hold the key then it will block the high priority task and continue executing.
2.2.1 Demo
- In this demo, we create 2 tasks: low priority task and high priority task. The low priority task will hold the key and block the high priority task.
2.2.2 Hardware
No extra hardware
2.2.3 Software
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
SemaphoreHandle_t xMutex;
void setup() {
  Serial.begin(112500);
  /* create Mutex */
  xMutex = xSemaphoreCreateMutex();
  
  xTaskCreate(
      lowPriorityTask,           /* Task function. */
      "lowPriorityTask",        /* name of task. */
      1000,                    /* Stack size of task */
      NULL,                     /* parameter of the task */
      1,                        /* priority of the task */
      NULL);                    /* Task handle to keep track of created task */
  delay(500);
  /* let lowPriorityTask run first then create highPriorityTask */
  xTaskCreate(
      highPriorityTask,           /* Task function. */
      "highPriorityTask",        /* name of task. */
      1000,                    /* Stack size of task */
      NULL,                     /* parameter of the task */
      4,                        /* priority of the task */
      NULL);                    /* Task handle to keep track of created task */
}

void loop() {

}
void lowPriorityTask( void * parameter )
{
  Serial.println((char *)parameter);
  for(;;){
    Serial.println("lowPriorityTask gains key");
    xSemaphoreTake( xMutex, portMAX_DELAY );
    /* even low priority task delay high priority 
    still in Block state */
    delay(2000);
    Serial.println("lowPriorityTask releases key");
    xSemaphoreGive( xMutex );
  }
  vTaskDelete( NULL );
}

void highPriorityTask( void * parameter )
{
  Serial.println((char *)parameter);
  for(;;){
    Serial.println("highPriorityTask gains key");
    /* highPriorityTask wait until lowPriorityTask release key */
    xSemaphoreTake( xMutex, portMAX_DELAY );
    Serial.println("highPriorityTask is running");
    Serial.println("highPriorityTask releases key");
    xSemaphoreGive( xMutex );
    /* delay so that lowPriorityTask has chance to run */
    delay(1000);
  }
  vTaskDelete( NULL );
}
2.2.4 Result
 Figure: Mutex demo

2.3 Counting Semaphore
- In previous demo, we use binary semaphore to delegate interrupt processing to a task instead of processing it directly in interrupt callback function. In case the task is not finished processing the interrupt event but 2 interrupts occur then we will lose 1 interrupt event because binary semaphore only store one event (binary semaphore is like a queue with size is 1).
- In order to overcome this, we will use counting semaphore where it can store more than 1 events.
2.3.1 Demo
- We will re-use the Demo 2.1.1 in ISR callback function we call xSemaphoreGiveFromISR() more than once (6 times), then we will see the task that process the interrupt event will blink LED 3 times before moving to Block state to wait for next interrupt.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/* LED pin */
byte ledPin = 14;
/* pin that is attached to interrupt */
byte interruptPin = 12;
/* hold the state of LED when toggling */
volatile byte state = LOW;
SemaphoreHandle_t xCountingSemaphore;

void setup() {
  Serial.begin(115200);
  pinMode(ledPin, OUTPUT);
  /* set the interrupt pin as input pullup*/
  pinMode(interruptPin, INPUT_PULLUP);
  /* attach interrupt to the pin
  function blink will be invoked when interrupt occurs
  interrupt occurs whenever the pin rising value */
  attachInterrupt(digitalPinToInterrupt(interruptPin), ISRcallback, RISING);
  /* initialize counting semaphore that can store 10 events */
  xCountingSemaphore = xSemaphoreCreateCounting( 10, 0 );
  /* this task will process the interrupt event 
  which is forwarded by interrupt callback function */
  xTaskCreate(
    ISRprocessing,           /* Task function. */
    "ISRprocessing",        /* name of task. */
    1000,                    /* Stack size of task */
    NULL,                     /* parameter of the task */
    4,                        /* priority of the task */
    NULL);  
}

void loop() {
}

/* interrupt function callback */
void ISRcallback() {
  /* */
  BaseType_t xHigherPriorityTaskWoken = pdFALSE;
  /* un-block the interrupt processing task now */
  /* each couple is a blinky cycle 
  we bink 3 times then call this 6 times */
  xSemaphoreGiveFromISR( xCountingSemaphore, &xHigherPriorityTaskWoken );
  xSemaphoreGiveFromISR( xCountingSemaphore, &xHigherPriorityTaskWoken );
  xSemaphoreGiveFromISR( xCountingSemaphore, &xHigherPriorityTaskWoken );
  xSemaphoreGiveFromISR( xCountingSemaphore, &xHigherPriorityTaskWoken );
  xSemaphoreGiveFromISR( xCountingSemaphore, &xHigherPriorityTaskWoken );
  xSemaphoreGiveFromISR( xCountingSemaphore, &xHigherPriorityTaskWoken );
}

/* this function will be invoked when additionalTask was created */
void ISRprocessing( void * parameter )
{
  Serial.println((char *)parameter);
  /* loop forever */
  for(;;){
    /* task move to Block state to wait for interrupt event */
    xSemaphoreTake( xCountingSemaphore, portMAX_DELAY );
    Serial.println("ISRprocessing is running");
    /* toggle the LED now */
    state = !state;
    digitalWrite(ledPin, state);
    /* elay here to see LED blinky */
    delay(1000);
  }
  vTaskDelete( NULL );
}
2.3.2 Result
Note: if you see LED blink more than 3 times, it is caused by the noise when pressing button, you may use the capacitor to filter this noise.
2.4 Critical section
- A critical section is a region of code that need to be protected from any concurrent accesses to change it. The critical section must keep be short because in the critical section most of operations are suspended. If this section is long the suspension time is long too. It affect the behavior of the system.
- In order to implement critical section, there are 2 ways:
+ Call taskENTER_CRITICAL() before enter critical section and call taskEXIT_CRITICAL() after leaving it. Using these function we will disable most of interrupts.
+ Call vTaskSuspendAll() before enter critical section and call xTaskResumeAll() after leaving it. Using these functions we still keep interrupts.
Note: in general the watch dog timer willl be feed by RTOS but we disable scheduler so we have to feed it.
2.4.1 Demo
- In this demo, we create 2 tasks: low priority task and high priority task. The low priority task will go to the critical section and increase a counter by 1 until it reaches 1000. When low priority task leave critical section, we check the counter is 1000 from high priority task. It proves that high priority task is suspended.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include "esp_task_wdt.h"

int count = 0;
portMUX_TYPE mmux = portMUX_INITIALIZER_UNLOCKED;

void setup() {
  Serial.begin(112500);
  /* create Mutex */
  xTaskCreate(
      lowPriorityTask,           /* Task function. */
      "lowPriorityTask",        /* name of task. */
      10000,                    /* Stack size of task */
      NULL,                     /* parameter of the task */
      1,                        /* priority of the task */
      NULL);                    /* Task handle to keep track of created task */
  /* let lowPriorityTask run first */ 
  delay(500);
  /* let lowPriorityTask run first then create highPriorityTask */
  xTaskCreate(
      highPriorityTask,           /* Task function. */
      "highPriorityTask",        /* name of task. */
      10000,                    /* Stack size of task */
      NULL,                     /* parameter of the task */
      4,                        /* priority of the task */
      NULL);                    /* Task handle to keep track of created task */
}

void loop() {

}
void lowPriorityTask( void * parameter )
{
  for(;;){

    Serial.println("lowPriorityTask lock section");
    /* if using shceduler stopping way then 
    un-comment/comments lines below accordingly */
    //vTaskSuspendAll();
    taskENTER_CRITICAL(&mmux);
    /* stop scheduler until this loop is finished */
    for(count=0; count<1000; count++){
      /* in general the watch dog timer willl be feed by RTOS 
      but we disable scheduler so we have to feed it */
      esp_task_wdt_feed();
    }
    /* we resume the scheduler so highPriorityTask will run again */
   // xTaskResumeAll();
   taskEXIT_CRITICAL(&mmux);
    Serial.println("lowPriorityTask leave section");

  }
  vTaskDelete( NULL );
}

void highPriorityTask( void * parameter )
{
  for(;;){
    /* highPriorityTask is resumed, we will check the counter should be 1000 */
    Serial.print("highPriorityTask is running and count is ");
    Serial.println(count);
    /* delay so that lowPriorityTask has chance to run */
    delay(50);
  }
  vTaskDelete( NULL );
}
2.4.2 Result
Figure: critical section demo

Post a Comment

1 Comments

Dorian McCarthy said…
Putting the term 'FromISR' at the end is referred to as a suffix, rather than a prefix.