Moderne Embedded-Systeme müssen mehrere Aufgaben parallel bearbeiten: Sensoren auslesen, WLAN verwalten oder Benutzeroberflächen aktualisieren. Der ESP32 unterstützt dies mit mit FreeRTOSTM einem echten Multitasking-Betriebssystem.
Während klassische Arduino-Programme meist nur aus einer einzelnen loop()-Funktion bestehen, ermöglicht FreeRTOSTM die Aufteilung eines Programms in mehrere unabhängige Tasks. Diese Tasks können parallel ausgeführt werden und erleichtern die Strukturierung komplexerer Anwendungen erheblich. Gleichzeitig entstehen dadurch jedoch neue Herausforderungen: Mehrere Tasks greifen oft auf gemeinsame Ressourcen zu - beispielsweise auf die serielle Schnittstelle, Sensoren oder Speicherbereiche. Ohne geeignete Synchronisation können dabei schwer nachvollziehbare Fehler auftreten.
In diesem Tutorial werden typische Probleme und Lösungsansätze aus der nebenläufigen Programmierung anhand leicht verständlicher Beispiele untersucht. Zunächst wird gezeigt, dass zwei parallel laufende Tasks gleichzeitig Ausgaben auf die serielle Schnittstelle schreiben können. Da diese Ressource nicht automatisch geschützt ist, vermischen sich die Ausgaben und werden unleserlich. Anschließend wird dieses Problem mithilfe eines Semaphors gelöst, sodass immer nur ein Task gleichzeitig auf die serielle Schnittstelle zugreift.
Die Beispiele orientieren sich bewusst an realen Problemen aus der Embedded-Entwicklung und vermitteln wichtige Grundlagen für robuste und zuverlässige Multitasking-Anwendungen auf dem ESP32.
Zielsetzung
Nach Abschluss dieses Tutorials sollen die Studierenden:
Darüber hinaus soll das Tutorial ein grundlegendes Verständnis dafür vermitteln, warum Betriebssystemkonzepte wie Synchronisation und Ressourcenverwaltung auch in kleinen eingebetteten Systemen von zentraler Bedeutung sind.
Ein Arduino-Programm arbeitet normalerweise nach einem sehr einfachen Prinzip:
void setup() {
// Initialisierung
}
void loop() {
// Wird endlos wiederholt
}
Die Funktion loop() läuft dabei kontinuierlich auf einem einzelnen Ablaufpfad. Müssen mehrere Aufgaben erledigt werden, werden diese häufig nacheinander innerhalb der Schleife ausgeführt. Mit FreeRTOSTM können dagegen mehrere unabhängige Tasks erstellt werden:
xTaskCreate(
taskFunction, // Funktion der Task
"TaskName", // Name der Task
2048, // Stackgröße
NULL, // Parameter
1, // Priorität
NULL // Task-Handle
);
Jeder Task besitzt dabei: einen eigenen Programmablauf, einen eigenen Stack, eine Priorität, sowie einen eigenen Zustand. Der Scheduler von FreeRTOSTM entscheidet, welcher Task gerade ausgeführt wird.
Nebenläufigkeit und gemeinsame Ressourcen
Wenn mehrere Tasks gleichzeitig arbeiten, greifen sie häufig auf gemeinsame Ressourcen zu. Solche gemeinsam genutzten Ressourcen bezeichnet man als kritische Ressourcen. Greifen mehrere Tasks gleichzeitig auf eine kritische Ressource zu, können Fehler entstehen. Dieses Problem wird als Race Condition bezeichnet. Ein Bereich im Programm, in dem auf eine gemeinsame Ressource zugegriffen wird, heißt kritischer Abschnitt. Damit nicht mehrere Tasks gleichzeitig diesen Abschnitt betreten, muss der Zugriff synchronisiert werden. Ein Semaphore dient allgemein zur Synchronisation zwischen Tasks. Damit kann signalisiert werden, dass ein Ereignis eingetreten ist oder eine Ressource verfügbar wird. Mehrere Tasks können dabei denselben Semaphore verwenden. Man kann sich ein Semaphor wie einen Schlüssel vorstellen:
Ist der Schlüssel verfügbar, darf ein Task die Ressource benutzen. Ist der Schlüssel bereits vergeben, muss der andere Task warten. Ein Semaphor besitzt zwei grundlegende Operationen:
xSemaphoreTake() Ressource anfordern
xSemaphoreGive() Ressource freigeben
Wir nutzen dabei einen Mutex (Mutual Exclusion), d. h. eine spezielle Form eines Semaphors zum Schutz gemeinsamer Ressourcen. Ein Mutex besitzt zusätzlich weitere Mechanismen wie Priority Inheritance, um Probleme wie Priority Inversion zu vermeiden.
Im ersten Beispiel erzeugen wir zwei parallele Tasks, die gleichzeitig auf die serielle Schnittstelle schreiben. Unser normaler Arduino-Sketch (oranges Puzzleteil) gibt einen Text aus. Dieser Text ist in zwei Teile aufgeteilt, zwischen denen es eine kurze Pause gibt. Parallel dazu erstellen wir einen zweiten Task (Werkzeugkasten ESP32:System, schwarzes Puzzleteil), welcher seinerseits einen Text ausgibt. Als Ergebnis beobachten wir im seriellen Monitor ein Durcheinander der ausgegebenen Texte.
Die serielle Schnittstelle stellt offenbar einen kritischen Pfad dar. Ist der Sketch bei der Ausgabe seines Textes, so darf der zweite Task nicht gleichzeitig auch etwas ausgeben. Um dies zu verhindern, fügen wir einen Semaphore mit Namen “Semaphore_Serial” ein. Bevor wir jetzt den kritischen Pfad betreten, fordern wir beim Betriebssystem mit SemaTake den Schlüssel an. Solange wir den Sema besitzen, kann kein anderer Task den kritischen Pfad betreten. Nach der Ausgabe geben wir diesen Pfad mit SemaGive wieder frei.
Im ersten Teil dieses Tutorials wurde gezeigt, dass parallele Tasks auf dem ESP32 gleichzeitig auf gemeinsame Ressourcen zugreifen können. Am Beispiel der seriellen Schnittstelle wurde deutlich, dass unkoordinierter Zugriff zu fehlerhaften oder unleserlichen Ausgaben führt. Solche Probleme treten in Multitasking-Systemen häufig auf und werden als Race Conditions bezeichnet.
Durch den Einsatz eines Semaphors konnte der Zugriff auf die serielle Schnittstelle koordiniert werden. Immer nur ein Task durfte gleichzeitig den kritischen Abschnitt betreten. Dadurch blieb die Ausgabe konsistent und nachvollziehbar. Semaphore sind damit ein wichtiges Werkzeug zur Synchronisation paralleler Prozesse in Embedded-Systemen.
Allerdings löst Synchronisation nicht automatisch alle Probleme nebenläufiger Systeme. Werden mehrere Ressourcen gleichzeitig verwendet, können neue Schwierigkeiten entstehen. Im nächsten Abschnitt wird daher ein weiteres klassisches Problem der Betriebssystemtechnik untersucht: die Verklemmung (Deadlock).
FreeRTOSTM is a trademark of Amazon.com, Inc. or its affiliates.
Sie verlassen die offizielle Website der Hochschule Trier