Przydzielanie zasobów za pomocą kolejki we freeRTOS
Cześć, ostatnio dowiedzieliśmy się w jaki sposób zarządzać dostępem do zasobu za pomocą semaforów. W dzisiejszym artykule pokażę jeszcze inny sposób na dostarczenie zawartości do zasobu przez kilka różnych wątków bez problemów z synchronizacją. Zapraszam do krótkiej lektury o kolejkach.
Koncepcja
Posłużymy się jedną z podstawowych struktur jaką jest kolejka. Kolejka w programowaniu działa dokładnie tak samo jak kolejka np. w sklepie. Dane, które pierwsze zostaną umieszczone w kolejce zostaną też jako pierwsze obsłużone. Kolejka jest przeciwieństwem stosu, na który wkładamy elementy, a następnie zdejmujemy je z niego w odwrotnej kolejności (ostatni element na stosie zostaje przetworzony jako pierwszy).
Aby taka kolejka mogła działac musimy trochę inaczej zaprojektować nasze taski. W wariancie z mutexami oba wątki (DigitalRead oraz AnalogRead) pisały bezpośrednio na port szeregowy. W przypadku kolejki rozwiążemy sprawę nieco inaczej. Dodamy kolejny wątek (SerialWrite), który będzie konsumentem treści z kolejki, natomiast dwa dotychczasowe wątki (DigitalRead oraz AnalogRead) będą tzw. producentami. Chodzi o to, żeby wątek konsumenta nieustannie monitorował, czy w kolejce są jakieś dane do skonsumowania, natoamist wątki producentów uzupełniały koleję nowymi danymi.
Na powyższej animacji przestawiono zasadę działania kolejki. Źródło: https://www.freertos.org/Embedded-RTOS-Queues.html
Dokumentacja
Dokładniej o kolejkach w systemie freeRTOS możemy poczytać tutaj. Dowiemy się z tej storny szczegółów działania takiej kolejki. Warto zauważyć, że nie będziemy w kolejce przesyłali całych stringów, a jedynie konkretne dane np. liczby odczytane z wejść cyfrowych i analogowych lub proste struktury.
W celu wykorzystania kolejek w projekcie musimy załączyć plik nagłówkowy queue.h
Aby utworzyć koleję do globalnej zmiennej typu QueueHandle_t przypisujemy rezultat funkcji xQueueCreate. Funkcja ta przyjmuje dwa parametry: rozmiar kolejki (w elementach), oraz rozmiar pojedynczego elementu (przyda się tutaj funkcja sizeof).
Dodawanie odbywa się poprzez funkcję o nazwie xQueueSend. Ta z kolei przyjmuje trzy parametry. Pierwszym z nich jest uchwyt do kolejki, czyli po prostu zmienna, w której zaalokowaliśmy naszą koleję wywołując funkcję xQueueCreate. Drugim parametrem jest wskaźnik do zmiennej z danymi, które mają zostać dodane do kolejki, natomiast trzeci parametr opisuje ilość ticków, na które wywołanie funkcji zablokuje wątek w oczekiwaniu na miejsce w kolejce (w przypadku, jeżeli kolejka jest pełna).
Zdejmowanie elementó odbywa się analogicznie. W tym przypadku użyjemy funkcji xQueueReceive. Jej peirwszy i ostatni parametr są takie same jak w przypadku xQueueSend. Środkowy parametr jest natomiast adresem w pamięci, do którego mają być zapisane dane zdjęte z kolejki.
Implementacja
Pierwszym krokeim jest załączenie plików nagłówkowych:
#include <Arduino_FreeRTOS.h> #include <queue.h>
Teraz możemy zadeklarować zmienną dla kolejki oraz funkcje dla wątków:
QueueHandle_t serialQueue; void TaskDigitalRead( void *pvParameters ); void TaskAnalogRead( void *pvParameters ); void TaskSerialWrite( void *pvParameters );
W funkcji setup uruchamiamy port szeregowy, inicjujemy kolejkę oraz uruchamiamy wątki:
void setup() { Serial.begin(9600); while (!Serial) { ; // wait for serial port to connect. Needed for native USB, on LEONARDO, MICRO, YUN, and other 32u4 based boards. } serialQueue = xQueueCreate(16, sizeof(int)); if(serialQueue == NULL){ Serial.println("Failed to create serialQueue"); while(true); } xTaskCreate( TaskDigitalRead , (const portCHAR *)"DigitalRead" // A name just for humans , 128 // This stack size can be checked & adjusted by reading the Stack Highwater , NULL , 2 // Priority, with 3 (configMAX_PRIORITIES - 1) being the highest, and 0 being the lowest. , NULL ); xTaskCreate( TaskAnalogRead , (const portCHAR *) "AnalogRead" , 128 // Stack size , NULL , 1 // Priority , NULL ); xTaskCreate( TaskSerialWrite , (const portCHAR *) "SerialWrite" , 128 // Stack size , NULL , 1 // Priority , NULL ); }
Funkcja loop jak zwykle w przypadku użycia freeRTOS’a pozostaje pusta.
void loop() { }
Przyjrzyjmy się teraz wątkom producentów:
void TaskDigitalRead( void *pvParameters __attribute__((unused)) ) { uint8_t pushButton = 2; pinMode(pushButton, INPUT); for (;;) { int buttonState = digitalRead(pushButton); xQueueSend(serialQueue, &buttonState, portMAX_DELAY); vTaskDelay(1); } } void TaskAnalogRead( void *pvParameters __attribute__((unused)) ) { for (;;) { uint8_t analogIn = A0; int sensorValue = analogRead(analogIn); xQueueSend(serialQueue, &sensorValue, portMAX_DELAY); vTaskDelay(1); } }
Jak widać zapisują one dane do zmiennej typu int, a następnie jej adres przekazywany jest do funkcji xQueueSend. W ten sposób kolejka pobiera spod danego adresu odpowiednią ilość bajtów.
Wątek konsumenta działa analogicznie, tylko w drugą stornę:
void TaskSerialWrite( void *pvParameters __attribute__((unused)) ) { for (;;) { int currentItem; while(xQueueReceive(serialQueue, ¤tItem, portMAX_DELAY) == pdTRUE) { Serial.println(currentItem); } vTaskDelay(1); } }
Deklarujemy zmienną typu int o nazwie currentItem, a następnie dopóki w kolejce znajdują się jakieś dane do skonsumowania zapisujemy odpowiednią ilość bajtów z pamięci kolejki do owej zmiennej, po czym wypisujemy ją na port szeregowy.
Gotowe, możemy teraz skompilować program, wgrać go na Arduino i zoabczyć jak działa nasza implementacja kolejki :)