FreeRTOS Task-Prioritäten erklärt: Wer läuft wann?
Wie der FreeRTOS-Scheduler Prioritäten auswertet, was Preemption und Time-Slicing bedeuten und welche Prioritäts-Fehler Einsteiger am häufigsten machen.
- Autor:
- Gaëtan Pandji
- Veröffentlicht:
- Zuletzt aktualisiert:
- 5 Min. Lesezeit
Wie entscheidet FreeRTOS, welcher Task läuft?
FreeRTOS verwendet einen prioritätsbasierten, präemptiven Scheduler (bei configUSE_PREEMPTION = 1, dem Standard). Die Regel ist einfach:
Es läuft immer der Task mit der höchsten Priorität, der sich im Zustand Ready befindet.
Ein Task ist Ready, wenn er nicht gerade blockiert (z. B. in vTaskDelay(), xQueueReceive() mit Timeout, ulTaskNotifyTake()), nicht suspendiert und nicht bereits der laufende Task ist. Wird ein Task mit höherer Priorität Ready – etwa weil eine ISR eine Queue füllt – unterbricht der Scheduler den laufenden Task sofort (Preemption), nicht erst beim nächsten Tick.
Zahlenwerte: 0 ist die niedrigste Priorität und gehört dem Idle-Task. Die höchste nutzbare Priorität ist configMAX_PRIORITIES - 1. Mehr Prioritätsstufen kosten RAM (eine Ready-Liste pro Stufe), also halte configMAX_PRIORITIES so klein wie möglich – fünf bis sieben Stufen reichen für die meisten Projekte.
Was passiert bei Tasks mit gleicher Priorität?
Haben mehrere Ready-Tasks dieselbe Priorität, verteilt FreeRTOS die CPU per Round-Robin: Bei jedem Tick-Interrupt (Standard: 1 ms bei configTICK_RATE_HZ = 1000) wechselt der Scheduler zum nächsten Task gleicher Priorität – vorausgesetzt configUSE_TIME_SLICING = 1. Ohne Time-Slicing läuft ein Task so lange, bis er selbst blockiert oder ein höher priorisierter Task bereit wird.
| Einstellung | Verhalten bei gleicher Priorität | Typischer Einsatz |
|---|---|---|
configUSE_PREEMPTION = 1, configUSE_TIME_SLICING = 1 |
Wechsel bei jedem Tick (Round-Robin) | Standard, einfach zu verstehen |
configUSE_PREEMPTION = 1, configUSE_TIME_SLICING = 0 |
Wechsel nur bei Blockieren oder höherem Task | Weniger Kontextwechsel, deterministischer |
configUSE_PREEMPTION = 0 |
Kooperativ: Wechsel nur bei taskYIELD() oder Blockieren |
Selten; Legacy-Portierungen |
Wichtig: Round-Robin ist keine Fairness-Garantie über Prioritätsstufen hinweg. Ein Task mit Priorität 3, der nie blockiert, bekommt 100 % der CPU – alle Tasks mit Priorität 0 bis 2 laufen nie wieder.
Welche Priorität sollte welcher Task bekommen?
Eine praxistaugliche Faustregel: Je kürzer die Deadline, desto höher die Priorität (das ist im Kern Rate-Monotonic Scheduling). Außerdem gilt: Je kürzer ein Task pro Aktivierung rechnet, desto unproblematischer ist eine hohe Priorität.
| Task-Typ | Beispiel | Priorität (von 5) | Begründung |
|---|---|---|---|
| Deferred-Interrupt-Task | UART-Empfang verarbeiten, Sensor-Fertig-Signal | 4 | Kurze Laufzeit, enge Latenzanforderung |
| Regelung / Mess-Task | PID-Regler alle 1 ms, ADC-Abtastung | 3 | Periodisch, harte Zeitbasis via vTaskDelayUntil() |
| Kommunikation | Modbus, MQTT, OPC-UA-Stack | 2 | Lange Laufzeit, tolerante Latenz |
| Applikationslogik / Logging | Zustandsmaschine, SD-Karten-Log | 1 | Darf unterbrochen werden |
| Idle-Hook / Diagnose | Stack-Watermarks sammeln, Low-Power | 0 (Idle) | Läuft nur, wenn sonst nichts ansteht |
Vermeide es, alles auf dieselbe Priorität zu setzen – dann verlierst du den Hauptvorteil eines RTOS. Vermeide aber auch zwanzig verschiedene Stufen, die niemand mehr begründen kann.
Beispiel: Zwei Tasks, ein Fehler
#define PRIO_SENSOR 3
#define PRIO_LOGGER 1
static void sensor_task(void *arg)
{
TickType_t last = xTaskGetTickCount();
for (;;) {
sample_t s = adc_read();
xQueueSend(q_samples, &s, 0);
vTaskDelayUntil(&last, pdMS_TO_TICKS(10)); /* blockiert: 10-ms-Periode */
}
}
static void logger_task(void *arg)
{
sample_t s;
for (;;) {
if (xQueueReceive(q_samples, &s, portMAX_DELAY) == pdTRUE) {
sd_write(&s); /* darf lange dauern */
}
}
}
int main(void)
{
q_samples = xQueueCreate(32, sizeof(sample_t));
xTaskCreate(sensor_task, "sensor", 256, NULL, PRIO_SENSOR, NULL);
xTaskCreate(logger_task, "logger", 512, NULL, PRIO_LOGGER, NULL);
vTaskStartScheduler();
}
Das funktioniert, weil sensor_task nach jeder Messung blockiert. Ersetzt du vTaskDelayUntil() durch eine Busy-Wait-Schleife (while (!adc_ready());), bleibt der Sensor-Task dauerhaft Ready – der Logger mit Priorität 1 bekommt nie CPU, die Queue läuft voll, Daten gehen verloren. Genau dieses Muster ist die häufigste Ursache für „mein Task läuft nicht“.
Welchen Bus der Sensor dabei nutzt, ist für die Prioritäten übrigens egal – wann I2C und wann SPI die bessere Wahl ist, steht im Artikel I2C vs. SPI: Wann nimmst du welchen Bus?.
Welche Prioritäts-Fehler sehe ich am häufigsten?
- Tasks, die nie blockieren. Jede Endlosschleife braucht mindestens einen blockierenden Aufruf (
vTaskDelay, Queue-Receive, Notification). Sonst verhungert alles darunter. - Interrupt-Prioritäten mit Task-Prioritäten verwechseln. Cortex-M-NVIC-Prioritäten sind ein anderes System (dort ist eine kleinere Zahl höher). Interrupts, die FreeRTOS-API aufrufen, müssen numerisch ≥
configMAX_SYSCALL_INTERRUPT_PRIORITYsein – sonst gibt es sporadische Abstürze. - Prioritätsinversion durch Semaphores statt Mutexe. Nur ein Mutex (
xSemaphoreCreateMutex) hat Prioritätsvererbung. Ein Binary-Semaphore als Lock verwendet, lässt einen hohen Task auf einen niedrigen warten, der wiederum von einem mittleren verdrängt wird. - Prioritäten zur Laufzeit „nachjustieren“.
vTaskPrioritySet()ist ein Werkzeug für Spezialfälle, kein Pflaster für ein unklares Design. Wenn du es brauchst, überdenke zuerst die Blockierpunkte. - Idle-Task verhungern lassen. Der Idle-Task gibt Speicher gelöschter Tasks frei (
vTaskDelete) und ruft den Idle-Hook auf. Bekommt er nie CPU, leckt Speicher, und Low-Power-Modi greifen nicht.
Wie prüfe ich, ob meine Prioritäten stimmen?
vTaskGetRunTimeStats()zeigt pro Task die CPU-Zeit in Prozent. Ein Task mit 100 % ohne blockierenden Aufruf ist dein Problem.uxTaskGetStackHighWaterMark()sagt dir, wie viel Stack jeder Task tatsächlich braucht – Prioritäten beeinflussen nicht den Stack, aber beide Zahlen gehören in dieselbe Review-Tabelle.- Trace-Tools (SEGGER SystemView, Percepio Tracealyzer) zeigen die Zeitachse: Welcher Task wird wann von wem verdrängt? Prioritätsinversion und Starvation sind dort auf einen Blick sichtbar.
- Ein einfaches Logic-Analyzer-Pattern: Setze am Task-Anfang einen GPIO, am Ende zurück. Vier Kanäle reichen, um die Reihenfolge und Jitter sichtbar zu machen.
Fazit
Prioritäten in FreeRTOS sind kein Tuning-Parameter, sondern eine Design-Entscheidung: kurze Deadline = hohe Priorität, und jeder Task muss blockieren. Wenn du diese zwei Regeln einhältst und Mutexe statt Semaphores für Locks nutzt, hast du 80 % der typischen RTOS-Probleme vermieden. Den Rest findest du mit Laufzeitstatistiken und einem Trace.