ESP-IDF FreeRTOS: Normale Tasks vs. Echtzeit-Tasks

3led

Inhaltsverzeichnis

Viele Entwickler setzen FreeRTOS-Tasks mit echter Parallelität oder Echtzeit gleich. Das führt häufig zu falschen Annahmen über das Verhalten des Systems. Eine Task macht ein System nicht echtzeitfähig. Entscheidend sind das Scheduling, die Prioritäten und ein exakt kontrolliertes Timing.

Wichtig ist zu verstehen:
Tasks laufen auf dem ESP32 (mit einem oder zwei Kernen) nicht wirklich parallel, sondern werden vom Scheduler in kleine Zeitscheiben aufgeteilt. Jede Task bekommt für einen kurzen Moment die CPU, dann wird zur nächsten gewechselt. Dieses schnelle Hin- und Herschalten erzeugt für uns den Eindruck, als würden alle Tasks gleichzeitig laufen.

Ein einfaches Beispiel macht das sichtbar:
Vier Tasks haben jeweils eine Gesamtlaufzeit von 10 Sekunden. Der Scheduler zerlegt jede davon in 10 Scheiben à 1 Sekunde. Haben alle Tasks die gleiche Priorität, arbeitet FreeRTOS sie typischerweise nach dem Round-Robin-Prinzip ab: Jede Task bekommt zuerst ihre erste Scheibe, dann alle nacheinander ihre zweite, dann die dritte – so lange, bis alle fertig sind.

Während eine Task läuft, sind alle anderen Taskkontexte eingefroren. Sie warten, bis der Scheduler sie wieder „dran nimmt“. Genau dieses Verhalten erzeugt die scheinbare Parallelität, obwohl technisch nur abwechselnde Ausführung stattfindet.

Diese Seite erklärt, wie solche Abläufe wirklich funktionieren, wo Missverständnisse entstehen und wie man saubere, echtzeitfähige und deterministische Task-Designs auf dem ESP32 umsetzt.

„In beiden Beispielen wird ein zufälliger Zeitfaktor verwendet, um in der Task GPIOs zu togglen. Die Tasks laufen auf diese Weise bei jeder Ausführung unterschiedlich lange. Dadurch erzeugen wir bewusst variable Last. Erwartungsgemäß zeigt die Echtzeit-Task dennoch eine konstante Periodendauer, während die normale Task deutliche Schwankungen aufweist.“

gpio_set_level(LED1, 1);

// große variable Last
int r = esp_random() & 0x1FFF;
for (int i = 0; i < r * 10; i++)
{
    asm("nop");
}

gpio_set_level(LED1, 0);

Beispielcode – Hohe Task-Last

 

Normale-Tasks

In FreeRTOS werden normale Tasks am einfachsten mit der Funktion xTaskCreate() erstellt. Für solide verwertbare Ergebnisse, binden wir aber die Tasks fest an einen Kern, mit xTaskCreatePinnedToCore().

In einer Task verwenden wir immer ein vTaskDelay(), damit die Task freiwillig für einen festen Zeitraum, z.B. 1 ms, CPU-Zeit abgibt. Ohne diese Zeit würde diese eine Task in der Endlosschleife bleiben und keine Andere Task könnte ausgeführt werden. Die Unterbrechung blockiert keine anderen Tasks, sondern sorgt dafür, dass nur die Tasks für einen Zeitraum blockiert werden, in denen vTaskDelay() gerade ausgeführt wird. In dieser Zeit wird die Prozessorzeit an andere Tasks abgegeben.

Bei einer Einstellung der Tick-Rate im menuconfig, auf 1000 Hz, wird die Task nach dem Aufrufen der Funktion

vTaskDelay(1 / portTICK_PERIOD_MS);

für exakt 1 ms blockiert. In dieser Zeit können andere Tasks ausgeführt werden. Nach 1 ms wird die blockierte Task vom Scheduler erneut gestartet.

Für Echtzeitanwendungen ist vTaskDelay() aber nicht geeignet, da Tasks zwar in diesem Beispiel für 1 ms blockiert werden, aber nicht berücksichtigt wird, wie lang die Ausführungszeit der Tasks selbst ist. vTaskDelay() unterbricht die Task erst ab dem Zeitpunkt des Aufrufes der Funktion, ohne Berücksichtigung vorheriger Aufgaben. Wenn in einer Task Funktionen mit unterschiedlichen Ausführungszeiten sind und diese Zeiten variieren, da bspw. auf Eingaben gewartet wird, oder auf Daten aus einer Queue, dann werden diese Funktionen zunächst ausgeführt. Dann erst folgt vTaskDelay() und blockiert die Task für bspw. 1 ms.

In Abb. 1 sieht man eine Timingaufnahme einer normalen Task mit einer Abtastrate von 5 ms. Man kann hier deutlich erkennen, dass der Abstand zwischen den Tasks stark variiert. Die Periodendauer schwankt zwischen 5 ms und 8 ms.

normaleabtastung
Abb. 1 – Normale Abtastung mit vTaskDelay(5/portTICK_PERIOD_MS)

In diesem Beispiel wurde nur eine Task verwendet! Wenn weitere Tasks mit unterschiedlichen Prioritäten hinzukommen, werden die Schwankungen noch offensichtlicher.

 

Echtzeit-Task

Für die Echtzeit-Tasks verwenden wir wieder xTaskCreatePinnedToCore(), um sicher zu stellen, dass die Tasks immer auf dem selben Kern abgearbeitet werden.

Für Echtzeitverhalten wird vTaskDelayUntil() verwendet. Diese Funktion berechnet die Zeit ab dem Start einer Task und blockiert die Task exakt für den gewünschten Zeitabschnitt, unabhängig von der Dauer der auszuführenden Aufgaben in der Task.

Voraussetzung ist, dass Aufgaben in der Task nicht länger dauern als der Zeitabschnitt, in dem die Task blockiert werden soll, da sonst unerwünschte, und teils schwer zu lokalisierende Fehler auftreten.

vTaskDelayUntil() blockiert eine Task absolut, d. h., dass die Ausführungszeit der Task in Abhängigkeit von der Startzeit berechnet wird. Für die Anwendung von vTaskDelayUntil() wird noch eine Variable vom Typ TickType_t deklariert und mit der aktuellen Startzeit der Task initialisiert.

TickType_t xLastWakeTime; // TickType Variable für Echtzeitabtastung
// Initialisiert die Variable xLastWakeTime mit aktueller Zeit.
xLastWakeTime = xTaskGetTickCount();
vTaskDelayUntil(&xLastWakeTime, 1000 / portTICK_PERIOD_MS);

Beispielcode – Definition für Echtzeitabtastung

Im folgenden Bild kann man sehr gut erkennen, dass die Ausführungszeit der Task variiert, aber der Startpunkt der Perioden, bzw. den steigenden Flanken ist exakt gleich, immer bei 5 ms.

Auch hier wurde der gleiche Code mit der hohen Task-Last verwendet.

echtzeitabtastung5ms
Abb. 2 – Echtzeit-Task mit vTaskDelayUntil(&xLastWakeTime, 5/portTICK_PERIOD_MS)

Kurz gesagt

„Normale Tasks eignen sich für allgemeine Abläufe, reagieren aber empfindlich auf variable Ausführungszeiten. Für deterministische Abläufe ist vTaskDelayUntil() zwingend notwendig, weil die Periodenzeit absolut und unabhängig von der Task-Last eingehalten wird. Damit lassen sich stabile, echtzeitfähige Abtastungen umsetzen. Die Wahl der richtigen Blocking-Methode entscheidet somit direkt über Timing-Genauigkeit und Systemverhalten.“

Obwohl gemessene 1-ms-Taktraten über viele Minuten hinweg exakt sein können, bedeutet dies nicht, dass das gesamte System harte Echtzeit garantiert. Weiche Echtzeit heißt: Hohe praktische Präzision, aber keine mathematische Garantie in allen denkbaren Systemzuständen.

 

Weiterführende Links

Blogartikel

 

Schreibe einen Kommentar