Użycie semaforów w systemie freeRTOS

W ostatnim artykule przedstawiłem pokrótce czym są systemy operacyjne czasu rzeczywistego na przykładzie freeRTOSa dla Arduino, przedstawiłem z grubsza ich wady i zalety oraz przeanalizowałem przykładowy program z dwoma wątkami działającymi współbieżnie. Jedną z wad, które wymieniłem jest to, że niektóre wątki mogą w jednej chwili chcieć korzystać z tego samego zasobu (np. portu szeregowego). W dzisiejszym artykule pokażę Wam jeden ze sposobów kontroli zasobów we freeRTOS.

Semafory

Semafory to nic innego jak flagi oznaczające czy dany zasób może być w tej chwili użyty. Nazwa jest nieprzypadkowa i można bez problemu zauważyć tutja pdoobieństwo do dwukolorowych semaforów, które znajdują się np. na skrzyżowaniach przy przejściach dla pieszych. W tym przypadku droga na któej znajduje się przejście dla pieszych będzie zasobem, który chcemy uzyskać. Piesi oraz samochody (zbiorczo) będą wątkami, które chcą z danego zasobu skorzystać. I tak kiedy wątek pieszych widzi sygnał czerwony oznacza to, że musi poczekać, gdyż droga (zasób) jest aktualnie używana przez inny wątek (samochodów). Kiedy samochody przestaną używać jezdni piesi otrzymują sygnał zielony i teraz to oni mogą korzystać na wyłączność z danego zasobu. Oczywiście jest to dość uproszczony przykład, gdyż sygnalizacja na drodze zmienia się automatycznie, a w przypadku wielozadaniowości każdy wątek może wziąć dla siebie dany zasób, jeżeli ten jest wolny. Nieco lepszym przykładem byłoby tutaj skrzyżowanie z pętlami indukcyjnymi oraz przyciskami dla pieszych ;)

Obsługa semaforów we freeRTOS

Na naszym podstawowym poziomie wystarczą nam do zapamiętania 3 funkcje systemu, a są to:

  • xSemaphoreCreateMutex
  • xSemaphoreGive
  • xSemaphoreTake

Służą one kolejno do tworzenia semaforu, zwalniania zasobu oraz blokowania zasobu.

Przykład

Aby zaprezentwać działanie semaforów otwórzmy przykład z menu PlikPrzykładyfreeRTOSAnalogRead_DigitalRead.

Zanim zaczniemy analizować kod dokonajmy paru drobnych zmian.

W miejscu wywołania obu funkcji xTaskCreate ustawmy priorytety obu zadań na identyczne (np. 1). Pozwoli to później zobrazować jak wątki z tym samym priorytetem korzystają z zasobu kiedy nie ma nad nim żadnej kontroli.

W funkcjach TaskDigitalRead oraz TaskAnalogRead zaraz przed wypisaniem na port szeregowy wartości buttonState i sensorValue dopiszmy taki kod:

Serial.println("To jest wykonanie zadania TaskDigitalRead:");
Serial.println(buttonState);

[...]

Serial.println("To jest wykonanie zadania TaskAnalogRead:");
Serial.println(sensorValue);

Dzięki temu wątki będą miały więcej do zrobienia co wydłuży ich czas pracy, tym samym pozwalając planiście systemowemu na ich wywłaszczenie.

Analiza

Pierwszą ważną rzeczą, którą musimy uwzględnić w kodzie jest dołączenie biblioteki do obsługi flag:

#include <semphr.h>

Teraz możemy zadelkarować globalną zmienną, która będzie reprezentowała nasz semafor.

SemaphoreHandle_t xSerialSemaphore;

Sama deklaracja nic nam jednak nie da. Musimy ten semafor zainicjować, a robimy to w funkcji setup:

if ( xSerialSemaphore == NULL )  // Check to confirm that the Serial Semaphore has not already been created.
{
  xSerialSemaphore = xSemaphoreCreateMutex();  // Create a mutex semaphore we will use to manage the Serial Port
  if ( ( xSerialSemaphore ) != NULL )
    xSemaphoreGive( ( xSerialSemaphore ) );  // Make the Serial Port available for use, by "Giving" the Semaphore.
}

W funkcjach TaskDigitalRead oraz TaskAnalogRead używana jest następująca konstrukcja:

if ( xSemaphoreTake( xSerialSemaphore, ( TickType_t ) 5 ) == pdTRUE )
{
  // tutaj korzystamy z zasobu
  xSemaphoreGive( xSerialSemaphore );
}

Jak widać za pomocą funkcji xSemaphoreTake rezerwujemy sobie dostęp do danego zasobu. Funkcja ta zwraca nam też informację, czy udało nam się uzyskać dostęp do tego zasobu. Pierwszy parametr tej funkcji to semafor, który chcemy zablokować, drugi natomiast jest czasem jaki możemy poświęcić na czekanie aż semafor zostanie udostępniony (wyrażony w czasie przydzielanym jednorazowo dla jednego wątku).

Kiedy zakończymy pracę z danym zasobem musimy odblokować semafor za pomocą funkcji xSemaphoreGive przekazując dany semafor jako parametr.

Po skompilowaniu kodu i wgraniu na płytkę zobaczymy, że wyniki uładają się ładnie jeden pod drugim:

[...]
To jest wykonanie zadania TaskAnalogRead:
464
To jest wykonanie zadania TaskDigitalRead:
0
To jest wykonanie zadania TaskAnalogRead:
460
To jest wykonanie zadania TaskDigitalRead:
0
To jest wykonanie zadania TaskAnalogRead:
460
To jest wykonanie zadania TaskDigitalRead:
0
To jest wykonanie zadania TaskAnalogRead:
460
To jest wykonanie zadania TaskDigitalRead:
0
To jest wykonanie zadania TaskAnalogRead:
465
To jest wykonanie zadania TaskDigitalRead:
[...]

 

Usuwamy semafory

Żeby zobaczyć co się stanie jakby nie było semaforów wystarczy, że zakomentujemy lub usuniemy linijki odpowiedzialne za zarządzanie semaforami z kodu wątków:

void TaskDigitalRead( void *pvParameters __attribute__((unused)) )  // This is a Task.
{
  /*
    DigitalReadSerial
    Reads a digital input on pin 2, prints the result to the serial monitor

    This example code is in the public domain.
  */

  // digital pin 2 has a pushbutton attached to it. Give it a name:
  uint8_t pushButton = 2;

  // make the pushbutton's pin an input:
  pinMode(pushButton, INPUT);

  for (;;) // A Task shall never return or exit.
  {
    // read the input pin:
    int buttonState = digitalRead(pushButton);

    // See if we can obtain or "Take" the Serial Semaphore.
    // If the semaphore is not available, wait 5 ticks of the Scheduler to see if it becomes free.
//    if ( xSemaphoreTake( xSerialSemaphore, ( TickType_t ) 5 ) == pdTRUE )
//    {
      // We were able to obtain or "Take" the semaphore and can now access the shared resource.
      // We want to have the Serial Port for us alone, as it takes some time to print,
      // so we don't want it getting stolen during the middle of a conversion.
      // print out the state of the button:
      Serial.println("To jest wykonanie zadania TaskDigitalRead:");
      Serial.println(buttonState);

//      xSemaphoreGive( xSerialSemaphore ); // Now free or "Give" the Serial Port for others.
//    }

    vTaskDelay(1);  // one tick delay (15ms) in between reads for stability
  }
}

void TaskAnalogRead( void *pvParameters __attribute__((unused)) )  // This is a Task.
{

  for (;;)
  {
    // read the input on analog pin 0:
    int sensorValue = analogRead(A0);

    // See if we can obtain or "Take" the Serial Semaphore.
    // If the semaphore is not available, wait 5 ticks of the Scheduler to see if it becomes free.
//    if ( xSemaphoreTake( xSerialSemaphore, ( TickType_t ) 5 ) == pdTRUE )
//    {
      // We were able to obtain or "Take" the semaphore and can now access the shared resource.
      // We want to have the Serial Port for us alone, as it takes some time to print,
      // so we don't want it getting stolen during the middle of a conversion.
      // print out the value you read:
      Serial.println("To jest wykonanie zadania TaskAnalogRead:");
      Serial.println(sensorValue);

//      xSemaphoreGive( xSerialSemaphore ); // Now free or "Give" the Serial Port for others.
//    }

    vTaskDelay(1);  // one tick delay (15ms) in between reads for stability
  }
}

Po wgraniu takiego programu wynik jego działania wygląda u mnie tak:

[...]
eg zadania TaskAnTlogRead:
936
e zadania TaskDTgitalRead:
0
e zadania TaskAnTlogRead:
936
e zadania TaskDiTitalRead:
0
eg zadania TaskAnTlogRead:
935
[...]