Reihe: Embedded World – Die unsichtbaren Gehirne verstehen (Teil 9)
Wenn es komplex wird – Echtzeitbetriebssysteme (RTOS) für Embedded Systems
Von DerSchneider
Einleitung: Der Dirigent im Chaos
Im letzten Artikel haben wir die Welt der Bare-Metal-Programmierung erkundet – die Kunst, mit einer einfachen Super-Loop und geschickt platzierten Interrupts auch komplexe Abläufe zu bewältigen. Doch diese Kunst hat ihre Grenzen. Sobald ein System mehr als ein halbes Dutzend Aufgaben koordinieren muss, sobald Prioritäten eine Rolle spielen und Aufgaben miteinander kommunizieren müssen, wird der Code schnell unübersichtlich, fehleranfällig und schwer wartbar.
Hier kommen Echtzeitbetriebssysteme ins Spiel – Real-Time Operating Systems, kurz RTOS. Sie sind die Dirigenten im Chaos der vielen Aufgaben. Sie übernehmen das Scheduling, die Synchronisation und die Kommunikation und erlauben dem Entwickler, sich auf die eigentliche Anwendungslogik zu konzentrieren.
Dieser Artikel führt in die Welt der RTOS ein. Wir lernen die grundlegenden Konzepte kennen, verstehen, wie sich ein RTOS von Desktop-Betriebssystemen unterscheidet, und sehen an Beispielen, wie Tasks, Queues und Semaphoren die Entwicklung komplexer Embedded Systems erleichtern.
Hauptteil
1. Was ist ein RTOS – und was nicht?
Ein Echtzeitbetriebssystem ist ein Betriebssystem, das für Anwendungen mit Zeitbedingungen entwickelt wurde. Es garantiert, dass bestimmte Aufgaben innerhalb vorgegebener Fristen ausgeführt werden.
Wichtig: Ein RTOS ist nicht einfach eine abgespeckte Version von Windows oder Linux. Die Unterschiede sind fundamental:
| Merkmal | Desktop-OS (Windows, Linux) | RTOS (FreeRTOS, Zephyr) |
|---|---|---|
| Ziel | Durchschnittlicher Durchsatz | Garantierte Reaktionszeiten |
| Scheduling | Fairness, alle kommen dran | Prioritäten, wichtige Tasks zuerst |
| Speicherschutz | Geschützte Adressräume | Meist ein gemeinsamer Adressraum |
| Vorhersagbarkeit | Nicht garantiert | Deterministisch |
| Größe | Gigabyte | Kilobyte |
| Echtzeit | „Weich“ (meist) | „Hart“ (garantiert) |
Ein RTOS ist kein Monolith, sondern eine Sammlung von Diensten, die der Entwickler nach Bedarf zuschalten kann. Es passt in wenige Kilobyte Speicher und läuft auf Mikrocontrollern, die zu schwach für auch nur einen Linux-Thread wären.
2. Die Grundbausteine: Tasks
Das zentrale Konzept jedes RTOS ist der Task (auch Thread oder Prozess genannt). Ein Task ist eine eigenständige Ausführungseinheit mit eigenem Stack und eigener Priorität.
Im Gegensatz zur Super-Loop, wo alles in einer einzigen Schleife läuft, werden bei einem RTOS die verschiedenen Aufgaben auf mehrere Tasks verteilt:
c
// Task für Tasterabfrage
void task_taster(void *params) {
while(1) {
int taster = gpio_lesen(TASTER_PIN);
if (taster) {
xQueueSend(tasterQueue, &taster, 0);
}
vTaskDelay(pdMS_TO_TICKS(10)); // 10 ms warten
}
}
// Task für Display-Aktualisierung
void task_display(void *params) {
while(1) {
display_aktualisieren();
vTaskDelay(pdMS_TO_TICKS(50)); // 50 ms warten
}
}
// Task für Sensorauslesung
void task_sensor(void *params) {
while(1) {
int wert = sensor_lesen();
daten_speichern(wert);
vTaskDelay(pdMS_TO_TICKS(1000)); // 1 Sekunde warten
}
}
Jeder Task lebt sein eigenes Leben. Der Scheduler entscheidet, welcher Task wann läuft – basierend auf Prioritäten und Ereignissen.
3. Das Herzstück: Der Scheduler
Der Scheduler ist die wichtigste Komponente eines RTOS. Er entscheidet, welcher Task als nächstes die CPU bekommt.
Die gängigsten Scheduling-Verfahren:
Prioritätsbasiertes Scheduling: Jeder Task hat eine Priorität. Immer der Task mit der höchsten Priorität, der lauffähig ist, bekommt die CPU. Das ist einfach und effizient, kann aber dazu führen, dass niederpriore Tasks verhungern, wenn hochpriore immer etwas zu tun haben.
Round-Robin: Tasks gleicher Priorität teilen sich die CPU fair, indem sie abwechselnd für eine feste Zeitscheibe laufen. Verhindert das Verhungern, aber weniger deterministisch.
Rate-Monotonic-Scheduling: Eine mathematisch fundierte Methode, bei der Tasks mit kürzeren Perioden höhere Prioritäten bekommen. Für bestimmte Aufgabenklassen lässt sich damit mathematisch beweisen, dass alle Fristen eingehalten werden.
Die meisten RTOS verwenden eine Mischung: prioritätsbasiertes Scheduling mit optionalem Round-Robin für gleiche Prioritäten.
4. Kommunikation: Queues und Mailboxen
In einem Multitasking-System müssen Tasks miteinander kommunizieren. Ein Task produziert Daten, ein anderer konsumiert sie. Dafür gibt es Queues (Warteschlangen):
c
// Queue erstellen
QueueHandle_t sensorQueue = xQueueCreate(10, sizeof(int));
// Task 1: Sensor auslesen und Wert in Queue senden
void task_sensor(void *params) {
int wert;
while(1) {
wert = sensor_lesen();
xQueueSend(sensorQueue, &wert, portMAX_DELAY);
vTaskDelay(100);
}
}
// Task 2: Werte aus Queue empfangen und verarbeiten
void task_logger(void *params) {
int wert;
while(1) {
xQueueReceive(sensorQueue, &wert, portMAX_DELAY);
sd_karte_schreiben(wert);
}
}
Die Queue entkoppelt die Tasks. Der Sensor-Task kann Werte produzieren, auch wenn der Logger-Task gerade beschäftigt ist. Die Werte werden zwischengespeichert. Und wenn die Queue voll ist, kann der Sender warten oder einen Fehler melden.
5. Synchronisation: Semaphoren und Mutexe
Wenn Tasks gemeinsame Ressourcen nutzen (z.B. eine serielle Schnittstelle oder einen Speicherbereich), müssen sie sich synchronisieren, damit nicht beide gleichzeitig zugreifen.
Binäre Semaphore: Ein einfaches Flag, das anzeigt, ob ein Ereignis eingetreten ist. Ein Task wartet darauf, ein anderer signalisiert es.
c
// Taster-ISR
void ISR_taster(void) {
xSemaphoreGiveFromISR(tasterSem, NULL); // Semaphor geben
}
// Task wartet auf Taster
void task_taster_aktion(void *params) {
while(1) {
xSemaphoreTake(tasterSem, portMAX_DELAY); // Warten
aktion_ausfuehren(); // Wird bei Tastendruck ausgeführt
}
}
Mutex (Mutual Exclusion): Ein spezieller Semaphor für den Zugriff auf gemeinsame Ressourcen. Er verhindert, dass zwei Tasks gleichzeitig auf dieselbe Ressource zugreifen.
c
// Gemeinsame Ressource: Serielle Schnittstelle
void task_send1(void *params) {
while(1) {
xSemaphoreTake(serialMutex, portMAX_DELAY);
printf("Task 1: Hallo Welt\n");
xSemaphoreGive(serialMutex);
vTaskDelay(100);
}
}
void task_send2(void *params) {
while(1) {
xSemaphoreTake(serialMutex, portMAX_DELAY);
printf("Task 2: Guten Tag\n");
xSemaphoreGive(serialMutex);
vTaskDelay(100);
}
}
Ohne Mutex würden sich die Ausgaben der beiden Tasks wild vermischen. Mit Mutex ist sichergestellt, dass jeder Task die Ausgabe ungestört zu Ende bringen kann.
6. Zeitdienste: Timer und Delays
RTOS bieten auch komfortable Zeitdienste:
Task-Delay: vTaskDelay(100) lässt den Task für 100 Ticks schlafen. Der Scheduler läuft inzwischen andere Tasks.
Software-Timer: Timer, die nach Ablauf eine Funktion aufrufen oder einen Task benachrichtigen. Nützlich für periodische Aktionen.
Timestamp-Dienste: Funktionen, um die vergangene Zeit hochpräzise zu messen.
7. Ein RTOS im Vergleich: FreeRTOS
Das mit Abstand am weitesten verbreitete RTOS ist FreeRTOS. Es ist Open Source, extrem portabel, läuft auf Dutzenden von Plattformen und ist klein genug für winzige Mikrocontroller.
FreeRTOS bietet:
- Prioritätsbasiertes Scheduling
- Tasks, Queues, Semaphoren, Mutexe
- Software-Timer
- Speicherverwaltung (mehrere Optionen)
- Hunderte von Portierungen
- Eine riesige Community
Ein komplettes FreeRTOS-System passt oft in 5-10 KB Flash und 1 KB RAM – Platz genug für erstaunlich komplexe Anwendungen.
8. Die Kosten eines RTOS
Ein RTOS ist kein Geschenk. Es hat seinen Preis – nicht in Geld, aber in anderen Ressourcen:
Speicher: Jeder Task braucht eigenen Stack. Bei vielen Tasks kann das den RAM schnell füllen.
CPU-Zeit: Der Scheduler verbraucht Rechenzeit. Bei jedem Taskwechsel, bei jedem Interrupt läuft RTOS-Code.
Komplexität: Ein RTOS-System ist schwerer zu debuggen als eine Super-Loop. Task-Wechsel, Prioritäten und Synchronisation können zu schwer reproduzierbaren Fehlern führen.
Determinismus: Obwohl RTOS für Echtzeit gemacht sind, können sie durch Prioritätsinversion und andere Effekte unvorhersehbares Verhalten zeigen.
9. Prioritätsinversion – Die RTOS-Falle
Ein klassisches Problem in RTOS ist die Prioritätsinversion. Stellen Sie sich vor:
- Task H (hohe Priorität) braucht eine Ressource.
- Task L (niedrige Priorität) hält die Ressource, kann aber nicht laufen, weil Task M (mittlere Priorität) gerade rechnet.
- Task H wartet auf L, L wartet auf M, M rechnet fröhlich vor sich hin.
Der hochpriore Task H muss warten, bis der mittelpriore Task M fertig ist – eine Umkehrung der Prioritäten.
Lösung: Priority Inheritance. Wenn ein niederpriorer Task eine Ressource hält, die ein hochpriorer braucht, bekommt der niederpriore kurzzeitig die Priorität des hochprioren. So kann er schnell fertig werden und die Ressource freigeben.
10. Wann RTOS, wann Bare-Metal?
Die Entscheidung ist nicht immer einfach. Hier eine Faustregel:
Bare-Metal ist besser, wenn:
- Das System sehr klein ist (wenige Aufgaben, wenig RAM)
- Die Anforderungen extrem deterministisch sind
- Sie maximale Kontrolle brauchen
- Der Stromverbrauch minimiert werden muss (Schlafmodi)
RTOS ist besser, wenn:
- Mehr als 5-10 Aufgaben koordiniert werden müssen
- Aufgaben unterschiedliche Prioritäten haben
- Komplexe Kommunikation zwischen Tasks nötig ist
- Sie Code zwischen Plattformen portieren wollen
- Sie vorhandene RTOS-Komponenten nutzen können
Viele Embedded-Entwickler haben beide Ansätze im Werkzeugkasten und entscheiden von Projekt zu Projekt.
Fazit und Ausblick
Echtzeitbetriebssysteme sind mächtige Werkzeuge für komplexe Embedded-Anwendungen. Sie entlasten den Entwickler von der Koordination der vielen Aufgaben und bieten bewährte Mechanismen für Kommunikation und Synchronisation. Tasks, Queues und Semaphoren sind die Grundbausteine, aus denen sich auch anspruchsvolle Systeme bauen lassen.
Doch mit der Macht kommt die Verantwortung. Ein RTOS ist kein Allheilmittel. Es erfordert ein tiefes Verständnis der Konzepte und der möglichen Fallstricke. Prioritätsinversion, Deadlocks und Race Conditions sind die Schreckgespenster, die im RTOS-Schrank lauern.
Aber selbst das beste Betriebssystem nützt nichts, wenn das Programm Fehler enthält. Wie findet man Fehler in einem System, das keine Tastatur und keinen Bildschirm hat? Wie debuggt man Code, der in Echtzeit auf unsichtbare Hardware zugreift?
Mit dieser Kunst des Debuggens beschäftigen wir uns im nächsten Artikel.
Kommentar abschicken