Echtzeitbetriebssysteme Einführung in die RTOS-Welt

Wer zum ersten Mal mit dem Thema Echtzeitbetriebssysteme zu tun hat, muss sich mit einer Menge neuer Begriffe wie Task, präemptives Multitasking, Semaphoren und Prioritätsinversion auseinandersetzen. Was das alles bedeutet, soll dieser Beitrag genauer beleuchten.

Moderne Mikrocontroller werden immer komplexer und stellen dabei immer höhere Anforderungen an die Entwickler von Embedded-Software. Gleichzeitig bieten solche Chips mit komplexen Peripherieelementen und großem integriertem Speicher mehr Leistung als je zuvor. Um all diese Vorteile wirkungsvoll nutzen zu können, kommen immer öfter Echtzeitbetriebssys-teme (Real-Time Operating System, RTOS) zum Einsatz.

Was versteht man unter einem Echtzeitbetriebssystem? Einfach ausgedrückt, verwaltet eine solche Software die Zeit eines Mikroprozessors oder Mikrocontrollers. Auch bietet ein RTOS eine Multitasking-Umgebung, in der mehrere Dinge jeweils gleichzeitig ablaufen. Dazu zerlegt es ein Programm in mehrere Tasks (Bild 1). Diese Maßnahme gaukelt dem System vor, es stünden mehrere CPUs zur Verfügung. Neben dem Multitasking bietet ein Echtzeitbetriebssystem auch weitere wertvolle Dienste: Verzögerungen, der Schutz gemeinsam genutzter Ressourcen und eine Kommunikation zwischen einzelnen Tasks sowie eine Synchronisation gehören zu dessen typischen Funktionen.

Ein Task ist ein Stück Programmcode, das »denkt«, ihm stehe die CPU allein zur Verfügung. Dieser Code kann eigenständig sein oder Schnittstellen zu anderer Software im System besitzen, wie dies zum Beispiel bei einem Kommunikations-Stack (TCP/IP, USB, usw.) der Fall ist. Meist besitzt jeder Task seinen eigenen Stack-Speicherplatz und eine seiner Wichtigkeit entsprechende Priorität. So kann die Anwendungssoftware in mehrere Tasks aufgespalten sein. Diese Aufspaltung orientiert sich an modularen Funktionsblöcken, zum Beispiel einem GUI-Task, einem Audio-Task, einem Netzwerk-Stack, einer Motorsteuerung, usw.

Prinzipiell formuliert man einen Task als eine Funktion, die eine Endlosschleife enthält und daher nie zum anfänglichen Aufruf zurückkehrt. Listing 1 zeigt einen in Pseudo-Code formulierten Task. Ein Task führt die Funktion der Benutzeranwendung aus und gibt anschließend irgendeinen Blockierungsaufruf (blocking call) ab. Ein solcher Blocking-Call kann eine vom Betriebssystem abgeleitete Verzögerung (Delay), ein Wartezustand auf einen Semaphor (Wait) oder auf ein anderes Signal von einer Interrupt-Serviceroutine (ISR) oder einem anderen Task sein. Ist ein Task blockiert und lässt sich nicht mehr ausführen, wird er vom Betriebssystem ausgesetzt, sodass planmäßig ein anderer Task ablaufen kann.

Nach Ablauf der Delay-Zeit oder wenn das Ereignis eintritt, wird der Task aktiviert, und das Betriebssystem übernimmt ihn, um seine Ausführung zu disponieren (schedule).

Ein Task kann jeweils einen von drei Zuständen einnehmen (Bild 2):

Blockiert - der Task wartet auf den Eintritt eines Ereignisses oder den Ablauf eines Timeouts.

Bereit (ready) - der Task ist bereit zur Ausführung und wartet darauf, dass ihn das Echtzeit-betriebssystem für die Ausführung durch die CPU in den Schedule übernimmt.

Aktiv (running) - der Task wird aktuell auf der CPU ausgeführt; es kann sich jeweils immer nur ein Task in diesem Zustand befinden.

Präemptiver Kernel und Kontext-Switching

Wenn man sich die Spezifikation eines Echtzeitbetriebssystems ansieht, so wird man zwangsläufig auf Begriffe wie »Präemptiver Kernel« oder »unterstützt Pre-Emption« stoßen. Was genau bedeutet das? Angenommen, ein System führt einen Task mit niedriger Priorität aus, während ein Task mit höherer Priorität blockiert ist, weil er auf ein Signal von einer ISR wartet. Sobald der Interrupt auftritt und die ISR ausgeführt wird, unterbricht das Betriebssystem den Task mit niedriger Priorität. Nach Ende der Interrupt-Serviceroutine springt die Programm-ausführung dann nicht etwa zum Task mit niedriger Priorität zurück, sondern arbeitet zunächst den Task mit der hohen Priorität ab. Der Task mit hoher Priorität hat also Vorrang vor dem Task mit niedriger Priorität erhalten.

Präemption trägt dazu bei, dass ein Echtzeitbetriebssystems schnell reagieren kann, weil der betriebsbereite Task mit der höchsten Priorität vom RTOS zur Ausführung in den Schedule übernommen und auf der CPU ausgeführt wird. Es gibt verschiedene Scheduling-Algorithmen:

  • prioritätsbasiertes Scheduling,
  • FIFO (first in, first out),
  • »Round-Robin« (Reihum-Abfolge) und
  • Zeitscheibe (time slice).

Der Prozess, in dem ein Betriebssystem einen Task anhält und einen anderen ausführt, heißt Kontext-Switch. Der Kontext eines Tasks besteht aus dem Inhalt der CPU-Register einschließlich des Stack-Zeigers (stack pointer) und des Programmzählers (program counter). Tritt ein Kontext-Switch auf, verschiebt das RTOS den Inhalt der CPU-Register in den Stack eines Tasks, stellt den Task-Zustand entweder auf »bereit« oder »blockiert« und speichert den Wert des Task-Zeigers im Task-Control-Block des entsprechenden Tasks ab. Anschließend stellt das Betriebssystem den Kontext des Tasks, der auf der CPU ausgeführt werden soll, wieder her. Dazu stellt es den Stack-Pointer anhand der Daten aus dem Task-Control-Block wieder her und liest den Inhalt der CPU-Register aus dem Stack des Tasks in die CPU zurück. Den Zustand des entsprechenden Tasks stellt das RTOS auf »aktiv«, und der so wiederhergestellte Task wird nun weiter ausgeführt.

Während eines Kontext-Switches führt das Echtzeitbetriebssystem eine interne Operation durch, sodass die Benutzeranwendung während dieser Zeit nicht ausgeführt werden kann. Je weniger Zeit also die Ausführung des Kontext-Switches benötigt, umso besser. Anbieter von Echtzeitbetriebssystemen spezifizieren oft die für die Durchführung eines Kontext-Switches benötigte Zeit für eine bestimmte CPU-Familie.

Der Zeitbedarf für einen Kontext-Switch lässt sich oft durch Nutzung spezieller Funktionen der verwendeten CPU minimieren. So besitzen zum Beispiel die CPU-Kerne »H8S«, »H8SX« und »SH-2A« von Renesas spezielle Befehle, mit denen sich der Inhalt mehrerer CPU-Register zum oder aus dem Stack verschieben lässt. Dementsprechend ist nur eine einzige Instruktion aufzurufen und auszuführen, um mehrere Aus- oder Einlesevorgänge durchzuführen. Dies vermindert den Code-Umfang und beschleunigt die Ausführung. Darüber hinaus unterstützt die SH-2A-CPU »Register-Banking«. Bei dieser Funktion lassen sich innerhalb von nur sechs Zyklen eine ISR eingeben und alle CPU-Register in eine Registerbank kopieren. Dadurch kann das System auf Interrupt-Signale in Echtzeit reagieren.