Last Updated on 18 January 2021 by Suffocation
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 - FreeRTOS Chapter referred.
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.
Many of the functions described below exist in multiple forms. For one thing, it can be determined during creation whether the memory should be reserved dynamically or statically at compile time. The static functions always end with 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.
Create Task
Libraries
#include "freertos/FreeRTOS.h"
#include "freertos/task.h""
Tasks are created with the command:
xTaskCreate(
<pointer zur Task Funktion,
<Task Name>,
<Speicher für Task>,
<Task parameter>,
<Priotität>,
<Referenz für Handle>);
It's good programming practice to always have a task only 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:
// Suspend a specific task
void vTaskSuspend();
// Suspend a task from an interrupt service routine
BaseType_t xTaskResumeFromISR()
// Suspend your own task
vTaskSuspend(NULL);;
In this case, the task will wait until it is activated from the outside again. This is done with the following command:
// normal resumption
void xTaskResume();
// from interrupt service routine
void xTaskResumeFromISR();;
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.
The following example creates multiple tasks, each running for a different length of time and producing output when active:
‚); document.write(‚
‚);
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.
- Traffic lights – Werden genutzt um Task mit einander zu Synchronisieren oder gemeinsam genutze Resourcen vor Datennfehlern zu schützen.
- Event Groups – 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 Post created.
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.
Library
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h""Create Queue:
QueueHandle_t xQueue;
xQueue = xQueueCreate( <Länge Queue>, <größe der Fehler>);
if( xQueue == 0 )
{
// Fehler Erkennung
ESP_LOGE(TAG,"Error during queue creation");
}Send data
if(!xQueueSendToBack(, (void*)&, )) {
// queue full
ESP_LOGW(TAG, "q - Queue Full");
}Receive data
if( ! xQueueReceive( , , ) ){
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.
Traffic lights
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.
Library
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h""Create semaphore:
SemaphoreHandle_t xSemaphore = NULL;
xSemaphore = xSemaphoreCreateBinary();
if( xSemaphore == NULL ) {
// Error
}Before first use, the semaphore must be released once.
Fetch semaphore
if( xSemaphoreTake( , timeout) == pdTRUE )
{
// it worked ...
xSemaphoreGive( );
} else {
// it didn't work
}Release the semaphore:
xSemaphoreGive();;
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)activate this to disable/enable the semaphores
#define enable_semaphoreZu 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
Library
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h""Setting the bit:
EventBits_t xEventGroupSetBits(
,
);;Bei der Wiederverwendung einer bestehenden Gruppe müssen die Bits zurückgesetzt werden.
EventBits_t xEventGroupClearBits(
<Gruppen Handle>,
<Bits zum Rücksetzten>
);
Wait for the set bits, this is done with:
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");
}Here is a complete example with multiple tasks. The main task waits until all bits are set. Once all are present, the main task continues.
Stream Buffer
Library
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/stream_buffer.h""Create:
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(, )Function to send:
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>)Received:
size_t xStreamBufferReceive(
<Stream Buffer>,
( void * )&<Buffer für Empfangsdaten>,
<Göße Emfpangsbuffer>,
<Timeout> );
Example Programme
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.
Library
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/message_buffer.h""Create:
xMessageBufferCreate(
);Write
size_t xMessageBufferSend(
,
( void * ) ,
,
);;Reading a message:
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 Implementation
- 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 Implementation
- 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.
Library
#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.
Ring buffer
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 ;).
Library
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h""Conclusion
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.
Verwandte Beiträge
Sources
IDF Beispiel - Task Watchdog (GitHub)
Infos zu Ticks und Hooks im Espressif Forum
| Date | Change | Author |
|---|---|---|
| 08.01.2021 | Post created | Suffocation |
Hello Markus,
Danke für deinen Kommentar, es freut mich immer wieder wenn der Blog hilft.
Grusd
Stefan
Hello 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