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, mitfree()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:
- Task A (niedrige Prio) sperrt Ressource mit Mutex
- Task C (hohe Prio) will dieselbe Ressource → blockiert
- Task B (mittlere Prio) läuft und verhindert, dass A fertig wird
- 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
- Jede Task eine klare Verantwortung
- Sensor-Task: nur Sensor auslesen
- Kommunikations-Task: nur Daten senden/empfangen
-
Verarbeitungs-Task: nur Berechnungen
-
Tasks in Endlosschleife mit Delay
void my_task(void *param) {
for(;;) {
do_work();
vTaskDelay(pdMS_TO_TICKS(100)); // 100 ms warten
}
}
- 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