Über den Beitrag
Mit diesem Beitrag möchte ich versuchen, auch Nutzern mit weniger Erfahrung eine einigermaßen verständliche Einführung in FreeRTOS zu bieten. Aus eigener Erfahrung weiß ich, dass das Thema ein wenig gewöhnungsbedürftig ist, dafür ist es aber ebenso spannend.
- FreeRTOS Dokumentation
- Tasks mit FreeRTOS erzeugen und nutzen
- Ticks und vTaskDelay()
- Den Speicherbedarf eines FreeRTOS Tasks ermitteln
- Tasks aussetzen und wieder aufnehmen
- Den ausführenden Kern festlegen
- FreeRTOS Tasks synchronisieren – Semaphore und Mutex
- Parameter an einen Task übergeben
Was ist FreeRTOS?
FreeRTOS ist ein quelloffenes Echtzeitbetriebssystem (RTOS), das speziell für eingebettete Systeme entwickelt wurde. Es bietet eine präemptive Multitasking-Umgebung, um Echtzeitanforderungen in verschiedenen Anwendungen zu erfüllen. Hä? OK, diese Erklärung braucht selbst wohl ein paar Erklärungen:
- RTOS steht für Realtime Operating System, also Echtzeitsystem. In einem Echtzeitsystem steht der Zeitrahmen für die Ausführung von Aufgaben im Vordergrund.
- Eingebettete Systeme sind – unter anderem – Mikroprozessoren, die in bestimmten Geräten verbaut sind oder Mikrocontroller. Im Gegensatz zu herkömmlichen Computern, die für eine Vielzahl von Anwendungen geeignet sind, sind eingebettete Systeme auf spezifische Aufgaben oder Funktionen ausgerichtet.
- Präemptiv heißt, dass eine Aufgabe unterbrochen werden kann, um einer anderen Aufgabe die Ausführung zu ermöglichen.
FreeRTOS wurde Anfang der 2000er-Jahre von Richard Barry entwickelt. Es darf frei verwendet, modifiziert und weiterverbreitet werden. FreeRTOS unterstützt eine Vielzahl von Architekturen und Prozessoren, darunter AVR, ARM, ESP, x86 und mehr. Es gibt also nicht das eine FreeRTOS, sondern diverse Anpassungen.
FreeRTOS Dokumentation
Zur Vertiefung empfehle ich euch parallel zu diesem Beitrag zur Vertiefung in die FreeRTOS Dokumention zu schauen, und zwar insbesondere in die API-Referenz (Application Programming Interface oder Programmierschnittstelle), die ihr hier findet. Sie ist sehr übersichtlich geordnet und sollte in Verbindung mit diesem Beitrag auch recht gut verständlich sein.
Ist mit FreeRTOS echtes Multitasking möglich?
Jein. Die meisten Mikrocontroller besitzen nur einen Kern. In diesen Fällen wird das sogenannte Time-Slicing angewendet, was bedeutet, dass jedem Task eine gewisse Prozessorzeit zugeordnet wird. Nach Ablauf der Zeit wird der Task unterbrochen und der nächste Task ist an der Reihe. Diese Wechsel geschehen in kurzer Folge, sodass der Eindruck von Multitasking entsteht. Eine Ausnahme ist unter anderem der ESP32, denn er besitzt zwei Kerne. Hier können Tasks tatsächlich parallel bearbeitet werden.
Welche FreeRTOS Implementierungen betrachten wir in diesem Beitrag?
Ich betrachte in diesem Beitrag den ESP32 und die AVR-basierten Arduino-Boards (also z. B. den UNO R3, den klassischen Nano oder den Pro Mini). Der ESP32 nutzt in der Arduino Umgebung ohnehin schon FreeRTOS. Es wird über das esp32 Boardpaket eingebunden, sodass ihr euch um die FreeRTOS Bibliotheken keine Gedanken machen müsst. Für die AVR-Arduinos gibt es die Arduino_FreeRTOS_Library, die ihr unter dem Namen „FreeRTOS“ im Bibliotheksmanager der Arduino finden und installieren könnt.
Da es ein paar kleinere Unterschiede in der Nutzung von FreeRTOS auf dem ESP32 und den AVR-basierten Arduinos gibt, habe ich alle Sketche in zwei Versionen geschrieben und auch getestet.
Es gibt noch viele weitere FreeRTOS Bibliotheken, zum Beispiel für SAMD21-, SAMD51-, STM32-Boards. Die konnte ich nicht alle ausprobieren und schon gar nicht hier im Detail behandeln. Aber wenn ihr euch einmal in FreeRTOS hineingedacht habt, dann sollten euch weitere Varianten keine größeren Probleme bereiten.
Tasks mit FreeRTOS erzeugen und nutzen
Wir beginnen ganz einfach mit einem Blink-Sketch. Drei LEDs sollen mit jeweils unterschiedlichen Frequenzen blinken. Ohne FreeRTOS könnte man so etwas mit einer delay()
Konstruktion realisieren, wobei das die schlechteste Lösung wäre. Schon besser, da nicht blockierend, wäre eine Lösung à la if((millis() - lastToggle) > blinkPeriod){...}
. Aber auch damit kann es Probleme geben, wenn etwa andere Prozesse die millis()
Abfrage verzögern. Alternativ würden sich Timer Interrupts anbieten – sofern denn genügend Timer zur Verfügung stehen.
Mit FreeRTOS umgehen wir diese Probleme einfach, indem jede LED ihren eigenen Task bekommt. Im Prinzip also drei Blinksketche, die parallel laufen.
Hier zunächst der Code:
#define LED1 25 #define LED2 26 #define LED3 17 void setup() { pinMode(LED3, OUTPUT); xTaskCreate( blink1, // Function name of the task "Blink 1", // Name of the task (e.g. for debugging) 2048, // Stack size (bytes) NULL, // Parameter to pass 1, // Task priority NULL // Task handle ); xTaskCreate( blink2, // Function name of the task "Blink 2", // Name of the task (e.g. for debugging) 2048, // Stack size (bytes) NULL, // Parameter to pass 1, // Task priority NULL // Task handle ); } void blink1(void *parameter) { pinMode(LED1, OUTPUT); while(1){ digitalWrite(LED1, HIGH); delay(500); // Delay for Tasks digitalWrite(LED1, LOW); delay(500); } } void blink2(void *parameter) { pinMode(LED2, OUTPUT); while(1) { digitalWrite(LED2, HIGH); delay(333); digitalWrite(LED2, LOW); delay(333); } } void loop(){ digitalWrite(LED3, HIGH); delay(1111); digitalWrite(LED3, LOW); delay(1111); }
#include<Arduino_FreeRTOS.h> #define LED1 7 #define LED2 8 #define LED3 9 void setup() { xTaskCreate( blink1, // Function name of the task "Blink 1", // Name of the task (e.g. for debugging) 128, // Stack size (bytes) NULL, // Parameter to pass 1, // Task priority NULL // Task handle ); xTaskCreate( blink2, // Function name of the task "Blink 2", // Name of the task (e.g. for debugging) 128, // Stack size (bytes) NULL, // Parameter to pass 1, // Task priority NULL // Task handle ); xTaskCreate( blink3, // Function name of the task "Blink 3", // Name of the task (e.g. for debugging) 128, // Stack size (bytes) NULL, // Parameter to pass 1, // Task priority NULL // Task handle ); } void blink1(void *pvParameters){ pinMode(LED1, OUTPUT); while(1){ digitalWrite(LED1, HIGH); delay(500); digitalWrite(LED1, LOW); delay(500); } } void blink2(void *pvParameters){ pinMode(LED2, OUTPUT); while(1){ digitalWrite(LED2, HIGH); delay(333); digitalWrite(LED2, LOW); delay(333); } } void blink3(void *pvParameters){ pinMode(LED3, OUTPUT); while(1){ digitalWrite(LED3, HIGH); delay(1111); digitalWrite(LED3, LOW); delay(1111); } } void loop(){}
Erläuterung des Codes
Den Task erzeugen
Wenn ihr eine Aufgabe in einem eigenen Task erledigen lassen wollt, dann müsst ihr den Task zunächst einmal mit xTaskCreate()
erschaffen. Allerdings ist das, was ihr da erzeugt, nur eine Art leeres Gehäuse, das ihr erst in einem zweiten Schritt mit Leben füllt. Hier die allgemeine Form von xTaskCreate()
:
BaseType_t xTaskCreate( TaskFunction_t pvTaskCode, const char * const pcName, configSTACK_DEPTH_TYPE usStackDepth, void *pvParameters, UBaseType_t uxPriority, TaskHandle_t *pxCreatedTask );
xTaskCreate Parameter
Die sechs zu übergebenden Parameter bedeuten:
- pvTaskCode: Das ist der Name der Funktion, die den Code enthält, welcher in dem Task ausgeführt werden soll. Die Vorgehensweise ist vergleichbar mit dem Anmelden einer Interrupt Service Routine in
attachInterrupt()
. - pcName: Dieser Parameter erlaubt es euch, dem Task einen leicht verständlichen Namen zu geben, beispielsweise „Mein Lieblingstask“. pcName wird als Zeiger übergeben.
- usStackdepth: Zu der Erstellung eures „Taskgehäuses“ gehört auch die Definition seiner Größe im Stack, also einem Teil des Arbeitsspeichers (SRAM). Zu SRAM, Stack und Heap habe ich hier etwas geschrieben. Wie ihr die Größe des Tasks bestimmt, dazu kommen wir noch.
- pvParameters: Ihr könnt dem Task Parameter als Zeiger übergeben. Der Variablentyp „void“ dürfte für viele ungewöhnlich sein. Er macht die Übergabe sehr flexibel. In diesem Beispiel übergeben wir nichts, also NULL (ein Zeiger nach nirgendwo).
- uxPriority: Mit diesem Parameter könnt ihr die Priorität des Tasks festlegen.
- pxCreatedTask: Ein Task Handle (wörtlich: Griff) ist eine optionale Variable, die ihr beispielsweise nutzen könnt, um Eigenschaften des Tasks abzufragen. Man könnte sagen, es ist ein Bezeichner für den Task.
Die Namensgebung der Präfixe der Variablen und Funktionen ergibt sich aus dem Variablentyp bzw. dem Variablentyp des Rückgabewertes. Ein „c“ steht für „char“, ein „s“ für „short“, „v“ für „void“, ein „u“ für „unsigned“ und „x“ für alle Nicht-Standard-Typen. Handelt es sich bei einer Variablen um einen Pointer (Zeiger), dann wird noch ein „p“ davor gesetzt. Mehr zu den Namenskonventionen findet ihr hier.
Die Funktion xTaskCreate()
liefert pdPASS
zurück, wenn der Task erfolgreich erstellt wurde. Ihr könntet das also mit if(xTaskCreate(......)==pdPASS)){....}
prüfen. Wenn der Task nicht erstellt werden konnte, liefert die Funktion errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY
zurück.
Den Task mit Inhalt füllen – Die Taskfunktion
Um den zuvor erstellten Task mit Leben zu füllen, nutzt ihr die Task-Funktion, die ihr in xTaskCreate()
definiert habt (pvTaskCode). Es notwendig, void *pvParameters
noch einmal in die empfangende Funktion zu schreiben, selbst wenn ihr NULL
übergebt. Also, beispielsweise: void blink1(void *pvParameters){....}
.
Für die Task-Funktion gilt:
- Sie hat keinen Rückgabewert.
- Wie ein eigenständiger Sketch besteht die Task-Funktion aus einer Art Setup, also Code der nur einmal ausgeführt wird und einer Dauerschleife, die ihr mit
while(1){....}
oderfor(;;){....}
realisiert. - Ein Task kann sich mit
vTaskDelete( NULL );
selbst löschen. Von außerhalb des Tasks erfolgt die Löschung mithilfe des Task Handles, alsovTaskDelete(pxCreatedTask)
.
Interessant ist das Verhalten von delay()
. Innerhalb des Tasks ist delay()
wie gewohnt blockierend. Beim Taskwechsel hingegen wird das delay()
unterbrochen.
Verwendung von loop()
Wie ihr seht, habe ich die Steuerung der LED3 im ESP32 Sketch in loop()
untergebracht. Das ist nicht der beste Stil, ich wollte aber zeigen, dass es funktioniert. Macht ihr dasselbe mit einem AVR basierten Arduino, dann bleibt das Programm hängen. Deswegen ist dort die Steuerung von LED3 in einem dritten Task ausgelagert.
Ticks und vTaskDelay()
Die Tasks müssen sich den oder die Prozessorkerne teilen. Um das zu organisieren, teilt FreeRTOS die Zeit in Ticks. Für den Zeitraum eines Ticks kann der Task den Prozessor ungestört nutzen. Nach Ablauf des Ticks wird geprüft, ob ein anderer Task an die Reihe kommt.
Ein Tick ist auf dem ESP32 auf 1 Millisekunde voreingestellt, was ihr euch auch mit Serial.println(portTICK_PERIOD_MS)
ausgeben lassen könnt. Anders ausgedrückt beträgt die Tickfrequenz 1 kHz. Die Tickfrequenz wiederum könnt ihr mit Serial.println(configTICK_RATE_HZ)
abfragen. Die FreeRTOS Version für die AVR-basierten Arduinos legt eine Tickfrequenz von 62 Hz als Standard fest, d. h. ein Tick hat eine Länge von ca. 16 Millisekunden.
Wenn ihr im Netz nach FreeRTOS Code Beispielen sucht, dann werdet ihr häufig auf die Funktion vTaskDelay()
stoßen. Sie macht im Prinzip dasselbe wie delay()
, allerdings mit dem Unterschied, dass ihr vTaskDelay()
nicht Millisekunden, sondern Ticks übergebt. Beim ESP32 macht das keinen Unterschied, wohl aber bei den AVR-Arduinos. Um 500 Millisekunden Wartezeit zu erreichen, müsstet ihr also schreiben: vTaskDelay(500/portTICK_PERIOD_MS)
.
Ich bleibe bei allen Beispielen beim guten, alten delay()
.
Den Speicherbedarf eines FreeRTOS Tasks ermitteln
Die Berechnung des vom Task benötigten Speichers ist nicht trivial. Stattdessen reservieren wir erst einmal eine ausreichende Menge Speicher und lassen und uns dann mit der Funktion uxTaskGetStackHighWaterMark()
den verbliebenen freien Speicher im reservierten Bereich anzeigen. Um der Funktion uxTaskGetStackHighWaterMark()
mitzuteilen, von welchem wir den Task wir den freien Speicher ermitteln wollen, übergeben wir ihr den Task Handle. Diesen erzeugt ihr zunächst mit TaskHandle_t name
und verknüpft ihn in xTaskCreate()
mit dem Task.
Ich habe den folgenden Sketch auf zwei Blink Tasks reduziert. Um besser sichtbar zu machen, dass uxTaskGetStackHighWaterMark()
tatsächlich den freien Speicher anzeigt, habe ich den reservierten Speicher für die Tasks variiert.
Würden wir die Abfrage des Speicherbedarfs in dem zu untersuchenden Task implementieren, würde das das Ergebnis verfälschen. Die Speicherabfrage bekommt deshalb ihren eigenen Task.
#define LED1 25 #define LED2 26 TaskHandle_t taskBlink1Handle; // create task handle TaskHandle_t taskBlink2Handle; void setup() { Serial.begin(115200); pinMode(LED1, OUTPUT); pinMode(LED2, OUTPUT); xTaskCreate( blink1, // Function name of the task "Blink 1", // Name of the task (e.g. for debugging) 2048, // Stack size (bytes) NULL, // Parameter to pass 1, // Task priority &taskBlink1Handle // Assign task handle ); xTaskCreate( blink2, // Function name of the task "Blink 2", // Name of the task (e.g. for debugging) 1048, // Stack size (bytes) NULL, // Parameter to pass 1, // Task priority &taskBlink2Handle // Assign task handle ); xTaskCreate( printWatermark, // Function name of the task "Print Watermark", // Name of the task (e.g. for debugging) 2048, // Stack size (bytes) NULL, // Parameter to pass 1, // Task priority NULL // Task handle ); } void blink1(void *pvParameters){ while(1){ digitalWrite(LED1, HIGH); delay(500); digitalWrite(LED1, LOW); delay(500); } } void blink2(void *pvParameters){ while(1){ digitalWrite(LED2, HIGH); delay(333); digitalWrite(LED2, LOW); delay(333); } } void printWatermark(void *pvParameters){ while(1){ delay(2000); Serial.print("TASK: "); Serial.print(pcTaskGetName(taskBlink1Handle)); // Get task name with handler Serial.print(", High Watermark: "); Serial.print(uxTaskGetStackHighWaterMark(taskBlink1Handle)); Serial.println(); Serial.print("TASK: "); Serial.print(pcTaskGetName(taskBlink2Handle)); // Get task name with handler Serial.print(", High Watermark: "); Serial.print(uxTaskGetStackHighWaterMark(taskBlink2Handle)); Serial.println(); } } void loop(){}
#include<Arduino_FreeRTOS.h> #define LED1 7 #define LED2 8 TaskHandle_t taskBlink1Handle; // create task handle TaskHandle_t taskBlink2Handle; void setup() { Serial.begin(9600); pinMode(LED1, OUTPUT); pinMode(LED2, OUTPUT); xTaskCreate( blink1, // Function name of the task "Blink 1", // Name of the task (e.g. for debugging) 128, // Stack size (bytes) NULL, // Parameter to pass 1, // Task priority &taskBlink1Handle // Assign task handle ); xTaskCreate( blink2, // Function name of the task "Blink 2", // Name of the task (e.g. for debugging) 98, // Stack size (bytes) NULL, // Parameter to pass 1, // Task priority &taskBlink2Handle // Assign task handle ); xTaskCreate( printWatermark, // Function name of the task "Print Watermark", // Name of the task (e.g. for debugging) 128, // Stack size (bytes) NULL, // Parameter to pass 1, // Task priority NULL // Task handle ); } void blink1(void *pvParameters){ while(1){ digitalWrite(LED1, HIGH); delay(500); digitalWrite(LED1, LOW); delay(500); } } void blink2(void *pvParameters){ while(1){ digitalWrite(LED2, HIGH); delay(333); digitalWrite(LED2, LOW); delay(333); } } void printWatermark(void *pvParameters){ while(1){ delay(2000); Serial.print("TASK: "); Serial.print(pcTaskGetName(taskBlink1Handle)); // Get task name with handler Serial.print(", High Watermark: "); Serial.print(uxTaskGetStackHighWaterMark(taskBlink1Handle)); Serial.println(); Serial.print("TASK: "); Serial.print(pcTaskGetName(taskBlink2Handle)); // Get task name with handler Serial.print(", High Watermark: "); Serial.print(uxTaskGetStackHighWaterMark(taskBlink2Handle)); Serial.println(); } } void loop(){}
Und hier nun die Ausgabe:
Daraus können wir ableiten:
- Die Tasks benötigen auf dem ESP32: 1048 – 500 bzw. 2048 – 1500 = 548 Byte.
- Auf dem AVR Arduino sind es: 128 – 78 bzw. 98 – 48 = 50 Byte.
Der AVR Arduino geht also sparsamer mit dem Speicher um, was er ja auch muss, da sein SRAM nur einen Bruchteil des ESP32 SRAMs beträgt.
Wenn ihr exakt den eben ermittelten Speicherbedarf einsetzt, dann wird der Sketch wahrscheinlich abstürzen. Spendiert den Tasks ein paar Bytes mehr.
Was ihr dem Beispielsketch auch noch entnehmen könnt, ist, wie ihr mit pcTaskGetName()
an den Namen des Tasks kommt.
Tasks aussetzen und wieder aufnehmen
Tasks lassen sich sehr einfach mit vTaskSuspend(taskHandle);
aussetzen und mit vTaskResume(taskHandle);
wieder aufnehmen. Mit dem folgenden Sketch lassen wir eine LED mittels eines Tasks blinken und über einen weiteren Task setzen wir das Blinken regelmäßig aus.
#define LED1 25 TaskHandle_t blink1Handle; void setup() { xTaskCreate( blink1, // Function name of the task "Blink 1", // Name of the task (e.g. for debugging) 2048, // Stack size (bytes) NULL, // Parameter to pass 1, // Task priority &blink1Handle // Task handle ); xTaskCreate( suspend_resume, // Function name of the task "Suspend Resume", // Name of the task (e.g. for debugging) 2048, // Stack size (bytes) NULL, // Parameter to pass 1, // Task priority NULL // Task handle ); } void blink1(void *pvParameters){ pinMode(LED1, OUTPUT); while(1){ digitalWrite(LED1, HIGH); delay(100); digitalWrite(LED1, LOW); delay(100); } } void suspend_resume(void *pvParameters){ pinMode(LED1, OUTPUT); while(1){ delay(1999); vTaskSuspend(blink1Handle); delay(999); vTaskResume(blink1Handle); } } void loop(){}
#include<Arduino_FreeRTOS.h> #define LED1 7 TaskHandle_t blink1Handle; void setup() { xTaskCreate( blink1, // Function name of the task "Blink 1", // Name of the task (e.g. for debugging) 128, // Stack size (bytes) NULL, // Parameter to pass 1, // Task priority &blink1Handle // Task handle ); xTaskCreate( suspend_resume, // Function name of the task "Suspend Resume", // Name of the task (e.g. for debugging) 128, // Stack size (bytes) NULL, // Parameter to pass 1, // Task priority NULL // Task handle ); } void blink1(void *pvParameters){ pinMode(LED1, OUTPUT); while(1){ digitalWrite(LED1, HIGH); delay(100); digitalWrite(LED1, LOW); delay(100); } } void suspend_resume(void *pvParameters){ while(1){ delay(1900); vTaskSuspend(blink1Handle); delay(900); vTaskResume(blink1Handle); } } void loop(){}
Den ausführenden Kern festlegen
Auf dem ESP32 stehen uns zwei Kerne zur Verfügung, 0 und 1. Mit der Funktion xTaskCreatePinnedToCore()
können wir bestimmen, auf welchem der Kerne der Task ausgeführt werden soll. Der Funktion werden dieselben Parameter übergeben wie xCreateTask()
, nur dass am Ende noch der Kern hinzukommt. Hier ein einfaches Beispiel.
void setup() { Serial.begin(115200); xTaskCreatePinnedToCore( print1, // Function name of the task "Print 1", // Name of the task (e.g. for debugging) 2048, // Stack size (bytes) NULL, // Parameter to pass 1, // Task priority NULL, // Task handle 0 // run on Core 0 ); xTaskCreatePinnedToCore( print2, // Function name of the task "Print 2", // Name of the task (e.g. for debugging) 2048, // Stack size (bytes) NULL, // Parameter to pass 1, // Task priority NULL, // Task handle 1 // run on Core 1 ); } void print1(void *pvParameters){ while(1){ Serial.print("Task is running on core "); Serial.println(xPortGetCoreID()); delay(2500); } } void print2(void *pvParameters){ while(1){ Serial.print("Task is running on core "); Serial.println(xPortGetCoreID()); delay(1500); } } void loop(){}
Wie ihr seht, könnt ihr mit xPortGetCoreID()
prüfen, auf welchem Kern der Task ausgeführt wird.
Welchen Kern sollte ich nehmen?
Standardmäßig wird euer Arduino Code auf dem Kern 1 ausgeführt. Verwendet ihr dabei WiFi- oder BLE, ist eine Menge Arbeit im Hintergrund notwendig, die auf dem Kern 0 ausgeführt wird. Wenn ihr dem Kern 0 nun zu viele Extraaufgaben gebt, könnte es passieren, dass euer Wi-Fi und BLE nicht mehr zuverlässig funktionieren. Nutzt ihr diese Funktionen nicht, dann müsst ihr euch darum keine Sorgen machen.
FreeRTOS Tasks synchronisieren – Semaphore und Mutex
Wenn Tasks auf dieselben Ressourcen zugreifen, wie etwa externe Sensoren, EEPROMs, A/D-Wandler, serielle Übertragungswege usw., dann könnten sie sich mit unabsehbaren Folgen gegenseitig ins Gehege kommen. Sollte diese Gefahr bestehen, dann müssen die Tasks zeitlich aufeinander abgestimmt werden. Dazu gibt es zwei Techniken, nämlich Semaphore und Mutex.
Der Begriff Semaphor kommt aus dem Alt-Griechischem. Sema heißt Zeichen und phoros heißt tragend. Zusammengesetzt bedeutet das in etwa „Signalgeber“. Mit anderen Worten: der (alternativ: das) Semaphor gibt dem Task das Signal starten zu dürfen.
Mutex steht für „Mutual Exclusion Object“, es bedeutet also einen gegenseitigen Ausschluss der Tasks. Der Mutex ist dem Semaphor sehr ähnlich, allerdings gibt ein paar feine Unterschiede (siehe auch hier in der FreeRTOS Doku).
Man könnte sagen, es ist eine Art Staffelstab. Nur wer ihn hat, darf loslaufen. Ein wenig hinkt der Vergleich allerdings, weil der übergebende Läufer weiterlaufen darf.
Semaphore
Es gibt zwei Arten Semaphore, nämlich binäre Semaphore (binary semaphores) und zählende Semaphore (counting semaphores). Den Unterschied erkläre ich anhand von Beispielen.
Binäre Semaphore – Binary Semaphores
Im ersten Schritt erzeugt ihr mit SemaphoreHandle_t xSemaphore;
einen Semaphore-Handle, den ihr im zweiten Schritt nutzt, um den Semaphor mit Leben zu füllen: xSemaphore = xSemaphoreCreateBinary();
. Dabei ersetzt ihr „xSemaphore“ ggf. durch einen Namen eurer Wahl. Im weiteren Verlauf nenne ich den Semaphor kurz „sem“.
Und das sind die Spielregeln: damit der Semaphor benutzt werden kann, muss er mit xSemaphoreGive(sem)
freigegeben werden. Für die Entgegennahme des Semaphors gibt es die Funktion xSemaphoreTake(sem, xTicksToWait)
mit xTicksToWait
als maximaler Wartezeit in Ticks. Wenn der Semaphor innerhalb der Wartezeit angenommen wurde, liefert die Funktion den Wert pdTRUE
zurück, andernfalls pdFALSE
.
Jeder Task kann den Semaphor freigeben, sofern er ihn hat. Und jeder Task kann den Semaphore entgegennehmen, sofern er verfügbar ist.
Oftmals möchte man, dass der Task so lange auf den Task wartet, wie es eben notwendig ist. In dem Fall übergebt ihr als xTicksToWait
den Wert portMAX_DELAY
. Auf dem ESP32 ist portMAX_DELAY
232 – 1, auf den AVR Arduinos ist es 216 – 1.
Binary Semaphores – Beispielsketch I
Wir lassen wieder zwei LEDs blinken, diesmal aber im Wechselspiel. Erst soll die eine LED 10 Mal blinken, dann die andere und so soll es dann wechselseitig weitergehen.
#define LED1 25 #define LED2 26 SemaphoreHandle_t sem; // Create semaphore handle void setup() { sem = xSemaphoreCreateBinary(); // Create binary semaphore xTaskCreate( blink1, // Function name of the task "Blink 1", // Name of the task (e.g. for debugging) 2048, // Stack size (bytes) NULL, // Parameter to pass 1, // Task priority NULL // Task handle ); xTaskCreate( blink2, // Function name of the task "Blink 2", // Name of the task (e.g. for debugging) 2048, // Stack size (bytes) NULL, // Parameter to pass 1, // Task priority NULL // Task handle ); xSemaphoreGive(sem); } void blink1(void *pvParameters){ pinMode(LED1, OUTPUT); while(1){ xSemaphoreTake(sem,portMAX_DELAY); for(int i=0; i<10; i++){ digitalWrite(LED1, HIGH); delay(250); digitalWrite(LED1, LOW); delay(250); } xSemaphoreGive(sem); delay(100); // Short delay is needed! } } void blink2(void *pvParameters){ pinMode(LED2, OUTPUT); while(1){ xSemaphoreTake(sem,portMAX_DELAY); for(int i=0; i<10; i++){ digitalWrite(LED2, HIGH); delay(333); digitalWrite(LED2, LOW); delay(333); } xSemaphoreGive(sem); delay(100); // Short delay is needed! } } void loop(){}
#include <Arduino_FreeRTOS.h> #include <semphr.h> // Needed in case if you want to use semaphores #define LED1 7 #define LED2 8 SemaphoreHandle_t sem; // Create semaphore handle void setup() { sem = xSemaphoreCreateBinary(); // Create binary semaphore xTaskCreate( blink1, // Function name of the task "Blink 1", // Name of the task (e.g. for debugging) 128, // Stack size (bytes) NULL, // Parameter to pass 1, // Task priority NULL // Task handle ); xTaskCreate( blink2, // Function name of the task "Blink 2", // Name of the task (e.g. for debugging) 128, // Stack size (bytes) NULL, // Parameter to pass 1, // Task priority NULL // Task handle ); xSemaphoreGive(sem); // Release semaphore } void blink1(void *pvParameters){ pinMode(LED1, OUTPUT); while(1){ xSemaphoreTake(sem,portMAX_DELAY); // Take semaphore for(int i=0; i<10; i++){ digitalWrite(LED1, HIGH); delay(250); digitalWrite(LED1, LOW); delay(250); } xSemaphoreGive(sem); // Release semaphore delay(100); // Short delay is needed! } } void blink2(void *pvParameters){ pinMode(LED2, OUTPUT); while(1){ xSemaphoreTake(sem,portMAX_DELAY); // Take semaphore for(int i=0; i<10; i++){ digitalWrite(LED2, HIGH); delay(333); digitalWrite(LED2, LOW); delay(333); } xSemaphoreGive(sem); // Release semaphore delay(100); // Short delay is needed! } } void loop(){}
Im Setup wird der Semaphor kreiert und freigegeben. Einer der beiden Tasks nimmt ihn entgegen und somit kann dieser Task seine Arbeit aufnehmen. Solange der Semaphor nicht wieder freigegeben ist, muss der zweite Task warten. Wurde der Semaphor durch den ersten Task freigegeben und durch den zweiten entgegengenommen, dann muss wiederum der erste Task warten, wenn er auf die Funktion xSemaphoreTake()
trifft.
Ohne ein kurzes delay()
nach dem xSemaphoreGive()
funktioniert die Übergabe des Semaphors nicht. Anscheinend „schnappt“ sich der gerade ausführende Task den Semaphor sonst selbst. Das delay()
muss also mindestens einen Tick lang sein.
In diesem Beispiel haben wir nicht eindeutig geregelt, welcher der beiden Tasks nach dem Programmstart zuerst ausgeführt wird. Ihr könnt das steuern, indem ihr einem der beiden Tasks in xTaskCreate()
eine höhere Priorität gebt.
Zur AVR-Arduino Version ist noch zu sagen, dass ihr die Bibliotheksdatei semphr.h einbinden müsst, um die Semaphor-Funktionen nutzen zu können.
Binary Semaphores – Beispielsketch II
Der letzte Beispielsketch könnte den Eindruck hinterlassen haben, dass der binäre Semaphor exklusiv wirkt, d. h. nur die Ausführung eines der Tasks zulässt. Dass das nicht so ist, zeigt der nächste Sketch:
#define LED1 25 #define LED2 26 SemaphoreHandle_t sem; // Create semaphore handle void setup() { sem = xSemaphoreCreateBinary(); // Create a binary semaphore xTaskCreate( blink1, // Function name of the task "Blink 1", // Name of the task (e.g. for debugging) 2048, // Stack size (bytes) NULL, // Parameter to pass 1, // Task priority NULL // Task handle ); xTaskCreate( blink2, // Function name of the task "Blink 2", // Name of the task (e.g. for debugging) 2048, // Stack size (bytes) NULL, // Parameter to pass 1, // Task priority NULL // Task handle ); } void blink1(void *pvParameters){ pinMode(LED1, OUTPUT); while(1){ for(int i=0; i<10; i++){ digitalWrite(LED1, HIGH); delay(250); digitalWrite(LED1, LOW); delay(250); } xSemaphoreGive(sem); // Release semaphore delay(7000); // Give time to execute blink2 } } void blink2(void *pvParameters){ while(1){ pinMode(LED2, OUTPUT); xSemaphoreTake(sem,portMAX_DELAY); // Take semaphore for(int i=0; i<10; i++){ digitalWrite(LED2, HIGH); delay(333); digitalWrite(LED2, LOW); delay(333); } } } void loop(){}
#include <Arduino_FreeRTOS.h> #include <semphr.h> #define LED1 7 #define LED2 8 SemaphoreHandle_t sem; // Create semaphore handle void setup() { sem = xSemaphoreCreateBinary(); // Create binary semaphore xTaskCreate( blink1, // Function name of the task "Blink 1", // Name of the task (e.g. for debugging) 128, // Stack size (bytes) NULL, // Parameter to pass 1, // Task priority NULL // Task handle ); xTaskCreate( blink2, // Function name of the task "Blink 2", // Name of the task (e.g. for debugging) 128, // Stack size (bytes) NULL, // Parameter to pass 1, // Task priority NULL // Task handle ); } void blink1(void *pvParameters){ pinMode(LED1, OUTPUT); while(1){ for(int i=0; i<10; i++){ digitalWrite(LED1, HIGH); delay(250); digitalWrite(LED1, LOW); delay(250); } xSemaphoreGive(sem); // Release semaphore delay(7000); // Give time to execute blink2 } } void blink2(void *pvParameters){ pinMode(LED2, OUTPUT); while(1){ xSemaphoreTake(sem, portMAX_DELAY); // Take semaphore for(int i=0; i<10; i++){ digitalWrite(LED2, HIGH); delay(333); digitalWrite(LED2, LOW); delay(333); } } } void loop(){}
Im Gegensatz zum vorherigen Beispiel wird hier der Semaphor nicht im Setup freigegeben. Der Task blink2 ist deshalb zunächst durch das xSemaphoreTake()
blockiert. Das gilt aber nicht für den Task blink1. Nach dem zehnmaligen Blinken der LED1 wird der Semaphor freigegeben, aber trotzdem läuft blink1 noch sieben Sekunden mit einem delay()
weiter. In diesen sieben Sekunden wird die While-Schleife in blink2 einmal durchlaufen, bevor sie dann wegen fehlendem freien Semaphor blockiert. Also ist wieder blink1 an der Reihe und das Spiel beginnt von vorn.
Binary Semaphores – Beispielsketch III
Zur Vertiefung möchte ich noch einen dritten Beispielsketch präsentieren. Hier lassen wir die LED1 wieder zehnmal blinken, aber nachdem fünften Blinken kommt ein zehnmaliges, schnelles Blinken der LED2 hinzu. Das zeigt vielleicht noch etwas anschaulicher, wie die Tasks zeitlich durch Semaphore gesteuert werden.
#define LED1 25 #define LED2 26 SemaphoreHandle_t sem; // Create semaphore handle void setup() { sem = xSemaphoreCreateBinary(); // Create binary semaphore xTaskCreate( blink1, // Function name of the task "Blink 1", // Name of the task (e.g. for debugging) 2048, // Stack size (bytes) NULL, // Parameter to pass 1, // Task priority NULL // Task handle ); xTaskCreate( blink2, // Function name of the task "Blink 2", // Name of the task (e.g. for debugging) 2048, // Stack size (bytes) NULL, // Parameter to pass 1, // Task priority NULL // Task handle ); } void blink1(void *pvParameters){ pinMode(LED1, OUTPUT); while(1){ for(int i=0; i<5; i++){ digitalWrite(LED1, HIGH); delay(250); digitalWrite(LED1, LOW); delay(250); } xSemaphoreGive(sem); // Release semaphore for(int i=0; i<5; i++){ digitalWrite(LED1, HIGH); delay(250); digitalWrite(LED1, LOW); delay(250); } delay(2000); } } void blink2(void *pvParameters){ pinMode(LED2, OUTPUT); while(1){ xSemaphoreTake(sem,portMAX_DELAY); // take semaphore for(int i=0; i<10; i++){ digitalWrite(LED2, HIGH); delay(50); digitalWrite(LED2, LOW); delay(50); } } } void loop(){}
#include <Arduino_FreeRTOS.h> #include <semphr.h> #define LED1 7 #define LED2 8 SemaphoreHandle_t sem; // create semaphore handle void setup() { sem = xSemaphoreCreateBinary(); // create binary semaphore xTaskCreate( blink1, // Function name of the task "Blink 1", // Name of the task (e.g. for debugging) 128, // Stack size (bytes) NULL, // Parameter to pass 1, // Task priority NULL // Task handle ); xTaskCreate( blink2, // Function name of the task "Blink 2", // Name of the task (e.g. for debugging) 128, // Stack size (bytes) NULL, // Parameter to pass 1, // Task priority NULL // Task handle ); } void blink1(void *pvParameters){ pinMode(LED1, OUTPUT); while(1){ for(int i=0; i<5; i++){ digitalWrite(LED1, HIGH); delay(250); digitalWrite(LED1, LOW); delay(250); } xSemaphoreGive(sem); // release semaphore for(int i=0; i<5; i++){ digitalWrite(LED1, HIGH); delay(250); digitalWrite(LED1, LOW); delay(250); } delay(2000); } } void blink2(void *pvParameters){ pinMode(LED2, OUTPUT); while(1){ xSemaphoreTake(sem,portMAX_DELAY); // take semaphore for(int i=0; i<10; i++){ digitalWrite(LED2, HIGH); delay(50); digitalWrite(LED2, LOW); delay(50); } } } void loop(){}
Binäre Semaphore – Interrupt-gesteuert
Im letzten Beispielsketch für binäre Semaphore möchte ich zeigen, wie ihr Semaphore in einer Interruptroutine (ISR) freigebt. Um den folgenden Sketch auszuprobieren, hängt ihr zwei Taster an euren ESP32 bzw. an den AVR-Arduino. Die Taster verbindet ihr auf der einen Seite mit einem Interruptpin und auf der anderen Seite mit GND. Ansonsten kommen wieder die LEDs zum Einsatz. Ein Taster soll die LED1 zum Blinken bringen, der andere Taster ist für die LED2 verantwortlich.
Eigentlich gibt es bei dieser Aufgabenstellung nur einen einzigen Punkt, der neu ist. Und zwar verwendet ihr in der ISR nicht wie vorher xSemaphoreGive(xSemaphore)
, sondern die Funktion xSemaphoreGiveFromISR(xSemaphore,*pxHigherPriorityTaskWoken)
. Zur Bedeutung des zweiten Parameters schaut hier, in der FreeRTOS Dokumentation. Wir setzen den Parameter im Beispiel der Einfachheit halber auf NULL.
Der Rest des Sketches sollte eigentlich ohne weitere Erklärungen verständlich sein.
#define LED1 25 #define LED2 26 #define INT_PIN_1 15 #define INT_PIN_2 16 SemaphoreHandle_t interruptSemaphore1; // Create semaphore handle SemaphoreHandle_t interruptSemaphore2; void IRAM_ATTR keyISR1() { // ISR definition xSemaphoreGiveFromISR(interruptSemaphore1, NULL); } void IRAM_ATTR keyISR2() { xSemaphoreGiveFromISR(interruptSemaphore2, NULL); } void setup() { interruptSemaphore1 = xSemaphoreCreateBinary(); // Create semaphore interruptSemaphore2 = xSemaphoreCreateBinary(); xTaskCreate( blink1, // Function name of the task "Blink 1", // Name of the task (e.g. for debugging) 2048, // Stack size (bytes) NULL, // Parameter to pass 1, // Task priority NULL // Task handle ); xTaskCreate( blink2, // Function name of the task "Blink 2", // Name of the task (e.g. for debugging) 2048, // Stack size (bytes) NULL, // Parameter to pass 1, // Task priority NULL // Task handle ); if (interruptSemaphore1 != NULL) { attachInterrupt(digitalPinToInterrupt(INT_PIN_1), keyISR1, FALLING); } if (interruptSemaphore2 != NULL) { attachInterrupt(digitalPinToInterrupt(INT_PIN_2), keyISR2, FALLING); } } void blink1(void *pvParameters){ pinMode(LED1, OUTPUT); pinMode(INT_PIN_1, INPUT_PULLUP); while(1){ if (xSemaphoreTake(interruptSemaphore1, portMAX_DELAY)) { for(int i=0; i<10; i++){ digitalWrite(LED1, HIGH); delay(50); // Delay for Tasks digitalWrite(LED1, LOW); delay(50); } } } } void blink2(void *pvParameters){ pinMode(LED2, OUTPUT); pinMode(INT_PIN_2, INPUT_PULLUP); while(1){ if (xSemaphoreTake(interruptSemaphore2, portMAX_DELAY)) { for(int i=0; i<10; i++){ digitalWrite(LED2, HIGH); delay(50); digitalWrite(LED2, LOW); delay(50); } } } } void loop(){}
#include <Arduino_FreeRTOS.h> #include <semphr.h> #define LED1 7 #define LED2 8 #define INT_PIN_1 2 #define INT_PIN_2 3 SemaphoreHandle_t interruptSemaphore1; // Create semaphore handle SemaphoreHandle_t interruptSemaphore2; void keyISR1() { // ISR definition xSemaphoreGiveFromISR(interruptSemaphore1, NULL); } void keyISR2() { xSemaphoreGiveFromISR(interruptSemaphore2, NULL); } void setup() { interruptSemaphore1 = xSemaphoreCreateBinary(); // Create semaphore interruptSemaphore2 = xSemaphoreCreateBinary(); xTaskCreate( blink1, // Function name of the task "Blink 1", // Name of the task (e.g. for debugging) 128, // Stack size (bytes) NULL, // Parameter to pass 1, // Task priority NULL // Task handle ); xTaskCreate( blink2, // Function name of the task "Blink 2", // Name of the task (e.g. for debugging) 128, // Stack size (bytes) NULL, // Parameter to pass 1, // Task priority NULL // Task handle ); if (interruptSemaphore1 != NULL) { attachInterrupt(digitalPinToInterrupt(INT_PIN_1), keyISR1, FALLING); } if (interruptSemaphore2 != NULL) { attachInterrupt(digitalPinToInterrupt(INT_PIN_2), keyISR2, FALLING); } } void blink1(void *pvParameters){ pinMode(LED1, OUTPUT); pinMode(INT_PIN_1, INPUT_PULLUP); while(1){ if (xSemaphoreTake(interruptSemaphore1, portMAX_DELAY)) { for(int i=0; i<10; i++){ digitalWrite(LED1, HIGH); delay(50); digitalWrite(LED1, LOW); delay(50); } } } } void blink2(void *pvParameters){ pinMode(LED2, OUTPUT); pinMode(INT_PIN_2, INPUT_PULLUP); while(1){ if (xSemaphoreTake(interruptSemaphore2, portMAX_DELAY)) { for(int i=0; i<10; i++){ digitalWrite(LED2, HIGH); delay(50); digitalWrite(LED2, LOW); delay(50); } } } } void loop(){}
Zählende Semaphore – Counting Semaphores
Zählende Semaphore, also counting semaphores, erzeugt ihr mit xSemaphoreCreateCounting( uxMaxCount, uxInitialCount )
. Der Parameter uxMaxCount
gibt sozusagen an, wie viele Ausfertigungen dieses Semaphors im Umlauf sein dürfen. Der Parameter uxInitialCount
legt fest, wie viele Ausfertigungen des Semaphors zu Beginn zur Verfügung stehen. Jedes xSemaphoreTake()
reduziert die Anzahl der verfügbaren Semaphore um 1, und jedes xSemaphoreGive()
erhöht sie um 1, sofern die Limits 0 und uxMaxCount
nicht unter- bzw. überschritten werden.
Der Ausdruck xSemaphoreCreateCounting(1,0)
entspricht also xSemaphoreCreateBinary()
. Ihr könnt das ausprobieren, indem ihr die entsprechende Ersetzung im Sketch freertos_esp32_semaphores_binary_I.ino bzw. freertos_avr_semaphores_binary_I.ino vornehmt. Mit xSemaphoreCreateCounting(1,1)
könntet ihr euch auch noch das xSemaphoreGive(sem)
im Setup sparen.
Counting Semaphores – Beispielsketch I
Im ersten Beispielsketch nutzen wir zwei zählende Semaphore und lassen damit wieder zwei LEDs jeweils zehnmal im Wechsel blinken. Den ersten Semaphor (countingSem) verwenden wir wie einen binären Semaphor, um das gleichzeitige Blinken zu verhindern. Der andere Semaphor (countingSem2) begrenzt die Gesamtzahl der Blinksequenzen auf vier. Das erreichen wir, indem wir uxMaxCount
und uxInitialCount
auf 4 setzen und indem die Tasks den countingSem2 nur entgegennehmen, aber nicht zurückgeben.
Über uxSemaphoreGetCount(countingSem2)
halten wir uns informiert, wie viele der countingSem2 Semaphore noch verfügbar sind.
#define LED1 25 #define LED2 26 SemaphoreHandle_t countingSem; // Create handle SemaphoreHandle_t countingSem2; void setup() { Serial.begin(115200); pinMode(LED1, OUTPUT); pinMode(LED2, OUTPUT); countingSem = xSemaphoreCreateCounting(1,1); // Create counting semaphore countingSem2 = xSemaphoreCreateCounting(4,4); // Create second counting semaphore Serial.print("countingSem2 left: "); Serial.println(uxSemaphoreGetCount(countingSem2)); xTaskCreate( blink1, // Function name of the task "Blink 1", // Name of the task (e.g. for debugging) 2048, // Stack size (bytes) NULL, // Parameter to pass 1, // Task priority NULL // Task handle ); xTaskCreate( blink2, // Function name of the task "Blink 2", // Name of the task (e.g. for debugging) 2048, // Stack size (bytes) NULL, // Parameter to pass 1, // Task priority NULL // Task handle ); } void blink1(void *pvParameters){ while(1){ /* Take the semaphore, no semaphore (countingSem) will be left */ xSemaphoreTake(countingSem, portMAX_DELAY); /* Take the semaphore countingSem2 if still available */ xSemaphoreTake(countingSem2, portMAX_DELAY); Serial.print("countingSem2 left: "); Serial.println(uxSemaphoreGetCount(countingSem2)); for(int i=0; i<10; i++){ digitalWrite(LED1, HIGH); delay(250); digitalWrite(LED1, LOW); delay(250); } /* Give only semaphore countingSem */ xSemaphoreGive(countingSem); delay(200); // Short delay is needed! } } void blink2(void *pvParameters){ while(1){ /* Take the semaphore, no semaphore (countingSem) will be left */ xSemaphoreTake(countingSem, portMAX_DELAY); /* Take the semaphore countingSem2 if still available */ xSemaphoreTake(countingSem2, portMAX_DELAY); Serial.print("countingSem2 left: "); Serial.println(uxSemaphoreGetCount(countingSem2)); for(int i=0; i<10; i++){ digitalWrite(LED2, HIGH); delay(333); digitalWrite(LED2, LOW); delay(333); } /* Give only semaphore countingSem */ xSemaphoreGive(countingSem); delay(200); // Short delay is needed! } } void loop(){}
#include <Arduino_FreeRTOS.h> #include <semphr.h> #define LED1 7 #define LED2 8 SemaphoreHandle_t countingSem; // Create handle SemaphoreHandle_t countingSem2; void setup() { Serial.begin(115200); countingSem = xSemaphoreCreateCounting(1,1); // Create counting semaphore countingSem2 = xSemaphoreCreateCounting(4,4); // Create second counting semaphore Serial.print("countingSem2 left: "); Serial.println(uxSemaphoreGetCount(countingSem2)); xTaskCreate( blink1, // Function name of the task "Blink 1", // Name of the task (e.g. for debugging) 128, // Stack size (bytes) NULL, // Parameter to pass 1, // Task priority NULL // Task handle ); xTaskCreate( blink2, // Function name of the task "Blink 2", // Name of the task (e.g. for debugging) 128, // Stack size (bytes) NULL, // Parameter to pass 1, // Task priority NULL // Task handle ); } void blink1(void *pvParameters){ pinMode(LED1, OUTPUT); while(1){ /* Take the semaphore, no semaphore (countingSem) will be left */ xSemaphoreTake(countingSem, portMAX_DELAY); /* Take the semaphore countingSem2 if still available */ xSemaphoreTake(countingSem2, portMAX_DELAY); Serial.print("countingSem2 left: "); Serial.println(uxSemaphoreGetCount(countingSem2)); for(int i=0; i<10; i++){ digitalWrite(LED1, HIGH); delay(250); digitalWrite(LED1, LOW); delay(250); } /* Give only semaphore countingSem */ xSemaphoreGive(countingSem); delay(200); // Short delay is needed! } } void blink2(void *pvParameters){ pinMode(LED2, OUTPUT); while(1){ /* Take the semaphore, no semaphore (countingSem) will be left */ xSemaphoreTake(countingSem, portMAX_DELAY); /* Take the semaphore countingSem2 if still available */ xSemaphoreTake(countingSem2, portMAX_DELAY); Serial.print("countingSem2 left: "); Serial.println(uxSemaphoreGetCount(countingSem2)); for(int i=0; i<10; i++){ digitalWrite(LED2, HIGH); delay(333); digitalWrite(LED2, LOW); delay(333); } /* Give only semaphore countingSem */ xSemaphoreGive(countingSem); delay(200); // Short delay is needed! } } void loop(){}
Kleiner Hinweis noch: Wenn ihr Tasks nach x-maliger Ausführung tatsächlich nicht mehr benötigt, dann solltet ihr sie mit vTaskDelete( taskHandle )
löschen, damit ihr den Speicherplatz zurückbekommt. Ich habe davon im Beispiel abgesehen, um den Blick auf das Wesentliche zu lenken.
Counting Semaphores – Beispielsketch II
Im zweiten Beispielsketch lassen wir vier LEDs blinken, beschränken aber die Zahl der LEDs, die gleichzeitig blinken dürfen, auf zwei. Das realisieren wir mit einem zählenden Semaphor, den wir auf uxMaxCount = 2 setzen.
#define LED1 25 #define LED2 26 #define LED3 17 #define LED4 18 SemaphoreHandle_t countingSem; // Create semaphore handle void setup() { countingSem = xSemaphoreCreateCounting(2,2); // Create counting semaphore xTaskCreate(blink1, "Blink 1", 2048, NULL, 1, NULL); xTaskCreate(blink2, "Blink 2", 2048, NULL, 1, NULL); xTaskCreate(blink3, "Blink 3", 2048, NULL, 1, NULL); xTaskCreate(blink4, "Blink 4", 2048, NULL, 1, NULL); } void blink1(void *parameter){ pinMode(LED1, OUTPUT); while(1){ /* Take a semaphore if available */ xSemaphoreTake(countingSem, portMAX_DELAY); for(int i=0; i<10; i++){ digitalWrite(LED1, HIGH); delay(250); digitalWrite(LED1, LOW); delay(250); } /* Release the semaphore */ xSemaphoreGive(countingSem); delay(200); // Short delay is needed } } void blink2(void *parameter){ pinMode(LED2, OUTPUT); while(1){ /* Take a semaphore if available */ xSemaphoreTake(countingSem, portMAX_DELAY); for(int i=0; i<10; i++){ digitalWrite(LED2, HIGH); delay(333); digitalWrite(LED2, LOW); delay(333); } /* Release the semaphore */ xSemaphoreGive(countingSem); delay(200); // Short delay is needed } } void blink3(void *parameter){ pinMode(LED3, OUTPUT); while(1){ /* Take a semaphore if available */ xSemaphoreTake(countingSem, portMAX_DELAY); for(int i=0; i<10; i++){ digitalWrite(LED3, HIGH); delay(123);//delay(333); digitalWrite(LED3, LOW); delay(123); } /* Release the semaphore */ xSemaphoreGive(countingSem); delay(200); // Short delay is needed } } void blink4(void *parameter){ pinMode(LED4, OUTPUT); while(1){ /* Take a semaphore if available */ xSemaphoreTake(countingSem, portMAX_DELAY); for(int i=0; i<10; i++){ digitalWrite(LED4, HIGH); delay(444);//delay(333); digitalWrite(LED4, LOW); delay(444); } /* Release the semaphore */ xSemaphoreGive(countingSem); delay(200); // Short delay is needed } } void loop(){}
#include <Arduino_FreeRTOS.h> #include <semphr.h> #define LED1 7 #define LED2 8 #define LED3 9 #define LED4 10 SemaphoreHandle_t countingSem; // Create semaphore handle void setup() { countingSem = xSemaphoreCreateCounting(2,2); // Create counting semaphore xTaskCreate(blink1, "Blink 1", 128, NULL, 1, NULL); xTaskCreate(blink2, "Blink 2", 128, NULL, 1, NULL); xTaskCreate(blink3, "Blink 3", 128, NULL, 1, NULL); xTaskCreate(blink4, "Blink 4", 128, NULL, 1, NULL); } void blink1(void *parameter){ pinMode(LED1, OUTPUT); while(1){ /* Take a semaphore if available */ xSemaphoreTake(countingSem, portMAX_DELAY); for(int i=0; i<10; i++){ digitalWrite(LED1, HIGH); delay(250); digitalWrite(LED1, LOW); delay(250); } /* Release the semaphore */ xSemaphoreGive(countingSem); delay(200); // Short delay is needed } } void blink2(void *parameter){ pinMode(LED2, OUTPUT); while(1){ /* Take a semaphore if available */ xSemaphoreTake(countingSem, portMAX_DELAY); for(int i=0; i<10; i++){ digitalWrite(LED2, HIGH); delay(333);//delay(333); digitalWrite(LED2, LOW); delay(333); } /* Release the semaphore */ xSemaphoreGive(countingSem); delay(200); // Short delay is needed } } void blink3(void *parameter){ pinMode(LED3, OUTPUT); while(1){ /* Take a semaphore if available */ xSemaphoreTake(countingSem, portMAX_DELAY); for(int i=0; i<10; i++){ digitalWrite(LED3, HIGH); delay(123);//delay(333); digitalWrite(LED3, LOW); delay(123); } /* Release the semaphore */ xSemaphoreGive(countingSem); delay(200); // Short delay is needed } } void blink4(void *parameter){ pinMode(LED4, OUTPUT); while(1){ /* Take a semaphore if available */ xSemaphoreTake(countingSem, portMAX_DELAY); for(int i=0; i<10; i++){ digitalWrite(LED4, HIGH); delay(444);//delay(333); digitalWrite(LED4, LOW); delay(444); } /* Release the semaphore */ xSemaphoreGive(countingSem); delay(200); // Short delay is needed } } void loop(){}
Mutex-Objekte
Wenn ihr die binären und zählenden Semaphore verstanden habt, dann sind die Mutex-Objekte für euch keine Herausforderung mehr. Ihr erzeugt einen Mutex mit xSemaphoreCreateMutex()
. Ansonsten geht ihr damit genauso um, wie ihr das von den Semaphoren gewohnt seid. Der Mutex muss nicht über ein xSemaphoreGive()
verfügbar gemacht werden. Insofern verhält sich ähnlich wie ein zählender Semaphor, den ihr mit xSemaphorCreateCounting(1,1)
erzeugt.
Interessant ist vielleicht noch, dass ihr mit xSemaphoreGetMutexHolder()
herausbekommen könnt, welcher Task den Mutex gerade besitzt. Eine solche Funktion gibt es für die Semaphore nicht.
Hier ein Beispielsketch, der die nahe Verwandtschaft zum Semaphor verdeutlicht.
#define LED1 25 #define LED2 26 SemaphoreHandle_t mutex; // Create handle void setup() { mutex = xSemaphoreCreateMutex(); //Create the mutex object xTaskCreate( blink1, // Function name of the task "Blink 1", // Name of the task (e.g. for debugging) 2048, // Stack size (bytes) NULL, // Parameter to pass 1, // Task priority NULL // Task handle ); xTaskCreate( blink2, // Function name of the task "Blink 2", // Name of the task (e.g. for debugging) 2048, // Stack size (bytes) NULL, // Parameter to pass 1, // Task priority NULL // Task handle ); } void blink1(void *pvParameters){ pinMode(LED1, OUTPUT); while(1){ xSemaphoreTake(mutex,portMAX_DELAY); // Take the mutex for(int i=0; i<10; i++){ digitalWrite(LED1, HIGH); delay(250); digitalWrite(LED1, LOW); delay(250); } xSemaphoreGive(mutex); // Releases the mutex delay(200); // Short delay is needed! } } void blink2(void *pvParameters){ pinMode(LED2, OUTPUT); while(1){ xSemaphoreTake(mutex,portMAX_DELAY); // Take the mutex for(int i=0; i<10; i++){ digitalWrite(LED2, HIGH); delay(333); digitalWrite(LED2, LOW); delay(333); } xSemaphoreGive(mutex); // // Release the mutex delay(200); // Short delay is needed! } } void loop(){}
#include <Arduino_FreeRTOS.h> #include <semphr.h> #define LED1 7 #define LED2 8 SemaphoreHandle_t mutex; void setup() { mutex = xSemaphoreCreateMutex(); xTaskCreate( blink1, // Function name of the task "Blink 1", // Name of the task (e.g. for debugging) 128, // Stack size (bytes) NULL, // Parameter to pass 1, // Task priority NULL // Task handle ); xTaskCreate( blink2, // Function name of the task "Blink 2", // Name of the task (e.g. for debugging) 128, // Stack size (bytes) NULL, // Parameter to pass 1, // Task priority NULL // Task handle ); } void blink1(void *pvParameters){ pinMode(LED1, OUTPUT); while(1){ xSemaphoreTake(mutex,portMAX_DELAY); for(int i=0; i<10; i++){ digitalWrite(LED1, HIGH); delay(250); digitalWrite(LED1, LOW); delay(250); } xSemaphoreGive(mutex); delay(200); // Short delay is needed! } } void blink2(void *pvParameters){ pinMode(LED2, OUTPUT); while(1){ xSemaphoreTake(mutex,portMAX_DELAY); for(int i=0; i<10; i++){ digitalWrite(LED2, HIGH); delay(333); digitalWrite(LED2, LOW); delay(333); } xSemaphoreGive(mutex); delay(200); /// Short delay is needed! } } void loop(){}
Parameter an einen Task übergeben
Einmalige Übergabe als *pvParameters
Einen der Parameter für xCreateTask()
, nämlich *pvParameters
haben wir bisher nicht genutzt. Das wollen wir jetzt ändern. *pvParameters
ermöglicht uns, Parameter an den zu kreierenden Task zu übergeben. Es liegt in der Natur der Sache, dass wir diesen Zugang nur einmal, nämlich bei der Taskerstellung verwenden können.
In den Beispielsketchen haben wir viele Blink-Taskfunktionen verwendet, die sich lediglich in der Blinkperiode und der zu steuernden LED unterschieden haben. Hier kann uns die Parameterübergabe helfen, den Code zu straffen. Allerdings müssen wir dafür ein wenig mit Zeigern jonglieren.
Wir spielen das an einem einfachen Sketch durch. Hier zunächst der Code.
#define LED1 25 #define LED2 26 struct genericBlink{ int pin; int period; }; genericBlink ledBlink1 = {LED1, 500}; genericBlink ledBlink2 = {LED2, 333}; void setup() { xTaskCreate( &blink, // Function name of the task "Blink 1", // Name of the task (e.g. for debugging) 2048, // Stack size (bytes) (void*) &ledBlink1, // Parameter to pass 1, // Task priority NULL // Task handle ); xTaskCreate( &blink, // Function name of the task "Blink 2", // Name of the task (e.g. for debugging) 2048, // Stack size (bytes) (void*) &ledBlink2, // Parameter to pass 1, // Task priority NULL // Task handle ); } void blink(void *ledx){ genericBlink *ledBlink = (genericBlink *) ledx; pinMode(ledBlink->pin, OUTPUT); while(1){ digitalWrite(ledBlink->pin, HIGH); delay(ledBlink->period); digitalWrite(ledBlink->pin, LOW); delay(ledBlink->period); } } // void blink(void *ledx){ // genericBlink ledBlink = *(genericBlink *) ledx; // pinMode(ledBlink.pin, OUTPUT); // while(1){ // digitalWrite(ledBlink.pin, HIGH); // delay(ledBlink.period); // digitalWrite(ledBlink.pin, LOW); // delay(ledBlink.period); // } // } void loop(){}
#include<Arduino_FreeRTOS.h> #define LED1 7 #define LED2 8 struct genericBlink{ int pin; int period; }; genericBlink ledBlink1 = {LED1, 500}; genericBlink ledBlink2 = {LED2, 333}; void setup() { xTaskCreate( &blink, // Function name of the task "Blink 1", // Name of the task (e.g. for debugging) 128, // Stack size (bytes) (void*) &ledBlink1, // Parameter to pass 1, // Task priority NULL // Task handle ); xTaskCreate( &blink, // Function name of the task "Blink 2", // Name of the task (e.g. for debugging) 128, // Stack size (bytes) (void*) &ledBlink2, // Parameter to pass 1, // Task priority NULL // Task handle ); } void blink(void *ledx){ genericBlink *ledBlink = (genericBlink *) ledx; pinMode(ledBlink->pin, OUTPUT); while(1){ digitalWrite(ledBlink->pin, HIGH); delay(ledBlink->period); digitalWrite(ledBlink->pin, LOW); delay(ledBlink->period); } } // void blink(void *ledx){ // genericBlink ledBlink = *(genericBlink *) ledx; // pinMode(ledBlink.pin, OUTPUT); // while(1){ // digitalWrite(ledBlink.pin, HIGH); // delay(ledBlink.period); // digitalWrite(ledBlink.pin, LOW); // delay(ledBlink.period); // } // } void loop(){}
Erklärungen zum Sketch
Um Daten als Zeiger zu übergeben, bieten sich Arrays oder Strukturen (struct) an. Strukturen sind das Mittel der Wahl, wenn mehrere Parameter unterschiedlichen Datentyps übergeben werden sollen. In unserem Beispielsketch dazu definieren wir eine Struktur „genericBlink“, die die LED- bzw. Pinnummer und die Blinkperiode enthält. Die Variablen ledBlink1 und ledBlink2 sind Implementierungen dieser Struktur. ledBlink1 und ledBlink2 übergeben wir den Tasks in xCreateTask()
als Referenz, also mit dem voranstehenden &
.
Der Vorteil der Parameterübergabe ist, dass wir uns auf eine Blinkfunktion beschränken können.
Die Blinkfunktion „weiß“ nicht, welchen Datentyp der Zeiger *ledx
repräsentiert. Durch die Zeile:
genericBlink *ledBlink = (genericBlink *) ledx;
bekommen wir aus dem undefinierten Zeiger ledx den Zeiger ledBlink, von dem die Funktion jetzt weiß, dass er auf eine Struktur des Typs genericBlink zeigt. Damit können wir auf die Elemente der Struktur zugreifen. Da ledBlink aber nicht die Struktur selbst ist, sondern nur der Zeiger auf die Struktur, ist der Zugriff indirekt und wir müssen deswegen den Pfeiloperator ->
anstelle des Punktoperators verwenden.
Die Alternative wäre, eine Kopie der übergebenen Struktur zu erzeugen (was mehr Speicher kostet), so wie in den auskommentierten Zeilen des Beispielsketches. Dafür muss der Zeiger mit einem *
dereferenziert werden:
genericBlink ledBlink = *(genericBlink *) ledx;
Genug der Zeiger an dieser Stelle. Wer mehr über das Thema wissen möchte, der schaue z. B. hier.
Parameterübergabe mit Queues
Übergabe einfacher Datentypen mit Queues
Wenn Tasks während ihrer Laufzeit Daten austauschen sollen, dann müsst ihr zu den sogenannten Queues greifen. Wir schauen uns das an einem einfachen, an sich ziemlich sinnlosen Beispiel an. Dabei kommen zwei Tasks zum Einsatz. Der eine Task ermittelt die Zeit seit Programmstart und teilt sie dem anderen Task mit, der sie dann auf dem Bildschirm ausgibt.
Dabei kommen vier neue Elemente bzw. Funktionen zum Einsatz:
QueueHandle_t xQueue
: erzeugt den Queue HandlexQueue
.xQueueCreate(uxQueueLength, uxItemSize)
: erzeugt eine Queue.uxQueueLength
ist die maximale Anzahl der zu übergebenden Elemente.uxItemSize
definiert die Größe der einzelnen Elemente in Byte.xQueueSend(xQueue, *pvItemToQueue, xTicksToWait)
: sendet die Queue. Dabei istxQueue
der Queue Handle und*pvItemToQueue
ist der Zeiger zu den zu übergebenden Daten.xTicksToWait
ist die maximale Zeit in Ticks, die der Task warten soll, sozusagen um Sendezeit zu bekommen – oder noch bildlicher: um einen Platz in der Schlange zu ergattern.xQueueReceive(xQueue, *pvBuffer, xTicksToWait)
: empfängt die Queue mit dem HandlexQueue
.*pvBuffer
ist der Zeiger zur Adresse, an den die Daten kopiert werden.xTicksToWait
ist die maximale Zeit in Ticks, die der empfangende Task warten soll, bis er Daten erhält.
QueueHandle_t queue1; // Create handle void setup() { Serial.begin(115200); queue1 = xQueueCreate(1, sizeof(unsigned long)); // Create queue xTaskCreate( measureTime, // Function name of the task "Measure Time", // Name of the task (e.g. for debugging) 2048, // Stack size (bytes) NULL, // Parameter to pass 1, // Task priority NULL // Task handle ); xTaskCreate( printTime, // Function name of the task "Print time", // Name of the task (e.g. for debugging) 2048, // Stack size (bytes) NULL, // Parameter to pass 1, // Task priority NULL // Task handle ); } void measureTime(void *pvParameters){ while(1){ static unsigned long startTime = millis(); unsigned long timeSinceStart = (millis() - startTime)/1000; xQueueSend(queue1, &timeSinceStart, 0); // Send queue delay(100); } } void printTime(void *pvParameters){ while(1){ unsigned long buf; xQueueReceive(queue1, &buf, 0); // Receive queue Serial.print("Time since start: "); Serial.println(buf); delay(2000); } } void loop(){}
#include <Arduino_FreeRTOS.h> #include <queue.h> QueueHandle_t queue1; // Create handle void setup() { Serial.begin(9600); queue1 = xQueueCreate(1, sizeof(unsigned long)); // Create queue xTaskCreate( measureTime, // Function name of the task "Measure Time", // Name of the task (e.g. for debugging) 128, // Stack size (bytes) NULL, // Parameter to pass 1, // Task priority NULL // Task handle ); xTaskCreate( printTime, // Function name of the task "Print time", // Name of the task (e.g. for debugging) 128, // Stack size (bytes) NULL, // Parameter to pass 1, // Task priority NULL // Task handle ); } void measureTime(void *pvParameters){ while(1){ static unsigned long startTime = millis(); unsigned long timeSinceStart = (millis() - startTime)/1000; xQueueSend(queue1, &timeSinceStart, 0); // Send queue delay(100); } } void printTime(void *pvParameters){ while(1){ unsigned long buf; xQueueReceive(queue1, &buf, 0); // Receive queue Serial.print("Time since start: "); Serial.println(buf); delay(2000); } } void loop(){}
Ich denke, der Sketch braucht keine großen Erklärungen, oder? Wieder ist zu beachten, dass die Daten als Zeiger übergeben bzw. empfangen werden, also vergesst nicht die &
-Operatoren. Und probiert einfach einmal aus, was passiert, wenn ihr das delay()
in measureTime()
auf beispielsweise 10000 setzt. In einem zweiten Schritt könntet ihr zusätzlich die xTicksToWait
in printTime()
auf 15000 setzen.
Übergabe von Strukturen mit Queues
Als letzten Punkt dieser FreeRTOS Einführung schauen wir uns die Übergabe einer Struktur an einen Task an. In dem Beispiel dazu besteht die Struktur aus dem Ergebnis eines analogRead()
(also irgendeiner imaginären Sensormessung) und dem Zeitpunkt der Messung.
#define SENSOR_PIN 34 QueueHandle_t queue1; // Create handle struct dataPack{ // define struct for the variables to be sent unsigned long sensorTime; int sensorValue; }; void setup() { Serial.begin(115200); queue1 = xQueueCreate(1, sizeof(dataPack)); // Create queue xTaskCreate( getSensorData, // Function name of the task "Get Sensor Data", // Name of the task (e.g. for debugging) 2048, // Stack size (bytes) NULL, // Parameter to pass 1, // Task priority NULL // Task handle ); xTaskCreate( printSensor, // Function name of the task "Print sensor", // Name of the task (e.g. for debugging) 2048, // Stack size (bytes) NULL, // Parameter to pass 1, // Task priority NULL // Task handle ); } void getSensorData(void *pvParameters){ static unsigned long startTime = millis(); while(1){ dataPack sensorData = {0,0}; sensorData.sensorTime = (millis() - startTime)/1000; sensorData.sensorValue = (analogRead(SENSOR_PIN)); xQueueSend(queue1, &sensorData, 0); // Send queue delay(500); } } void printSensor(void *pvParameters){ while(1){ dataPack currentData; xQueueReceive(queue1, ¤tData,0); // Receive queue Serial.print("Time: "); Serial.println(currentData.sensorTime); Serial.print("Value: "); Serial.println(currentData.sensorValue); delay(2000); } } void loop(){}
#include <Arduino_FreeRTOS.h> #include <queue.h> #define SENSOR_PIN A0 QueueHandle_t queue1; // Create handle struct dataPack{ // Create struct for the data to be sent unsigned long sensorTime; int sensorValue; }; void setup() { Serial.begin(9600); queue1 = xQueueCreate(1, sizeof(dataPack)); // Create queue xTaskCreate( getSensorData, // Function name of the task "Get Sensor Data", // Name of the task (e.g. for debugging) 128, // Stack size (bytes) NULL, // Parameter to pass 1, // Task priority NULL // Task handle ); xTaskCreate( printSensor, // Function name of the task "Print sensor", // Name of the task (e.g. for debugging) 128, // Stack size (bytes) NULL, // Parameter to pass 1, // Task priority NULL // Task handle ); } void getSensorData(void *pvParameters){ while(1){ static unsigned long startTime = millis(); dataPack sensorData = {0,0}; sensorData.sensorTime = (millis() - startTime)/1000; sensorData.sensorValue = (analogRead(SENSOR_PIN)); xQueueSend(queue1, &sensorData, 0); // Send queue delay(500); } } void printSensor(void *pvParameters){ while(1){ dataPack currentData; xQueueReceive(queue1, ¤tData, 0); // Receive queue Serial.print("Time: "); Serial.println(currentData.sensorTime); Serial.print("Value: "); Serial.println(currentData.sensorValue); delay(2000); } } void loop(){}
Der Sketch sollte eigentlich keine weiteren Erklärungen benötigen.
Super informativer Beitrag, vielen Dank!
Macht es Sinn, Callbacks in Tasks zu packen? Mein ESP32 empfängt und sendet BLE-Nachrichten und empfängt zudem noch ESPNOW-Nachrichten von Sensoren. Außerdem werden empfangende Daten mit LittleFS in eine Datei geschrieben. Oder sind Callbacks schon so etwas wie eine Art Task und könnten auch zwei gleichzeitig eintreffende Nachrichten von unterschiedlichen Sensoren abarbeiten? Dann würde ich mir das an dieser Stelle nämlich sparen können 😉
Das Schreiben in eine Datei würde ich schon mal gefühlsmäßig in einen Task packen, damit immer nur eine Operation schreibenden Zugriff auf die Datei erhält und alle anderen warten müssen.
Hallo Christian,
ich kann dir das nicht fundiert beantworten. Bevor ich spekuliere, bin ich lieber still. Ich würde immer erst einmal einfach beginnen und schauen, ob es funktioniert. Komplizierter machen kann man die Dinge immer noch. Vielleicht kannst du mal bei den Kollegen von Random Nerd Tutorials fragen, z.B. hier:
https://randomnerdtutorials.com/esp-now-esp32-arduino-ide/
Die sind etwas spezialisierter aus ESP32 als ich.
VG, Wolfgang
Hi Wolfgang,
danke trotzdem für deine Antwort und auch für diesen tollen Blog hier 🙂
Ausgezeichneter Beitrag. Endlich einmal systematisch und mit einer guten Gliederung aufgebaut.
Vielen Dank!!
Super Beitrag, keine Frage, bei meiner „heavy load“ Anwendung hat sich am Ende doch die klassische loop() mit einem globalCnt als die bessere und stabile Variante herausgestellt. Immerhin liefen aber 8 Tasks parallel, aber mit dem Sync ist so eine Sache, der eine fummelt auf dem TFT herum, der andere vielleicht auch; nicht einfach.
Wie auch immer: sehr interessant, danke.
Ja, bei zu vielen Tasks wird es unübersichtlich. Zum einen muss man bei den Zugriffen auf bestimmte Ressourcen aufpassen, zum anderen funktioniert das eine oder andere unter Umständen nicht, wenn es nur ein Achtel der Zeit bekommt und so in Länge gezogen wird. Und Semaphore können auch schon bei weit weniger Tasks verwirren.
Super, hat mir weitergeholfen. Mach weiter so. Hatte bis jetzt keine Zeit, mich in RTOS so richtig reinzuarbeiten. Hoffe, es kommt noch weiteres vom RTOS. Ich benutze den ESP32.
Klaus
Toll. Danke. Und wieder zur rechten Zeit. So ne Art Magie. ;o}