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 :)
