Multitasking ohne Betriebssystem: Millis() statt Delay() ist eines der wichtigsten Konzepte, wenn Ihre Mikrocontroller-Projekte von „Spielerei“ zu stabilen Anwendungen wachsen. Viele Einsteiger starten mit delay(), weil es einfach ist: LED an, warten, LED aus, warten. Das funktioniert bei einem Blink-Sketch perfekt – aber sobald mehrere Dinge parallel passieren sollen, wird delay() zum Problem. Während der Mikrocontroller „wartet“, macht er nichts anderes: keine Buttons abfragen, keine Sensorwerte sauber erfassen, keine Netzwerkverbindung pflegen, keine Zustandswechsel ausführen. Genau dann entstehen typische Symptome: Taster reagieren verzögert, Displays ruckeln, Datenpakete gehen verloren, Motoren laufen unruhig, WLAN-Verbindungen brechen ab oder das System fühlt sich „träge“ an. Die Lösung ist nicht zwingend ein echtes Betriebssystem (RTOS), sondern oft ein einfacher, robuster Ansatz: zeitgesteuerte Abläufe mit millis() (oder micros()) und nicht-blockierenden Zustandslogiken. Damit entsteht eine Art kooperatives Multitasking: Jede Aufgabe erledigt nur kleine Schritte, gibt sofort wieder die Kontrolle ab und wird beim nächsten Durchlauf fortgesetzt. In diesem Artikel lernen Sie, warum delay() bremst, wie Millis() funktioniert, wie Sie mehrere Aufgaben sauber takten, wie Sie typische Fallen (Overflow, Drift, Timing-Fehler) vermeiden und wie Sie aus Ihrem Sketch ein reaktionsschnelles System machen – ohne Betriebssystem, aber mit professioneller Struktur.
Warum delay() in echten Projekten fast immer schadet
delay(ms) blockiert den Programmlauf für eine feste Zeit. Währenddessen kann Ihr Code keine anderen Aufgaben ausführen. Auf einem PC würde man „warten“ und gleichzeitig andere Threads laufen lassen – auf vielen Mikrocontrollern ohne RTOS gibt es jedoch keinen Scheduler, der nebenbei andere Aufgaben ausführt. Das führt zu Problemen, sobald Sie mehr als eine Sache zuverlässig tun müssen.
- Verpasste Ereignisse: Buttons, Encoder oder Sensor-Interrupts werden zu spät verarbeitet
- Unruhige Abläufe: Displays aktualisieren ruckelig, Motorsteuerung schwankt
- Kommunikationsprobleme: UART/I2C/WLAN benötigen regelmäßige Pflege
- Fehlerdiagnose schwierig: „manchmal“ Bugs durch Timing und Blockaden
Delay ist nicht „falsch“ – nur für sehr einfache Abläufe geeignet
Für kurze Tests, Blink-Beispiele oder einfache Sequenzen ist delay() okay. Sobald jedoch Interaktion, Kommunikation oder mehrere Perioden ins Spiel kommen, lohnt sich der Umstieg auf nicht-blockierende Logik praktisch immer.
Was macht millis() eigentlich?
millis() liefert die Anzahl der Millisekunden seit dem Start des Mikrocontrollers (bzw. seit Reset). Dieser Wert steigt kontinuierlich an. Sie nutzen millis() nicht, um „zu warten“, sondern um zu prüfen, ob eine bestimmte Zeitspanne vergangen ist. Der entscheidende Unterschied: Ihr Programm läuft weiter und kann in jedem Durchlauf andere Aufgaben erledigen. Wenn die Zeit reif ist, führen Sie die nächste Aktion aus.
- Zeitbasis: „Uhr“ seit Systemstart in Millisekunden
- Nicht-blockierend: keine Wartezeit, sondern Zeitprüfung
- Skalierbar: mehrere unabhängige Aufgaben mit eigenen Timern
- Standard in Arduino: verfügbar auf vielen Boards und Cores
Eine gute Referenz für Zeitfunktionen finden Sie in der Arduino Referenz zu millis().
Das Grundmuster: „Wenn Zeit vergangen ist, tue etwas“
Das klassische Millis()-Muster besteht aus zwei Variablen: einem gespeicherten Zeitstempel (letzter Ausführungszeitpunkt) und einem Intervall. In der loop() prüfen Sie regelmäßig, ob jetzt – zuletzt größer oder gleich dem Intervall ist. Dann führen Sie die Aktion aus und setzen den Zeitstempel neu. Das System „arbeitet“ in kleinen Schritten, statt zu blockieren.
- Zeitstempel: wann lief die Aufgabe zuletzt?
- Intervall: wie oft soll sie laufen?
- Prüfung: if (now – last >= interval) …
- Update: last = now (oder last += interval für weniger Drift)
Warum „now – last“ besser ist als „now > last + interval“
Weil millis() irgendwann überläuft. Mit der Subtraktion und dem Vergleich in unsigned-Arithmetik bleibt das Muster auch beim Überlauf korrekt. Das ist ein Standardtrick in Embedded-Systemen.
Millis statt Delay: Mehrere Aufgaben parallel planen
Der große Gewinn zeigt sich, wenn Sie mehrere Aufgaben gleichzeitig brauchen: LED blinken, Sensor auslesen, Button abfragen, Display aktualisieren, Netzwerkverbindung prüfen – jeweils mit unterschiedlichen Intervallen. Mit millis() erhält jede Aufgabe ihren eigenen Timer. Da jede Aufgabe nur kurz arbeitet, fühlt sich das System wie Multitasking an, obwohl alles in einem Thread läuft.
- LED-Heartbeat: z. B. alle 500 ms umschalten
- Sensor-Sampling: z. B. alle 200 ms messen
- Display-Refresh: z. B. alle 250 ms aktualisieren
- Netzwerk-Check: z. B. alle 5 Sekunden Reconnect prüfen
- Button-Scan: z. B. bei jedem Loop-Durchlauf oder alle 5–10 ms
Kooperatives Multitasking: Jede Aufgabe muss „fair“ sein
Weil es keinen Scheduler gibt, ist Fairness Ihre Aufgabe: Jede Routine sollte schnell zurückkehren. Wenn eine Aufgabe plötzlich 200 ms blockiert (z. B. durch lange Serial-Ausgaben oder Warte-Schleifen), leidet wieder alles andere.
Delay ersetzen: Typische Praxisbeispiele
Viele Delay-Anwendungen lassen sich direkt mit millis() umsetzen. Dazu gehört nicht nur Blinken, sondern auch zeitgesteuerte Sequenzen: „Nach 2 Sekunden Motor starten“, „Nach 10 Sekunden wieder stoppen“, „Wenn Button gedrückt, 3 Sekunden lang aktiv“. Statt in einer delay-Kette zu hängen, modellieren Sie diese Abläufe über Zeitstempel oder Zustände.
- Blink ohne Blockade: LED toggeln, wenn Intervall abgelaufen
- Timeouts: statt „warte bis verbunden“ eine Maximalzeit prüfen
- Debouncing: Tasterprellen über Zeitfenster filtern
- Sequenzen: Schrittfolgen über Zustände und Zeitstempel steuern
Millis und Zustandsautomaten: Das Dream-Team für sauberen Code
Wenn Abläufe mehrstufig werden, reicht ein einzelner Timer nicht mehr aus. Dann helfen Zustandsautomaten (State Machines): Jeder Zustand beschreibt, was jetzt gilt und welche Bedingungen (Zeit, Events, Sensorwerte) zum nächsten Zustand führen. millis() liefert dabei die Zeitbedingungen. So entsteht sauberer Code, der nicht in verschachtelten if-else-Ketten und Delay-Sequenzen endet.
- Zustand definieren: z. B. IDLE, HEATING, STABILIZE, ERROR
- Entry-Zeit merken: timestamp beim Eintritt speichern
- Timeouts prüfen: now – stateStart > timeout
- Events verarbeiten: Button, Sensor, Netzwerkstatus
Warum das skalierbar ist
Ein Zustandsautomat zwingt Sie, Logik zu strukturieren: „Was darf in diesem Zustand passieren?“ Das reduziert Nebenwirkungen und macht Erweiterungen leichter, weil neue Funktionen meist nur neue Zustände oder Übergänge hinzufügen.
Typische Fallstricke: Overflow, Drift und ungenaue Intervalle
Millis() läuft nicht unendlich. Je nach Plattform überläuft der Zähler nach einer gewissen Zeit (bei 32-bit Millisekunden typischerweise nach einigen Wochen). Das ist kein Problem, wenn Sie das richtige Vergleichsmuster nutzen. Zusätzlich gibt es Drift: Wenn Sie nach jeder Ausführung last = now setzen, verschiebt sich der Rhythmus leicht, wenn der Loop-Durchlauf variiert. Für gleichmäßige Takte nutzen Sie oft last += interval (mit Schleife, falls mehrere Intervalle verpasst wurden).
- Overflow-Sicherheit: unsigned Subtraktion verwenden
- Drift reduzieren: last += interval statt last = now
- Nachholen: bei großen Verzögerungen mehrere Schritte abarbeiten (vorsichtig)
- Intervall realistisch wählen: zu kurze Intervalle überlasten die loop()
Überlauf in der Praxis: Keine Panik, wenn man korrekt vergleicht
Viele Systeme laufen wochenlang. Wenn Sie unsigned Zeitdifferenzen verwenden, bleibt die Logik auch nach dem Überlauf korrekt, weil die Arithmetik modulo arbeitet.
Prioritäten und „Task-Design“: Was wirklich oft laufen muss
Wenn Sie mehrere Aufgaben planen, ist die Frequenz entscheidend. Nicht alles muss 100-mal pro Sekunde laufen. Ein häufiger Fehler ist, zu viele Dinge zu oft zu aktualisieren (z. B. Display bei jedem Loop). Definieren Sie sinnvolle Intervalle: Buttons und Encoder eher schnell, Sensoren und Displays moderat, Netzwerkchecks eher langsam. So bleibt CPU-Zeit für das Wichtige.
- Sehr häufig: Eingaben (Buttons, Encoder), Sicherheitschecks
- Moderat: Sensoren, UI-Updates, Regelungen (je nach System)
- Selten: Status-Logs, Netzwerk-Reconnect, Cloud-Sync
- Event-basiert: wenn möglich, lieber Events statt Polling (Interrupts)
Millis und Interrupts: Echtzeit-Events ohne Chaos
Interrupts können Ereignisse präzise erfassen, millis() kann die zeitliche Verarbeitung steuern. Ein sauberes Muster ist: ISR setzt nur ein Flag oder speichert einen Zeitstempel, der Hauptcode verarbeitet das Ereignis später nicht-blockierend. So bleiben Ihre Abläufe stabil und „echtzeitnah“, ohne dass Sie Logik in Interrupts quetschen.
- ISR: Flag setzen, Zähler erhöhen, Zeitstempel sichern
- Loop: Flag auswerten und passende Aktion/State-Transition ausführen
- Timing: millis() für Debounce, Cooldowns und Timeouts nutzen
Wann millis() nicht reicht: Grenzen und sinnvolle Alternativen
Millis()-Multitasking ist kooperativ: Es funktioniert nur, wenn Ihre Aufgaben kurz bleiben. Wenn Sie harte Echtzeit brauchen (sehr präzise, jitterarm), wenn viele Aufgaben komplex werden oder wenn Netzwerk-Stacks und Sensoren stark konkurrieren, kann ein RTOS sinnvoll sein (z. B. auf ESP32). Für extrem präzises Timing nutzen Sie Timer-Interrupts oder Hardware-PWM. Für sehr zeitkritische Messungen ist micros() oder dedizierte Peripherie (Capture/Compare) oft geeigneter.
- Millis()-Stärken: einfach, robust, ideal für viele Maker-Projekte
- Schwächen: blockierende Aufgaben ruinieren Fairness, Timing-Jitter möglich
- Alternativen: Timer-Interrupts, RTOS-Tasks, Event-Queues, DMA (fortgeschritten)
ESP32-Hinweis: RTOS ist vorhanden, aber nicht zwingend nötig
Der ESP32 nutzt intern FreeRTOS. Dennoch ist millis()-basierte Struktur oft weiterhin sinnvoll, weil sie den Code übersichtlich hält. Ein RTOS ist dann hilfreich, wenn Aufgaben wirklich getrennt priorisiert oder parallelisiert werden müssen.
Best Practices: So wird Ihr Code „multitaskingfähig“ und wartbar
Der Umstieg auf millis() ist mehr als eine Technik – es ist ein Stil. Sie gestalten Aufgaben klein, definieren Intervalle, vermeiden Blockaden und strukturieren Abläufe über Zustände. Damit werden Projekte nicht nur reaktionsschnell, sondern auch leichter zu debuggen und zu erweitern.
- Keine blockierenden Wartezeiten: delay() und wartende Schleifen vermeiden
- Task-Intervalle planen: jede Aufgabe mit sinnvoller Frequenz
- Kleine Funktionen: Aufgaben in kurze, wiederholbare Schritte zerlegen
- Zustandsautomaten nutzen: für mehrstufige Abläufe und Timeouts
- Debugging bewusst: Serial-Ausgaben dosieren, Timing nicht zerstören
- Overflow-sicher: Zeitdifferenzen mit unsigned-Arithmetik vergleichen
LSI-Keywords für SEO und Weiterrecherche
Wenn Sie tiefer recherchieren, helfen Begriffe wie „nicht-blockierender Code“, „cooperative multitasking“, „Arduino Blink Without Delay“, „State Machine“, „Timer“, „event-driven programming“, „Scheduler“ und „loop timing“.
Weiterführende Ressourcen
- Arduino Referenz: millis()
- Arduino Beispiel: Blink Without Delay
- Ereignisgesteuerte Programmierung: Grundidee hinter reaktivem Code
- Espressif Dokumentation: Timer, Tasks und Systemverhalten auf dem ESP32
IoT-PCB-Design, Mikrocontroller-Programmierung & Firmware-Entwicklung
PCB Design • Arduino • Embedded Systems • Firmware
Ich biete professionelle Entwicklung von IoT-Hardware, einschließlich PCB-Design, Arduino- und Mikrocontroller-Programmierung sowie Firmware-Entwicklung. Die Lösungen werden zuverlässig, effizient und anwendungsorientiert umgesetzt – von der Konzeptphase bis zum funktionsfähigen Prototyp.
Diese Dienstleistung richtet sich an Unternehmen, Start-ups, Entwickler und Produktteams, die maßgeschneiderte Embedded- und IoT-Lösungen benötigen. Finden Sie mich auf Fiverr.
Leistungsumfang:
-
IoT-PCB-Design & Schaltplanerstellung
-
Leiterplattenlayout (mehrlagig, produktionstauglich)
-
Arduino- & Mikrocontroller-Programmierung (z. B. ESP32, STM32, ATmega)
-
Firmware-Entwicklung für Embedded Systems
-
Sensor- & Aktor-Integration
-
Kommunikation: Wi-Fi, Bluetooth, MQTT, I²C, SPI, UART
-
Optimierung für Leistung, Stabilität & Energieeffizienz
Lieferumfang:
-
Schaltpläne & PCB-Layouts
-
Gerber- & Produktionsdaten
-
Quellcode & Firmware
-
Dokumentation & Support zur Integration
Arbeitsweise:Strukturiert • Zuverlässig • Hardware-nah • Produktorientiert
CTA:
Planen Sie ein IoT- oder Embedded-System-Projekt?
Kontaktieren Sie mich gerne für eine technische Abstimmung oder ein unverbindliches Angebot. Finden Sie mich auf Fiverr.

