Kurs FreeRTOS #5: semafory

freertos semafory

Dzisiaj zajmiemy się czymś zupełnie nowym – semaforami! 🙂 Możliwe, że już kiedyś obiło Ci się o uszy takie pojęcie. Jeśli tak to nic dziwnego. Semafory nie są narzędziem dostępnym tylko we FreeRTOSie. Znajdziesz je w każdym wielozadaniowym systemie operacyjnym. Potwierdza się to o czym mówiłem w pierwszym poście z tej serii – zdobytą w tym kursie wiedzę będziesz mógł łatwo przenieść na inne systemy, ponieważ wykorzystywane są w nich takie same mechanizmy 🙂 Dzisiejszy post ma rolę wprowadzającą do semaforów, rozwijany będzie jeszcze w kolejnych wpisach przy okazji opisywania innych zagadnień.

Czym są semafory?

Semafory są obiektami, które umożliwiają kontrolowanie dostępu do współdzielonych zasobów między zadaniami oraz synchronizację zadań lub zadań i przerwań. Wyróżnić można dwa podstawowe typy semaforów: binarne i zliczające. W tym artykule wykorzystamy wyłącznie semafory binarne i zajmiemy się realizacją synchronizacji zadań. Do pozostałych zagadnień przejdziemy w kolejnych wpisach.

Semafory binarne

Jak można się domyślić, semafor binarny może przyjmować dwa stany: dostępny (1) oraz niedostępny (0). Dzięki mechanizmom udostępnionym przez system istnieje możliwość uzależnienia stanu zadania od stanu semafora. W przypadku braku semafora zadanie jest w stanie zablokowanym, natomiast gdy semafor się pojawi to przypisane mu zadanie przechodzi natychmiast w stan gotowości i jeśli będzie miało wyższy priorytet od aktualnie wykonywanego to dojdzie do jego wywłaszczenia. Istnieją dwie podstawowe czynności związane z semaforami: V oraz P. Są one często różnie nazywane, jednak odnoszą się do tych samych operacji. V służy do inkrementacji semafora i często jest nazywana jako post, release, signal, natomiast P służy do dekrementacji semafora i jest nazywana jako pend, acquire, wait. W przypadku FreeRTOS wykorzystano nazwy give oraz take. Odpowiadają im funkcje  xSemaphoreTake() oraz xSemaphoreGive().

Synchronizacja zadań za pomocą semaforów binarnych

Głównym zadaniem semaforów binarnych jest synchronizowanie zadań lub zadań i przerwań. Dzisiaj zajmiemy się synchronizacją zadań, a do przerwań przejdziemy w następnym wpisie. Chcąc zsynchronizować dwa zadania, w jednym z nich wywołujemy funkcję xSemaphoreTake(), która służy do pobierania (dekrementacji) semafora. Jeśli semafor jest dostępny to jest on pobierany, zadanie jest dalej wykonywane i realizowana jest jego właściwa funkcjonalność. W przeciwnym wypadku przechodzi ono w stan zablokowania i oczekuje na pojawienie się semafora. Do synchronizacji dojdzie, gdy w innym zadaniu zostanie wywołana funkcja xSemaphoreGive(). Spowoduje to pojawienie się (inkrementację) semafora, dzięki czemu zadanie oczekujące przejdzie do stanu gotowości. Zanim będziemy mogli korzystać z powyższych funkcji konieczne jest utworzenie semafora. Do tworzenia semaforów binarnych przeznaczona jest funkcja  xSemaphoreCreateBinary(), zwracająca uchwyt, za pomocą którego będziemy mogli się do nich odnosić. Wewnątrz funkcji realizowana jest dynamiczna alokacja pamięci potrzebnej do przechowywania informacji o stanie semafora. W przypadku niewystarczającej ilości pamięci nie uda się go utworzyć i w takiej sytuacji funkcja zwróci NULL. Alternatywą dla funkcji  xSemaphoreCreateBinary() jest xSemaphoreCreateBinaryStatic() opierająca się na statycznej alokacji pamięci. Więcej na temat możesz przeczytać na stronie Static vs Dynamic Memory Allocation. Po utworzeniu semafor ma wartość 0 – jest niedostępny. Aby możliwe było jego pobranie za pomocą funkcji xSemaphoreTake() musi być wcześniej przekazany za pomocą funkcji xSemaphoreGive().

Wprowadzenie teoretyczne za nami. Przejdźmy do kodu! 🙂

Przykład

W przykładzie utworzymy semafor i dwa zadania, które ze sobą zsynchronizujemy. Chcąc korzystać z semaforów konieczne jest dołączenie na samym początku plików nagłówkowych queue.h oraz semphr.h. O tym dlaczego trzeba dodać także plik queue.h opowiem przy okazji opisu kolejek w Kurs FreeRTOS #8: kolejki.

Zanim przejdziemy do utworzenia semafora w systemie musimy zdefiniować zmienną, która posłuży do przechowywania uchwytu do semafora.

Pozostałej części kodu nie będę już opisywał linia po linii, ponieważ większość rzeczy jest już znana z poprzednich wpisów 🙂

Nową rzeczą, która się pojawiła jest tworzenie semafora. Jak już wspominałem, funkcja  xSemaphoreCreateBinary() zwraca uchwyt do utworzonego semafora lub NULL, gdy nie uda się go utworzyć. Dzięki temu możemy jakoś zareagować na zaistniałą sytuację. Ja w tym wypadku wchodzę do nieskończonej pętli. Tym samym zapobiegam dalszemu wykonywaniu programu. Podobny mechanizm można zastosować przy tworzeniu zadań. Nie opisywałem tego wcześniej, ponieważ nie było to wtedy mega istotne, ale pomyślałem, że teraz przy okazji mogę o tym wspomnieć 😉 Funkcja xTaskCreate() w przypadku sukcesu zwraca wartość pdPASS, więc można to sprawdzać i odpowiednio zareagować. Co więcej, podobnie jak w przypadku semaforów, zadania również można tworzyć korzystając z statycznej alokacji pamięci. Służy do tego funkcja xTaskCreateStatic(). Mały offtop, ale już wracamy do głównego tematu 🙂

Implementacja zadań

Oprócz semafora, tworzone są dwa zadania: LEDTask1 o priorytecie 1 oraz LEDTask2 o priorytecie 2. Pozostała jeszcze ich implementacja. Niech zadanie o wyższym priorytecie  LEDTask2 będzie zadaniem pobierającym semafor, a LEDTask1 dającym semafor. Przejdźmy teraz do implementacji jakiegoś prostego scenariusza, na podstawie którego będziemy w stanie zaobserwować działanie semaforów w praktyce. Znów wykorzystamy w tym celu dwie diody led… 😀 Spokojnie, przyjdzie też czas na np. obsługę UART. Wszystko w swoim czasie 🙂 Wracając do implementacji, niech w zadaniu LEDTask1 dioda mignie cztery razy, po czym kontrolę natychmiast przejmie zadanie LEDTask2. W nim raz migniemy inną diodą, po czym kontrolę ponownie przejmie  LEDTask1. Poniżej zamieszczam kod opisywanych zadań.

Analiza działania systemu

Na początku uruchamiane jest zadanie o wyższym priorytecie  LEDTask2, w którym na samym początku wywoływana jest funkcja xSemaphoreTake(). Jak widać przyjmuje ona dwa argumenty. Pierwszym jest uchwyt do semafora, który chcemy pobrać, a drugim timeout, czyli maksymalny czas jaki zadanie będzie przebywało w stanie zablokowania czekając na semafor, gdy ten jest niedostępny. Jego wartość, podobnie jak w przypadku znanej już funkcji vTaskDelay, jest podawana w tyknięciach zegara systemowego, ale w tym wypadku także można wykorzystać makro portTICK_RATE_MS. Funkcja zwraca wartość pdTRUE jeśli semafor będzie dostępny i uda się go pobrać lub pdFALSE, gdy nie uda się go pobrać w zdefiniowanym czasie. W przypadku, gdy nie chcemy korzystać z timeout’u możemy przekazać do funkcji wartość portMAX_DELAY. W takiej sytuacji należy jednak pamiętać, aby makro  INCLUDE_vTaskSuspend miało ustawioną wartość 1 w pliku konfiguracyjnym FreeRTOSConfig.h.

Po wejściu do funkcji  xSemaphoreTake() sprawdzany jest stan semafora. W związku z jego brakiem LEDTask2 przechodzi w stan zablokowania i wykonywany zaczyna być LEDTask1. W zadaniu następuje czterokrotne mignięcie diodą LED, po czym wywoływana jest funkcja  xSemaphoreGive(). Przyjmuje ona wyłącznie jeden argument i jest nim uchwyt do semafora. Po wywołaniu funkcji semafor binarny staje się dostępny i zadanie  LEDTask2 zmienia stan z zablokowanego na gotowe do wykonywania. Ma ono wyższy priorytet, więc zadanie LEDTask1 zostaje wywłaszczone i wykonywane jest zadanie LEDTask2. Tam następuje pobranie semafora i realizowane jest mignięcie drugą diodą LED. Wraz z pobraniem semafora jego wartość zmienia się na 0, dlatego przy kolejnym wejściu w funkcję  xSemaphoreTake() zadanie ponownie przechodzi w stan zablokowania i znów wykonywane jest zadanie  LEDTask1.

Podsumowanie

Hura! Poznaliśmy nowy mechanizm systemowy 🙂 W kolejnych wpisach będziemy jeszcze rozwijać ten temat, ale bardzo solidną bazę już mamy. W kolejnym wpisie opowiem o obsłudze przerwań podczas pracy z systemem FreeRTOS i związanych z tym zagadnieniach. Przy okazji wykorzystamy semafory do synchronizacji zadań z przerwaniami, a także opowiem o tym jak działają semafory zliczające. Zapraszam 🙂

Kurs FreeRTOS #6: przerwania