Last Updated on 18. Januar 2021 by sfambach
Ein Task ist eine (wiederkehrende) Aufgabe für den Prozessor. Mehrere Tasks ringen um die Aufmerksamkeit der CPU. Wie diese erstellt werden und welche Datenstrukturen zur Datenübermittlung und Synchronisation vorhanden sind, wird in diesem Beitrag kurz umrissen.
Tasks können zu einer sehr komplexen Angelegenheit werden, weshalb ich hier nur die Spitze des Eisberges ankratzen kann. Dennoch handelt es sich hierbei, um den bisher längsten Beitrag der Serie. Für die weitere Recherche sei auch auf die Espressif Website – Kapitel FreeRtos verwiesen.
Grundsätzliches
Tasks können als gekapselte Aufgaben betrachtet werden. In der Espressif IDF können je nach Anzahl der Prozessor-Kerne ein oder mehrere Task parallel/geleichzeitig ausgeführt werden. Grundsätzlich können aber mehrere Tasks gestartet werden, die um den/die Prozessorkern(e) konkurieren. Immer wenn ein Task den Prozessorkern nicht mehr braucht gibt er ihn frei und der nächste Tasks kommt an die Reihe.
Welcher Task als nächstes bevorzugt wird hängt grundsätzlich mit seiner Priorität zusammen und wie lange er schon wartet. Wie genau die Abarbeitungsreihenfolge ist hängt von der Implementierung des Shedulers ab. Die Tasks in der IDF stammen aus dem FreeRtos hier wacht zusätzlich ein Wachhund (Watchdog) darüber, dass die Task nicht zu lange brauchen. Weiterhin wird davon ausgegangen das Tasks ewig laufen, sollte ein Task beendet werden muss dieser vorher gelöscht werden sonst kommt es zu einem Fehler.
Viele der im folgenden beschiebenen Funktionen gibt es mehrfach. Zum einen kann bei der Erstellung bestimmt werden, ob der Speicher dynamisch oder statisch zur Kompilierzeit reserviert werden soll. Die statischen Funktionen enden immer mit static.
Zum Anderen gibt es Funktionen die aus einem Interrupt gerufen werden können, diese enden immer mit ‚FromISR‘.
Es wird im fogenden meist nur auf die Funktionen mit dynamischer Speicher Reservierung eingegangen, für weitere Infos sei auch hier auf die IDF Referenz verwiesen.
Task Erstellen
Bibliotheken:
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
Erstellt werden Tasks mit dem Befehl:
xTaskCreate(
<pointer zur Task Funktion,
<Task Name>,
<Speicher für Task>,
<Task parameter>,
<Priotität>,
<Referenz für Handle>);
Gut Programmierpraxis ist es einen Task immer nur so lange laufen zu lassen wie wirklich nötig ist. Muss ein Task auf einen Wert oder Aktion warten sollte er sich schlafen legen und den Prozessor an einen anderen Task weiter geben. Das Schlafen kann mit der folgenden Funktion eingeleitet werden.
void vTaskDelay(const TickType_t xTicksToDelay);
Mit diesem Befehl wartet der Task eine bestimmte Zeit bevor er wieder bereit zur Auführung ist. Generell schlafen legen kann man den Task mit dem folgenden Befehl:
// speziellen Task anhalten
void vTaskSuspend(<TaskHandle>);
// Task aus interupt routine anhalten
BaseType_t xTaskResumeFromISR(<TaskHandle>)
// eigenen Task anhalten
vTaskSuspend(NULL);
In diesem Fall wartet der Task bis er wieder von aussen aktiviert wird. Dies geschieht mit dem folgenden Befehl:
// normale Freigabe
void xTaskResume(<TaskHandle>);
// aus Interrupt Routine
void xTaskResumeFromISR(<TaskHandle>);
Freetos Task werden normal nicht beendet, wenn sie wie im Beispiel dennoch enden, wird ein Fehler geworfen. Um dies zu verhindern muss der Task vor Ende gelöscht werden. Dies geschieht mit der Anweisung:
vTaskDelete( xHandles[task_index] );
Zu Beachten ist, wenn wie im Beispiel das xTaskDelete in seiner eigenen Task-Funktion aufgerufen wird, werden alle Anweisung die auf diesen Befehl folgen nicht mehr ausgeführt, weil der Task dann schon gelöscht ist.
Das folgende Beispiel erstellt mehrere Task, die unterschiedlich lange laufen und wenn sie aktiv sind eine Ausgabe erzeugen:
Synchronisation
Freetos bietet Datenstrukturen zur Synchronisation und Übergabe der Daten von einem Task zum anderen. Folgende Hilfmittel konnte ich finden:
- Queues - Dies ist eine Liste. Hier wartet der lesende Task bis ein Eintrag in der Liste vorhanden ist.
- Semaphoren - Werden genutzt um Task mit einander zu Synchronisieren oder gemeinsam genutze Resourcen vor Datennfehlern zu schützen.
- Event Gruppen - Werden zur Synchronisation von Tasks verwendet, sie warte auf das setzen definierter Bits durch Tasks. Warten kann die Grupe auf ein oder alle Bits. Sind die Bits gesetzt, wird das Warten der Gruppe beendet.
- Stream Buffer - Datenübertragung per Stream. Der lesende Task wartet so lange bis im Stream Daten vorhanden sind.
- Message Buffer - Basiert auf dem Stream Buffer. Ein Task kann Nachrichten in den Buffer legen und ein anderer kann auf sie warten.
- Hooks - Werden verwendet um Task auszuführen wenn der Prozessor gerade nicht zu tun hat.
- Ring Buffer - Ein Rinbuffer ist eine Datenstruktur mit einem fixen Speicher. Wird das Ende des Speichers erreicht wird versucht im vorderen Bereich wieder Speicher zu finden. Da der Rinbuffer etwas Komplexer ist habe ich diesen erstmal aussen vor gelassen.
- Timer - Timer starten Tasks einmalig oder periodisch, für Timer habe ich einen eigenen Beitrag erstellt.
Werden diese Datenstrukturen aus Interruptroutinen heraus benutzt, müssen die gleichnamigen ISR Funktionen zum Aufruf verwendet werden.
Es wird bei den Datenstrukturen, ausser bei den Semaphoren immer davon ausgegangen dass es nur einen Sende-Task und einen Empfänger-Task gib. Wollen beispielsweise mehere Ressourcen senden, muss die Sendefunktion als kritischer Breich (z.B. mit Semaphor) geschützt werden.
Queues
Queues werden verwendet um Daten auszutauschen. Der sendende Task übergibt Daten an die Queue und kann danach weiter laufen. Der konsumierende Task holt sich die Daten wenn er bereit ist.
Bibliothek:
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
Queue erstellen:
QueueHandle_t xQueue;
xQueue = xQueueCreate( <Länge Queue>, <größe der Fehler>);
if( xQueue == 0 )
{
// Fehler Erkennung
ESP_LOGE(TAG,"Error during queue creation");
}
Daten senden:
if(!xQueueSendToBack(<queue>, (void*)&<wert>, <timeout>)) {
// queue voll
ESP_LOGW(TAG,"q - Queue Full");
}
Daten emfangen:
if( ! xQueueReceive( <queu>, <value>, <timeout>) ){
ESP_LOGW(TAG,"q - Queue empty!");
}
Das Beispiel unten arbeitet mit zwei Tasks. Einer füllt die Queue der andere liest die Werte aus. Dadurch, dass der Lese-Task am Anfang langsamer als der Schreibe-Task ist, wird die Queue gefüllt, bis sie voll ist und keinen Wert mehr aufnehmen kann. Durch die folgende Beschleunigung des Lese-Tasks, wird die Queue kürzer bis sie leer ist und der Lese-Task auf den schreiben Task warten muss.
Semaphoren
Semaphoren und Muteces dienen dazu Bereiche zu sichern und Threads zu synchronisieren. Ich beschreibe hier die Handhabung mit Hilfe des binären Semaphors. Muteces und recursive Semaphore werden analog erstellt habe aber noch zusätzliche Funktionalitäten.
Bibliothek:
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
Semaphor erstellen:
SemaphoreHandle_t xSemaphore = NULL;
xSemaphore = xSemaphoreCreateBinary();
if( xSemaphore == NULL ){
// Fehler
}
Vor der ersten Nutzung muss der Semaphor einmal Freigegeben werden.
Semaphor holen:
if( xSemaphoreTake( <Semaphor Handle>, timeout) == pdTRUE )
{
//hat geklapp ...
xSemaphoreGive( <Semaphor Handle> );
} else {
// hat nicht geklappt
}
Freigeben des Semaphor:
xSemaphoreGive( <Semaphor Handle>);
Das Bespiel unten erstellt 5 Tasks. Alle möchten eine Variable hochzählen. Damit es zu keinen Datenfehlern kommt ist der kritische Bereiche (hochzählen) mit einem Semaphor gesichert. Innerhalb dieses Bereiches wird den anderen Tasks kurz der Prozessor übergeben, so dass sie versuchen können den Semaphor zu belgen. Es wird hier explizit der Wert der Variablen erst Kopiert, dann den anderen Prozessen die Gelegenheit zur Änderung gegeben und dann erst der geänderte Wert zurück geschieben. Dies ist keine gute Praxis, hilft aber dem Beispiel ;). Ihr könnt den Semaphor heraus nehmen indem ihr das u.s. define ausdokumentiert. Die Tasks verhalten sich nun anders ;).
// (de)activat this to dis-/en-able the semaphores
#define enable_semaphore
Zu den rekursiven Mutece sei noch gesagt, diese können immer wieder von Besitzer-Task belegt werden. Die Anzahl der Belegungen wird mit gezählt und muss sich auch in der Anzahl der Freigaben wiederspiegeln. Diese können beispielsweise für Recursionen verwendet werden, bei denen die gleiche Funktion immer wieder, indem sie sich selbst aufruft, auf die selben Daten zugreifen muss.
Event Groups
Bibliothek:
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
Bit Setzen:
EventBits_t xEventGroupSetBits(
<Eventgruppen Handle>,
<Bit welches gesetzt werden soll>
);
Bei der Wiederverwendung einer bestehenden Gruppe müssen die Bits zurückgesetzt werden.
EventBits_t xEventGroupClearBits(
<Gruppen Handle>,
<Bits zum Rücksetzten>
);
Warte gesetzten Bits, dies geschieht mit:
xEventGroupWaitBits(
<Gruppen Handle>,
<BIT_0 | BIT_1| ... | BIT_n>,
<Bit Löschen vor Rückgabe>,
<Ein- od. Alle Bits>,
<Wie lange soll auf die Bits gewartet werden>
);
// Auswertung
if( xEventGroup == NULL )
{
// EVG konnte nicht erstellt werden
ESP_LOGE(TAG,"Event groupe not created");
} else if( (uxBits & ( BIT_0 | BIT_1| ... | BIT_n) ) == ( BIT_0 | BIT_1| BIT_2| ... | BIT_n ) )
{
// alle bits wurden gesetzt
ESP_LOGI(TAG,"Program finshed successfull");
} else {
// nicht alle Bits wurden gesetzt, timeout
ESP_LOGW(TAG,"Timeout");
}
Hier ein komplettes Beispiel mit mehreren Tasks. Der Haupttask wartet darauf, dass alle Bit gesetzt werden. Sind alle vorhanden arbeitet der Haupttask weiter.
Stream Buffer
Bibliothek:
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/stream_buffer.h"
Erstellen:
Triggerlevel und MaxAnzBytes sind beide vom Typ const size_t. Ersteres gibt an wie groß der Bufferstream ist. Zweiteres beschreibt ab wieviel Bytes der Empfänger aus seiner Wartehaltung erlöst wird.
xStreamBufferCreate(<MaxAnzBytes>, <TriggerLevel>)
Funktion zum Senden:
Die Funktion nimmt einen Streambuffer und die zu sendenden Daten auf. Der Rückgabewert liefert die Anzahl der gesendeten Daten zurück.
size_t xStreamBufferSend(
<StreamBuffer>,
<SendeDaten>,
<LängeDerSendeDaten>,
<Timeout>)
Empfangen:
size_t xStreamBufferReceive(
<Stream Buffer>,
( void * )&<Buffer für Empfangsdaten>,
<Göße Emfpangsbuffer>,
<Timeout> );
Beispiel Programm
Message Buffer
Der Messagebuffer baut auf dem Streambuffer auf. Zusätzlich könen die Daten in Nachrichten gekapselt werden. Eine Nachricht wird durch ihre Länge definiert. Vor Empfang einer Nachricht kann die Länger vom Puffer erfragt werden.
Bibliothek:
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/message_buffer.h"
Erstellen:
MessageBufferHandle_t xMessageBufferCreate(
<size_t GreoesseInBytes>
);
Schreiben:
size_t xMessageBufferSend(
<BufferHandle>,
( void * ) <Nachricht>,
<laengeDerNachricht>,
<TimeOut>
);
Lesen einer Nachricht:
Vorab die Länge der nächsten Nachricht erfragen:
size_t xMessageBufferNextLengthBytes(<BufferHandle>);
Für den eigentlichen Empfang wird die folgende Funktion verwendet
size_t xStreamBufferReceive(
<BufferHandle>,
( void * ) <EmpfangsBuffer>,
<LaengeDerNachricht>,
<TimeOut>
);
So sieht der Gesamtcode aus. Dieser unterscheidet sich nur geringfügig vom Code des StreamBuffers.
Hooks
Es gibt Idle und Tick Hooks. Idle Hooks werden ausgelöst wenn die CPU im leerlauf ist. Tickhooks werden über den Tickinterrupt ausgelöst. Zweiteres soll Zeitlich nicht sehr zuverlässig sein. Für zeitliche Abfolgen sollten eher Timer oder PWM Signale verwendet werden.
Weiterhin gibt es die standard Hooks des FreeRtos und eigene Varianten der IDF.
FreeRtos Implementierung
- Zur Nutzung CONFIG_FREERTOS_LEGACY_HOOKS aktivieren
- Es kann nur einen Idle und eine Tick Hook geben
- FreeRtos Implementierung ist auf single core ausgelegt.
IDF Implementierung
- Es können mehrer Hooks/Ticks registriert werden.
- Ein Hooks/Ticks mus seiner CPU zugewiesen werden
- Die registrierten Hooks/Ticks werden routierend ausgeführt.
Es dürfen niemals blockierende Codestellen in Hooks und Ticks verwendet werden. Diese müssen immer komplett durhlaufen werden und sollten möglichst kurz sein, da sie sonst andere Tasks behindern könnten.
In Ticks dürfen zusätzlich keine API Funktionionen ohne "FromIsr" verwendet werden. LogAusgaben sind ebenfalls nicht möglich.
Bibliothek:
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_freertos_hooks.h"
Idle Hook erstellen
esp_err_t esp_register_freertos_idle_hook_for_cpu(
<Funktion für Callback>,
<CPU ID>
)
// oder
esp_err_t esp_register_freertos_idle_hook_for_cpu(
<Funktion für Callback>
)
// Funktionsprototyp
static bool hook1(void)
{}
Tick erstellen
esp_err_t esp_register_freertos_tick_hook(
<tickFunctionName>
<CPU ID>
);
// oder
esp_err_t esp_register_freertos_tick_hook(
<tickFunctionName>
);
// Funktionsprototyp
static void tick2(void)
{}
Fehler
- ESP_OK: Hat geklappt
- ESP_ERR_NO_MEM: Kein Speicher für den Prozessorkern um einen Hook zu registrieren
- ESP_ERR_INVALID_ARG: Ungültige CPU
CPU ID abfragen
// core des aktuellen Tasks
const uint32_t core_id = xPortGetCoreID();
// bei zwei Kernen den anderen bekommen
const uint32_t core_id = !xPortGetCoreID();
Hier das Beispielprogramm.
Ringbuffer
Der Ringbuffer der IDF sind eine "interessante" Sache. Ich spare sie mir an dieser Stelle mal auf für einen anderen Zeitpunkt auf, andem ich sie wirklich brauche ;).
Bibliothek:
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
Fazit
Jede Aktion in einen eigenen Task zu verpacken bedarf einiger Übung und Planung. Um so wichtiger ist es die richtigen Bibliotheksfunktionen zur Verfügung zu habe. Ob diese Funktionalitäten ausreichend sind, muss sich in der Praxis zeigen. Grundsätzlich scheint aber alle was man brauch zur Verfügung zu stehen.
Hallo Markus,
Danke für deinen Kommentar, es freut mich immer wieder wenn der Blog hilft.
Grusd
Stefan
Hallo Stefan,
vielen Dank für die Mühe die du dir hier gibst! Dieser Blog zum Thema Tasks mit der IDF ist echt genial und hat mir super weiter geholfen.
5 *****
Gruß – Markus