6.1 Einführung in Echtzeitbetriebssysteme (RTOS)

In der modernen Embedded-Systementwicklung stößt man schnell an die Grenzen traditioneller sequenzieller Programmierung. Wenn ein Mikrocontroller gleichzeitig mehrere Aufgaben bewältigen muss - etwa Sensordaten auslesen, eine Kommunikation aufrechterhalten und eine Benutzerschnittstelle bedienen - wird eine strukturierte Lösung benötigt: das Echtzeitbetriebssystem (Real-Time Operating System, RTOS).

6.1.1 Was ist ein Echtzeitbetriebssystem (RTOS)?

Ein RTOS ist ein spezialisiertes Betriebssystem für Embedded Systems, das entwickelt wurde, um zeitkritische Aufgaben zuverlässig und vorhersagbar auszuführen. Im Gegensatz zu Desktop-Betriebssystemen (wie Windows oder Linux) liegt der Fokus nicht auf maximaler Rechenleistung, sondern auf deterministischem Zeitverhalten.

Hauptmerkmale eines RTOS

  • Deterministisches Zeitverhalten: Garantierte Reaktionszeiten auf Ereignisse
  • Task-Management: Verwaltung mehrerer parallel laufender Aufgaben (Tasks)
  • Prioritätsbasiertes Scheduling: Wichtige Tasks werden bevorzugt ausgeführt
  • Minimaler Ressourcenverbrauch: Optimiert für Mikrocontroller mit begrenztem Speicher
  • Echtzeitfähigkeit: Einhaltung von zeitlichen Anforderungen (Deadlines)

6.1.2 Warum brauchen wir ein RTOS?

Ohne RTOS: Die "Super-Loop" Architektur

int main(void) {
    init_hardware();

    while(1) {
        read_sensors();
        process_communication();
        update_display();
        check_buttons();
        control_actuators();
    }
}

Probleme dieser Architektur:

  • Alle Aufgaben blockieren sich gegenseitig
  • Zeitkritische Ereignisse können verpasst werden
  • Schwierige Wartbarkeit bei wachsender Komplexität
  • Keine klare Trennung der Funktionalitäten

Mit RTOS: Task-basierte Architektur

Ein RTOS ermöglicht es, das Programm in unabhängige Tasks aufzuteilen, die quasi-parallel ausgeführt werden:

void task_sensor(void *parameters) {
    while(1) {
        read_sensors();
        vTaskDelay(100);  // 100ms warten
    }
}

void task_communication(void *parameters) {
    while(1) {
        process_messages();
        vTaskDelay(50);
    }
}

void task_display(void *parameters) {
    while(1) {
        update_display();
        vTaskDelay(200);
    }
}

6.1.3 Grundkonzepte eines RTOS

Tasks (Aufgaben)

Eine Task ist eine unabhängige Programmeinheit mit eigenem Kontext (Stack, Programm-Counter, Register). Jede Task verhält sich wie ein eigenständiges Mini-Programm.

Task-Zustände:

  • Running: Task wird gerade ausgeführt
  • Ready: Task ist bereit zur Ausführung, wartet aber auf CPU-Zeit
  • Blocked: Task wartet auf ein Ereignis (z.B. Timer, Semaphore)
  • Suspended: Task wurde manuell pausiert


    ┌─────────────┐
    │  Suspended  │
    └──────┬──────┘
           │ Resume
           ↓
    ┌─────────────┐  Scheduler wählt Task aus  ┌─────────────┐
    │    Ready    │ ──────────────────────────→│   Running   │
    └─────────────┘                            └──────┬──────┘
           ↑                                          │
           │                                          │
           │ Event tritt ein                          │ Wartet auf Event/Timer
           │                                          │ oder höhere Priorität
           │                                          ↓
    ┌──────┴──────┐                             ┌─────────────┐
    │   Blocked   │←────────────────────────────│   (delay)   │
    └─────────────┘                             └─────────────┘

Scheduler (Aufgabenplaner)

Der Scheduler entscheidet, welche Task zu welchem Zeitpunkt ausgeführt wird. Die wichtigsten Scheduling-Strategien:

Preemptive Priority-Based

  • Die Task mit höchster Priorität läuft immer
  • Eine Task mit hoher Priorität verdrängt (preempt) eine Task mit niedriger Priorität sofort
  • Standard in den meisten RTOS

Round-Robin

  • Tasks gleicher Priorität bekommen abwechselnd CPU-Zeit
  • Jede Task erhält eine feste Zeitscheibe (Time Slice)
  • Fair, aber nicht deterministisch

Cooperative:

  • Tasks geben freiwillig die CPU ab
  • Keine Verdrängung möglich
  • Einfacher, aber gefährlich (eine blockierende Task stoppt alles)

Prioritäten

Jede Task erhält eine Priorität. Typischerweise gilt: Höhere Zahlen = höhere Priorität.

Beispiel-Prioritätsverteilung:

  • Hohe Priorität (40-48): Zeitkritische Tasks (Motorsteuerung, Interrupt-Verarbeitung)
  • Mittlere Priorität (24-32): Regelmäßige Tasks (Sensordatenverarbeitung, Regelung)
  • Niedrige Priorität (8-16): Hintergrundaufgaben (Logging, Display-Updates, Statistiken)

Wichtige Regel: Eine laufende Task wird nur unterbrochen, wenn eine Task mit höherer Priorität bereit wird!

Zeitachse: ─────────────────────────────────────────────────────────────→ Task A (Prio 10): ████░░░░░░░░░░████░░░░░░░░░░████░░░░
Task B (Prio 20): ░░░░████████░░░░░░████████░░░░░░░░░░
Task C (Prio 30): ░░░░░░░░░░░░░░░░░░░░░░░░████████░░░░

Legende: █ = läuft, ░ = wartet/blockiert

Synchronisation und Kommunikation

Tasks müssen oft miteinander kommunizieren oder gemeinsame Ressourcen nutzen. Das RTOS bietet dafür verschiedene Mechanismen:

Semaphoren

Zugriffskontrolle auf gemeinsame Ressourcen.

Binary Semaphore: Wie ein "Türschild" (besetzt/frei)

xSemaphoreTake(sem, portMAX_DELAY);
// Kritischer Bereich - nur eine Task gleichzeitig
shared_variable++;
xSemaphoreGive(sem);

Counting Semaphore: Zählt verfügbare Ressourcen (z.B. freie Buffer)

// Initial: 5 Buffer verfügbar
xSemaphoreTake(buffer_sem, portMAX_DELAY);  // Jetzt 4 verfügbar
// Buffer benutzen
xSemaphoreGive(buffer_sem);                 // Wieder 5 verfügbar

Mutex (Mutual Exclusion)

Spezieller Semaphor für gegenseitigen Ausschluss mit Priority Inheritance (verhindert Prioritätsinversion).

xSemaphoreTake(uart_mutex, portMAX_DELAY);
// Nur eine Task kann UART gleichzeitig nutzen
UART_Transmit("Nachricht");
xSemaphoreGive(uart_mutex);

Unterschied Mutex vs. Binary Semaphore:

  • Mutex: Nur die Task, die ihn "genommen" hat, darf ihn "geben"
  • Binary Semaphore: Jede Task kann geben/nehmen (z.B. für Signalisierung von ISR)

Queues

Nachrichten zwischen Tasks austauschen (FIFO-Prinzip).

// Sender-Task
uint32_t sensor_data = read_sensor();
xQueueSend(data_queue, &sensor_data, 0);

// Empfänger-Task
uint32_t received_data;
xQueueReceive(data_queue, &received_data, portMAX_DELAY);
process_data(received_data);

Vorteile von Queues:

  • Thread-safe (keine Race Conditions)
  • Blockieren bei voller/leerer Queue möglich
  • Entkopplung von Sender und Empfänger

Event Groups

Warten auf mehrere Ereignisse gleichzeitig.

// Task wartet auf Bit 0 UND Bit 1
xEventGroupWaitBits(event_group, 
                    BIT_0 | BIT_1,     // Warte auf beide Bits
                    pdTRUE,            // Bits nach Empfang löschen
                    pdTRUE,            // Warte auf ALLE Bits
                    portMAX_DELAY);

Nützlich für komplexe Synchronisation (z.B. "warte bis Sensor bereit UND Kommunikation frei").

6.1.4 Speicherverwaltung in RTOS

Stack vs. Heap

In Embedded Systems gibt es verschiedene Speicherbereiche im RAM:

Allgemeine Speicheraufteilung:

┌─────────────────────────────────┐  ← Höchste Adresse
│         Stack (wächst ↓)        │  Lokale Variablen, Funktionsaufrufe
├─────────────────────────────────┤
│              ↓ ↑                │
│         (freier Bereich)        │
│              ↑ ↓                │
├─────────────────────────────────┤
│         Heap (wächst ↑)         │  Dynamischer Speicher
├─────────────────────────────────┤
│    .bss (nicht initialisiert)   │  Globale Variablen ohne Startwert
├─────────────────────────────────┤
│    .data (initialisiert)        │  Globale Variablen mit Startwert
└─────────────────────────────────┘  ← Niedrigste Adresse

Beispielhafte FreeRTOS-Speicheraufteilung:

┌────────────────────────────────────────────┐
│  Global variables, static data             │
├────────────────────────────────────────────┤
│  Main stack (before scheduler starts)      │
├────────────────────────────────────────────┤
│  FreeRTOS HEAP                             │  ← configTOTAL_HEAP_SIZE
│  ┌──────────────────────────────────────┐  │
│  │ Task A stack (256 words)             │  │
│  │ Task B stack (512 words)             │  │
│  │ Queue (50 items × 4 bytes)           │  │
│  │ Semaphore                            │  │
│  │ ... free space ...                   │  │
│  └──────────────────────────────────────┘  │
└────────────────────────────────────────────┘

Stack (Stapelspeicher):

  • Für lokale Variablen in Funktionen
  • Automatische Verwaltung (LIFO = Last In, First Out)
  • Wird automatisch aufgeräumt
  • Jede Task hat ihren eigenen Stack!

Heap (Haldenspeicher):

  • Für dynamisch angeforderten Speicher
  • Mit malloc() angefordert, mit free() freigegeben
  • Muss manuell verwaltet werden
  • RTOS nutzt eigenen Heap für Tasks/Queues

Der RTOS-Heap

FreeRTOS verwendet einen eigenen Heap (nicht den Standard-C-Heap):

Vorteile:

  • Deterministisch
  • Thread-safe
  • Optimiert für Embedded
  • Konfigurierbare Größe

Heap-Bedarf berechnen:

Heap = Σ(Task-Stacks) + Σ(Queue-Größen) + Overhead + Reserve

Beispiel:
- 3 Tasks à 512 Bytes Stack   = 1536 Bytes
- 2 Queues à 200 Bytes         = 400 Bytes
- Verwaltungsstrukturen        = ~400 Bytes
- Reserve 20%                  = ~500 Bytes
                              ___________
Total:                         ≈ 2900 Bytes
Aufgabenkomplexität Typische Stack-Größe (Words)
Minimal (setzt nur ein Flag) 128
Einfache Logik, wenige Variablen 256
Verwendet printf, sprintf 512+
Komplexe Logik, verschachtelte Aufrufe 512–1024

Merke: Lokale Arrays und tiefe Funktionsaufrufe verbrauchen schnell Stack-Speicher.

Tip: Während der Entwicklung großzügig mit Stack-Größen sein. Später ggfs. optimieren.

6.1.5 Vor- und Nachteile eines RTOS

Vorteile

Strukturierte Programmarchitektur: Klare Aufgabentrennung
Bessere Wartbarkeit: Jede Task ist eigenständiges Modul
Erweiterbarkeit: Neue Tasks einfach hinzufügbar
Garantierte Reaktionszeiten: Deterministisches Verhalten
Effiziente Ressourcennutzung: CPU nur wenn nötig aktiv
Wiederverwendbarkeit: Task-Module können recycelt werden
Parallele Entwicklung: Teams arbeiten an verschiedenen Tasks

Nachteile

Höherer Speicherbedarf: RAM für Heap, Stacks, Verwaltung
Zusätzliche Komplexität: Lernkurve für Entwickler
Overhead: Context-Switching kostet Zeit (~1-10 µs)
Debugging schwieriger: Race Conditions, Deadlocks möglich
Potentielle Fehlerquellen: Prioritätsinversion, Starvation

6.1.6 Wann sollte man ein RTOS einsetzen?

RTOS sinnvoll bei:

  • Mehreren zeitkritischen Aufgaben parallel
  • Komplexen Anwendungen mit vielen Funktionalitäten
  • Notwendigkeit von garantierten Reaktionszeiten
  • Projekten mit zukünftiger Erweiterbarkeit
  • Mehreren Kommunikationsschnittstellen gleichzeitig
  • Regelungstechnik mit verschiedenen Regelschleifen
  • Event-getriebenem Programmablauf

RTOS nicht notwendig bei:

  • Sehr einfachen Anwendungen (LED blinken)
  • Stark begrenzten Ressourcen (winzige µC mit <8 kB RAM)
  • Rein sequenziellen Abläufen ohne Zeitkritik
  • Prototyping-Phase mit einfacher Funktionalität
  • Single-Task-Anwendungen

Entscheidungshilfe:

1. Gibt es >3 unabhängige Aufgaben? RTOS erwägen
2. Sind Reaktionszeiten kritisch? RTOS erwägen
3. Wird das Projekt wachsen? RTOS erwägen
4. Ist RAM >16 kB verfügbar? RTOS möglich
5. Gibt es zeitkritische Interrupts? RTOS hilft

Mindestens 2-3 x JA? → RTOS ist sinnvoll!

6.1.7 Bekannte RTOS für Embedded Systems

Open Source RTOS

FreeRTOS

  • Weltweit am häufigsten verwendet
  • Umfangreiche Mikrocontroller-Unterstützung
  • Kleine Footprint: ab 4-5 kB Flash, 1-2 kB RAM
  • Ausgezeichnete Dokumentation
  • MIT-Lizenz (frei nutzbar)

Zephyr

  • Modern, von Linux Foundation
  • Modularer Aufbau
  • IoT-fokussiert
  • Große Treiber-Bibliothek

RIOT

  • Speziell für IoT-Anwendungen
  • Energieeffizient
  • Unterstützt viele Netzwerkprotokolle

RT-Thread

  • Chinesischer Ursprung, stark wachsend
  • Gut für IoT-Anwendungen
  • Umfangreiches Paket-System

Kommerzielle RTOS

ThreadX (Azure RTOS)

  • Microsoft, jetzt Open Source
  • Safety-zertifiziert (IEC 61508, DO-178B)
  • Sehr performant

embOS (SEGGER)

  • Extrem schnell und kompakt
  • Hervorragender Support
  • Professionelle Tools (SystemView)

Micrium µC/OS-III

  • Safety-zertifiziert
  • Sehr gute Dokumentation
  • Kommerziell, aber kostenlos für Bildung

VxWorks

  • Industriestandard für kritische Systeme
  • NASA Mars Rover, Flugsysteme
  • Sehr teuer, aber extrem zuverlässig

6.1.8 Typische RTOS-Probleme und Lösungen

Problem 1: Prioritätsinversion

Situation:

  1. Task A (niedrige Prio) sperrt Ressource mit Mutex
  2. Task C (hohe Prio) will dieselbe Ressource → blockiert
  3. Task B (mittlere Prio) läuft und verhindert, dass A fertig wird
  4. Resultat: Task C wartet auf niederpriore Task B!

Lösung: Priority Inheritance

  • Task A "erbt" temporär Priorität von Task C
  • Task A läuft vor Task B und gibt Ressource schnell frei
  • In FreeRTOS: Mutex statt Binary Semaphore verwenden

Problem 2: Deadlock

Situation:

  • Task 1 hat Mutex A, wartet auf Mutex B
  • Task 2 hat Mutex B, wartet auf Mutex A
  • Beide warten ewig!

Lösung:

  • Immer gleiche Reihenfolge beim Nehmen von Mutexen
  • Timeouts verwenden statt unendlich warten
  • Deadlock-Erkennung implementieren

Problem 3: Starvation (Verhungern)

Situation:

  • Niedrigpriore Task bekommt nie CPU-Zeit
  • Hochpriore Tasks laufen ständig

Lösung:

  • Priority Aging: Priorität steigt mit Wartezeit
  • Round-Robin für gleiche Prioritäten
  • Faire Scheduler-Strategie

Problem 4: Race Conditions

Situation:

  • Zwei Tasks greifen gleichzeitig auf Variable zu
  • Ergebnis hängt von Timing ab

Lösung:

// FALSCH - Race Condition:
shared_counter++;

// RICHTIG - Mutex-geschützt:
xSemaphoreTake(mutex, portMAX_DELAY);
shared_counter++;
xSemaphoreGive(mutex);

6.1.9 RTOS Best Practices

Task-Design

  1. Jede Task eine klare Verantwortung
  2. Sensor-Task: nur Sensor auslesen
  3. Kommunikations-Task: nur Daten senden/empfangen
  4. Verarbeitungs-Task: nur Berechnungen

  5. Tasks in Endlosschleife mit Delay

   void my_task(void *param) {
       for(;;) {
           do_work();
           vTaskDelay(pdMS_TO_TICKS(100));  // 100 ms warten
       }
   }
  1. Keine blockierenden Funktionen ohne Timeout
   // FALSCH:
   while(!data_ready);  // Busy-Wait!

   // RICHTIG:
   xSemaphoreTake(data_ready_sem, pdMS_TO_TICKS(1000));

Prioritäten-Richtlinien

  • Verwende nicht zu viele verschiedene Prioritätsstufen (3-5 reichen oft)
  • Zeitkritische ISR-Handler: höchste Priorität
  • Schnelle Reaktions-Tasks: hohe Priorität
  • Verarbeitungs-Tasks: mittlere Priorität
  • UI/Logging: niedrige Priorität

Kommunikations-Patterns

Producer-Consumer:

// Producer
sensor_data = read_sensor();
xQueueSend(queue, &sensor_data, 0);

// Consumer
xQueueReceive(queue, &data, portMAX_DELAY);
process(data);

Synchronisierung:

// Task 1
do_first_part();
xSemaphoreGive(sync_sem);

// Task 2
xSemaphoreTake(sync_sem, portMAX_DELAY);
do_second_part();

6.1.10 Zusammenfassung

Ein Echtzeitbetriebssystem ermöglicht die strukturierte Entwicklung komplexer Embedded-Anwendungen durch:

  • Task-basierte Architektur: Aufteilung in unabhängige Aufgaben
  • Prioritätsbasiertes Scheduling: Wichtige Aufgaben haben Vorrang
  • Synchronisationsmechanismen: Sichere Kommunikation zwischen Tasks
  • Deterministisches Zeitverhalten: Vorhersagbare Reaktionszeiten

Kernkonzepte:

  • Tasks mit verschiedenen Zuständen (Ready, Running, Blocked, Suspended)
  • Scheduler entscheidet über CPU-Zuteilung
  • Queues für Datenübertragung
  • Semaphoren und Mutexe für Synchronisation
  • Separater Heap für dynamische Speicherverwaltung

Wann RTOS?

  • Bei mehreren parallelen Aufgaben
  • Wenn Reaktionszeiten kritisch sind
  • Für wartbare, erweiterbare Systeme