Embedded C/C++: ein verbessertes OS für Arduino

Ein Mini-RTOS für den Arduino Uno - Teil 3

In Teil 2 dieser kleinen Reihe wurde bereits ein sehr einfaches Betriebssystem für den Arduino vorgestellt. Die Rollen von Betriebssystem und ausführbaren Tasks waren jedoch nicht eindeutig bzw. stark miteinander verwoben. Die nächste Stufe des Arduino RTOS ist diesbezüglich stark verbessert worden.

Das Hauptprogramm



Das Hauptprogramm schaut ganz anders aus als die sonst übliche Struktur aus  setup() { ... } und loop() { ... }.


Kern des Betriebssystems ist der fortwährende Aufruf des Task-Managers:

while ( os_task_manager () );


Der Task-Manager wird ganz zu Beginn mit der Zykluszeit des Betriebssystems initialisiert, hier 20ms: os_init_task_mgr ( 20 );


Aufgaben werden dem Task-Manager mit os_add_task ( ... ) übergeben. Das gilt auch für den sonst üblichen Prolog (setup() { ... }) zur Hauptschleife, hier als myinit-Task angemeldet und nur ein einziges Mal (TASK_ONCE) ausgeführt wird.


Was für ein "schlankes" Hauptprogramm! Und wie sieht die dazugehörige OS-Implementierung aus?

Was Anwender über das OS wissen müssen


Die API zur Nutzung des Betriebssystems ist in os.hpp beschrieben.


Es gibt so gut wie keine Eingriffsmöglichkeiten in den Kernel. Lediglich über die Funktion uint8_t os_init_task_mgr ( uint16_t OS_heartbeat_ms ) ist es möglich, die Zykluszeit zu beeinflussen.

Den eigentlichen Task-Manager kann man mit os_task_manager () aufrufen und wenn Zing keinen Mist gebaut hat, dann wird das schon funktionieren.


Einzig wirklich relevante Schnittstelle für Anwendungsprogrammierer ist der Systemaufruf os_add_task ( ... ), mit dem der Programmierer dem Task-Manager Aufgaben zuteilen kann. Dabei lassen sich z.B. die Priorität oder die Wiederholrate der Task festlegen.


Diese API soll in der Zukunft um weitere Funktionen erweitert werden, um Tasks z.B. anzuhalten, zu löschen oder sonst zu konfigurieren.


Als proof of concept für das verbesserte Betriebssystem genügt der hier vorgestellte Funktionsumfang allemal.

Futter für den Task-Manager


Damit der Task-Manager verstehen kann, was eine Task ist und wie sie von ihm behandelt werden soll, benötigt der Task-Manager eine Beschreibung der Task. Diese Beschreibung iner Task liefert die Struktur task_descriptor_t. Mit task_descriptor_t werden u.a. die Priorität, der Status und die erforderlichen Zeitparameter einer Task festgehalten.

Nicht zuletzt wird auch mitgeteilt, was denn eine Task überhaupt tut. Das wird mittels eines Zeigers auf eine Funktion erreicht: uint8_t (*pt_task) ( void ).


Sämtliche task_descriptor werden zu einer Liste im heap-Speicher hinzugefügt: task_list


task_entries enthält lediglich die Anzahl der Einträge in der task_list.


Diese task_list kann nun der  Task-Manager regelmäßig abarbeiten.

Bevor beschrieben wird, wie ein Task-Manager das tut, muss jedoch auf die wichtigste Task des gesamten Betriebssystems eingegangen werden.


Wenn Nichtstun zur Pflicht wird


Wenn der Anwendungsprogrammierer versäumt überhaupt irgendeine Task beim Task-Manager anzumelden, dann muss das Betriebssystem trotzdem funktionieren!


Diese Art der Untätigkeit ist auch eine Aufgabe, nämlich die os_idle_task (). Die Idle-Task sorgt dafür, dass das Betriebssystem im Betriebssystemtakt den nächsten Durchlauf für den Task-Manager anstößt. Auch der watchdog wird an dieser Stelle zurückgesetzt. Weil die os_idle_task () so wichtig für die Funktion des Betriebssystems ist, verfügt sie auch über die höchste Priorität aller Tasks. Keine andere Anwender-Task kann eine höhere Priorität haben. (Wenn man dem Chef erklären würde, dass Untätigkeit die wichtigste Aufgabe ... )


Die os_idle_task () muss der Anwendungsprogrammierer nicht anmelden. Das geschieht implizit beim Aufruf von os_init_task_mgr ( ... ).

Was mennähtscht ein Task-Manager eigentlich?


Der Task-Manger arbeitet regelmäßig sämtliche Einträge der task_list sequenziell ab.


Abhängig von Status der jeweiligen Task, wird diese dann


  • einmalig ausgeführt (TASK_ONCE),
  • zu bestimmten Zeitpunkten regelmäßig ausgeführt (TASK_START) oder
  • nicht ausgeführt (TASK_STOP).


(TASK_WAIT ist bisher nicht implementiert, wird jedoch später benötigt, wenn die Tasks kooperativ werden.)











Und mehr gibt es dazu auch gar nicht zu berichten. Es sollte aber noch erklärt werden, wie Anwendungsprogrammierer eine Task erstellen können.

Wie schreibt man eine Task?


Tasks werden fernab des Betriebssystems in task.hpp bekannt gemacht und in task.cpp implementiert.


Jede task muss dem Aufrufschema uint8_t any_task ( void ) folgen. Anderslautende Definitionen würde das Betriebssystem nicht akzeptieren.

Nach erfolgreicher Abarbeitung muss jede Task 0x01 zurückgeben.

Innerhalb einer Task verwendete Variablen müssen unbedingt static deklariert werden, weil die Task ansonsten nach jedem Aufruf durch den Task-Manager/das Betriebssystem ihr "Gedächtnis" verlieren würde, außer das sei vom Anwendungsprogrammierer ausdrücklich erwünscht.





Ganz schön viel Aufwand, um '*' und '#' auf dem Terminal ausgeben und die eingebaute LED wild flackern zu lassen ...

Wie könnte es weitergehen? Jedes vernünftige Betriebssystem verfügt über eine Systemuhr mit Datum und Zeit. Mit so einer Systemuhr ließe sich dann auch ein sog. Scheduler betreiben. Eine einfache Kommandoschnittstelle und eine Shell würden einem Anwender die Interaktion mit dem Betriebssystem möglich machen.

Die Frage, wie Tasks kooperativ werden, ist noch zu klären. Zudem fehlt noch ein Mechanismus, wie verschiedene Tasks (z.B. IO-Task und Ctrl-Task) miteinander Daten austauschen können.

Und wenn wir schon bei Daten sind, wie wäre es mit einem Dateisystem und zur Laufzeit ladbaren Programmen?

Den Code zum Blog gibt es hier:


(Übrigens, alles noch mit der Arduino-IDE erstellt.)

Zing • 2. Januar 2026