Multitasking auf dem Arduino: Blockaden lösen ohne RTOS
Wer mit dem Arduino arbeitet, stößt schnell an eine Grenze: Sobald man eine LED blinken lässt und gleichzeitig einen Taster abfragen oder einen Motor steuern möchte, reagiert das System träge oder gar nicht. Dieser Artikel erklärt, wie man echtes Multitasking ohne ein komplexes Echtzeitbetriebssystem (RTOS) realisiert.

Das Problem: Blockierende Schleifen und der Timer-Überlauf
Das Hauptproblem bei vielen Arduino-Sketchen ist die Function delay(). Während ein delay(1000) läuft, steht der Prozessor für eine Sekunde komplett still. In dieser Zeit können keine Sensoren ausgelesen, keine Benutzereingaben verarbeitet und keine weiteren Berechnungen angestellt werden. Das System ist "blockiert".
Die Gefahr des Timer-Überlaufs
Um Zeit ohne Blockade zu messen, nutzt man die Function millis(), die die Millisekunden seit dem Systemstart zählt. Dieser Wert wird in einer Variable vom Typ unsigned long gespeichert (32 Bit), die einen Bereich von 0 bis 4.294.967.295 abdeckt.
- Der Überlauf: Nach etwa 50 Tagen (genau 49 Tage, 17 Stunden) erreicht der Zähler seinen Maximalwert und springt zurück auf 0.
- Falsche Berechnung: Wer versucht, die Zeit mit
millis() >= (startZeit + Intervall)zu prüfen, riskiert, dass die Bedingung nach 50 Tagen sofort wahr wird, obwohl die Zeit noch nicht um ist. - Die sichere Lösung: Nur die Subtraktion $(millis() - startZeit) \ge Intervall$ ist mathematisch sicher gegenüber dem Überlauf, da das Ergebnis bei vorzeichenlosen Ganzzahlen (unsigned) auch beim Umspringen korrekt bleibt.
Die Lösung: Die millisDelay-Bibliothek
Anstatt die Logik der Zeitberechnung jedes Mal manual zu schreiben, bietet die SafeString-Bibliothek (V3+) die Klasse millisDelay. Diese übernimmt das Zeitmanagement im Hintergrund, ist sicher gegen den 50-Tage-Überlauf und macht den Code lesbarer.
Das Prinzip dahinter ist der Task-basierte Ansatz:
- Jede Aufgabe (z. B. LED blinken, Sensor lesen) wird in eine eigene Method ausgelagert.
- Diese Methods werden in der
loop()nacheinander aufgerufen. - Jede Method prüft kurz, ob sie etwas zu tun hat (Zeit abgelaufen?), erledigt dies sofort und gibt die Kontrolle direkt wieder an die
loop()zurück.
Beispiele: Einmaliges Delay und Repeating Timer
Hier sehen Sie, wie die Bibliothek die Programmierung vereinfacht:
1. Einmaliges Delay (Single-Shot)
Ein Single-Shot-Delay ersetzt das klassische delay(), ohne den Rest des Codes anzuhalten.
- Anwendung: Eine Aktion soll genau einmal nach einer bestimmten Zeit ausgeführt werden, z. B. eine LED nach 10 Sekunden ausschalten.
- Ablauf: Man started den Timer mit
start(10000). In jederloop()wird mitjustFinished()geprüft, ob die Zeit um ist.
2. Wiederkehrender Timer (Repeating Timer)
Dies ist ideal für Aufgaben, die regelmäßig stattfinden müssen.
- Anwendung: Ein Herzschlag-Signal oder das regelmäßige Auslesen eines Temperatursensors.
- Besonderheit: Die Bibliothek bietet die Function
repeat(). Im Gegensatz zu einem Neustart (restart()) korrigiertrepeat()kleine zeitliche Verzögerungen, die durch andere Aufgaben in derloop()entstehen könnten (sog. Drift), und hält das Interval langfristig präzise.
Praktische Beispiele
1. Declaration (Declaration)
In diesem Bereich werden die Bibliotheken eingebunden und die Timer-Objekte instanziiert. Es wird zwischen einem wiederholenden Timer (Repeating Timer) und einer einmaligen Verzögerung (Single-Shot Delay) unterschieden.
#include <millisDelay.h>
// Definition der Zeit-Intervalle
const unsigned long BLINK_INTERVAL = 1000; // 1 Sekunde
const unsigned long WELCOME_INTERVAL = 5000; // 5 Sekunden
// Instanzen der millisDelay-Klasse
millisDelay ledTimer; // Timer für das Blinken (Repeating)
millisDelay welcomeDelay; // Einmalige Verzögerung (Single-Shot)
const int LED_PIN = 13;
2. Initialisierung (Setup)
Im setup() werden die Pins konfiguriert und die Timer gestartet. Das Starten am End des Setups stellt sicher, dass die Zeitmessung genau mit dem Begin der Hauptschleife started.
void setup() {
Serial.begin(9600);
pinMode(LED_PIN, OUTPUT);
// Timer starten
ledTimer.start(BLINK_INTERVAL);
welcomeDelay.start(WELCOME_INTERVAL);
Serial.println("System startet…");
}
3. Hauptschleife (Loop) und Tasks
Die loop() bleibt "schlank", indem sie lediglich verschiedene Aufgaben (Tasks) aufruft. Jede Aufgabe prüft eigenständig, ob ihre Zeit abgelaufen ist, und gibt die Kontrolle sofort wieder an die Schleife zurück.
void loop() {
// Aufruf der einzelnen Aufgaben
taskHeartbeat();
taskOneTimeMessage();
// Hier könnten weitere Aufgaben stehen, die sofort ausgeführt werden müssen
}
// Aufgabe: LED blinken lassen (Wiederholend)
void taskHeartbeat() {
// justFinished() muss in jedem Durchlauf aufgerufen werden
if (ledTimer.justFinished()) {
// Startverzögerung ohne Drift
ledTimer.repeat();
digitalWrite(LED_PIN, !digitalRead(LED_PIN)); // LED Zustand umkehren
}
}
// Aufgabe: Eine Nachricht nach 5 Sekunden senden (Einmalig)
void taskOneTimeMessage() {
if (welcomeDelay.justFinished()) {
Serial.println("Willkommen! Das System ist seit 5 Sekunden aktiv.");
// welcomeDelay wird nicht neu gestartet und läuft daher nicht mehr
}
}
Performance-Check: Der Loop Monitor
Um sicherzustellen, dass dein Code tatsächlich effizient läuft, ist ein „Loop Monitor“ unerlässlich. Da jede Verzögerung in einer der Aufgaben (Tasks) die gesamte loop() ausbremst, sollte die Durchlaufgeschwindigkeit überwacht werden. Ein langsamer Loop führt dazu, dass Timer unpräzise werden und Eingaben verloren gehen. Mit der Klasse loopTimer aus der SafeString-Bibliothek kannst du messen, wie oft die Hauptschleife pro Sekunde durchlaufen wird und wie viel Zeit die langsamste Aufgabe beansprucht. Als Faustregel gilt:
Eine gut programmierte loop() sollte eine Frequenz von über 500 Hz (weniger als 2 ms pro Durchlauf) beibehalten. Sinkt dieser Wert drastisch, hast du eine blockierende Stelle im Code identifiziert, die optimiert werden muss.
Fazit und Best Practices
Einfaches Multitasking auf dem Arduino erfordert kein RTOS, sondern lediglich eine disziplinierte Programmierung.
Best Practices für einen reaktionsschnellen Arduino:
loop()schnell halten: Die Hauptschleife sollte so oft wie möglich pro Sekunde durchlaufen (idealerweise über 500 Hz). Achte darauf, dass keine andere Function innerhalb derloop()(wie z. B. langsameSerial.print()-Ausgaben) die Schleife blockiert, damitjustFinished()rechtzeitig abgefragt werden kann.delay()verbannen: Nutzen Sie konsequentmillisDelayfür alle zeitabhängigen Aufgaben.- Vermeidung von Drift: Verwenden Sie
repeat()anstelle vonrestart(), wenn Aufgaben in exakten Intervallen (z. B. 100 Mal pro Sekunde) ausgeführt werden sollen. - Serielle Blockaden vermeiden:
Serial.print()kann dieloop()verlangsamen, wenn der Puffer voll ist. Nutzen Sie stattdessenBufferedOutputaus der SafeString-Bibliothek. - Bibliotheken prüfen: Viele Drittanbieter-Bibliotheken nutzen intern
delay(). Diese sollten modifiziert oder durch nicht-blockierende Alternativen ersetzt werden. - Loop-Timer verwenden: Nutzen Sie die
loopTimer-Klasse, um während der Entwicklung die Geschwindigkeit Ihrerloop()zu überwachen und Engpässe zu finden.
Quellenangabe
Simple Multitasking Arduino on any board without using an RTOS by Matthew Ford