Multitasking ohne delay(): Zeitsteuerung mit millis()

Multitasking ohne delay() ist einer der wichtigsten Schritte, wenn du Arduino-Projekte bauen willst, die zuverlässig reagieren und mehrere Aufgaben „gleichzeitig“ erledigen. Viele Einsteiger beginnen mit delay(), weil es einfach ist: LED an, warten, LED aus, warten. Das funktioniert für erste Experimente, aber sobald du Taster abfragen, Sensorwerte regelmäßig lesen, eine Status-LED blinken lassen und nebenbei vielleicht noch ein Display aktualisieren möchtest, wird delay() zum Bremsklotz. Der Grund: delay() blockiert die gesamte Programmausführung. Während der Arduino wartet, kann er nichts anderes tun – keine Eingänge lesen, keine Zustände aktualisieren, keine Kommunikation verarbeiten. Die Lösung heißt Zeitsteuerung mit millis(). Damit misst du Zeit, ohne das Programm anzuhalten. Du planst Aktionen in Intervallen, prüfst Zeitdifferenzen und lässt loop() schnell weiterlaufen. So entsteht echtes Maker-„Multitasking“: nicht durch parallele Threads, sondern durch saubere, nicht-blockierende Abläufe. In diesem Artikel lernst du das millis()-Prinzip von Grund auf, typische Muster (Blink ohne delay, mehrere Timer, periodische Sensorabfrage), wichtige Datentypen, häufige Fehler und Strategien, wie du größere Projekte sauber strukturierst – damit dein Arduino jederzeit reaktionsfähig bleibt.

Warum delay() in echten Projekten zum Problem wird

delay() ist nicht „schlecht“, aber es ist blockierend. Das bedeutet: Der Arduino führt während delay() keine weiteren Zeilen Code aus. In der Praxis führt das zu den typischen Frustmomenten:

  • Taster reagieren verzögert oder gar nicht, weil sie nur selten abgefragt werden.
  • Sensorwerte wirken „träge“, weil Messungen zu selten stattfinden.
  • Mehrere Aufgaben lassen sich nicht sinnvoll kombinieren (z. B. Blinken + Motorsteuerung + Messung).
  • Debugging wird schwieriger, weil der zeitliche Ablauf unübersichtlich wird.

Wenn du dagegen loop() schnell und häufig durchlaufen lässt, kann dein Arduino jederzeit reagieren. Genau hier setzt millis() an.

Was ist millis() und was liefert die Funktion zurück?

millis() ist eine Arduino-Funktion, die die Zeit seit dem Start des Boards in Millisekunden liefert. Wichtig: millis() misst nicht „Uhrzeit“, sondern eine laufende Zeitbasis ab Reset/Start. Du nutzt diese Zeitbasis, um zu prüfen, ob seit einem bestimmten Zeitpunkt ein Intervall vergangen ist.

  • millis() gibt eine Zahl zurück, die ständig steigt.
  • Der Wert ist ideal, um Differenzen zu berechnen: „jetzt minus vorher“.
  • Für Zeitsteuerung speicherst du Zeitstempel und vergleichst sie in loop().

Die offizielle Referenz zu millis() findest du hier: Arduino Referenz: millis().

Das Grundprinzip: Zeitdifferenz statt Warten

Der zentrale Gedanke bei „Multitasking ohne delay()“ ist simpel: Du wartest nicht aktiv, sondern du prüfst, ob genug Zeit vergangen ist. Dadurch bleibt dein Sketch reaktiv.

Das Standardmuster in Worten

  • Merke dir, wann eine Aktion zuletzt stattgefunden hat (Zeitstempel).
  • Hole die aktuelle Zeit mit millis().
  • Wenn die Differenz größer/gleich dem Intervall ist, führe die Aktion aus.
  • Aktualisiere den Zeitstempel.

Warum dieses Muster so gut funktioniert

Weil loop() weiterlaufen kann. In jeder Runde kannst du zusätzlich Taster abfragen, Sensoren auslesen, Zustände aktualisieren und Ausgänge steuern – ohne dass ein einzelner delay()-Block alles einfriert.

Der richtige Datentyp: Warum unsigned long Pflicht ist

Für millis()-Zeitstempel solltest du auf dem Arduino Uno praktisch immer unsigned long verwenden. Der Grund ist der Wertebereich: millis() kann große Werte annehmen, und kleinere Datentypen laufen früher über.

  • unsigned long ist der Standard für Zeitstempel und Intervalle in Millisekunden.
  • int ist für Zeitmessung ungeeignet und führt zu Bugs bei größeren Werten.
  • Rechne mit Zeitdifferenzen (jetzt – vorher), das ist robust – auch bei Überlauf.

Überlauf: Warum millis() irgendwann „zurückspringt“ und trotzdem funktioniert

Ein häufiges Missverständnis ist, dass millis() „nach einer Weile kaputtgeht“. Tatsächlich läuft der Zähler irgendwann über und beginnt wieder bei 0. Das ist normales Verhalten eines begrenzten Zählers. Entscheidend ist: Wenn du Zeitsteuerung mit Differenzen aufbaust, funktioniert dein Programm auch dann weiterhin zuverlässig.

Die wichtigste Praxisregel

  • Vergleiche nicht „ist millis() größer als ein fester Wert?“, sondern nutze immer: (millis() – lastTime) >= interval.

Dieses Muster ist in Arduino-Projekten so verbreitet, weil es Überläufe sauber abfedert.

Blink ohne delay(): Das klassische Einstiegsbeispiel mit Mehrwert

Das bekannte „Blink“-Projekt wird mit millis() plötzlich viel interessanter: Du kannst eine LED blinken lassen und gleichzeitig andere Dinge tun – zum Beispiel einen Taster lesen oder Sensorwerte ausgeben.

Das offizielle Blink-Beispiel ist als Referenz nützlich, auch wenn es dort klassisch mit delay gezeigt wird: Arduino Built-in Example: Blink. Für echtes Multitasking ist jedoch die millis-Variante der entscheidende nächste Schritt.

Mehrere Aufgaben gleichzeitig: Drei Timer in einer loop()

In realen Projekten brauchst du selten nur „eine Sache“. Typische Aufgaben laufen in unterschiedlichen Takten:

  • Status-LED: alle 500 ms
  • Sensor lesen: alle 200 ms
  • Display aktualisieren: alle 1000 ms
  • Button-Abfrage: so oft wie möglich (jede loop-Runde)

Mit millis() kannst du für jede Aufgabe einen eigenen Zeitstempel und ein eigenes Intervall definieren. So entsteht ein kooperatives Multitasking: Alle Aufgaben teilen sich die CPU, aber keine blockiert die anderen.

Wichtiger Gedanke: Buttons ohne Intervall

Taster und andere schnelle Eingaben prüfst du am besten in jeder loop-Runde, ohne Zeitsteuerung. So bleibt die Reaktion unmittelbar. Zeitsteuerung ist vor allem für wiederkehrende Aktionen sinnvoll, die nicht bei jedem Durchlauf passieren müssen.

Entprellen und Reaktionsfähigkeit: millis() hilft auch bei Tastern

Taster „prellen“ mechanisch: Beim Drücken entstehen kurze, schnelle Signalwechsel. Viele Einsteiger lösen das mit delay(50), was aber wieder blockiert. Mit millis() kannst du Entprellen nicht-blockierend umsetzen: Du akzeptierst eine Änderung nur, wenn seit der letzten gültigen Änderung genug Zeit vergangen ist.

  • Letzte Zustandsänderung als Zeitstempel speichern
  • Neuen Zustand lesen
  • Nur übernehmen, wenn Zeitdifferenz > Entprellzeit

Das Ergebnis: Der Arduino bleibt reaktionsfähig, und dein Button-Input wird stabil.

Millis-Logik sauber strukturieren: Funktionen statt „Timer-Spaghetti“

Ein Risiko bei mehreren Timern ist unübersichtlicher Code: viele lastTime-Variablen, viele if-Blöcke, alles in loop. Das lässt sich gut strukturieren, indem du Aufgaben in Funktionen auslagerst.

Bewährtes Muster: loop als Ablaufplan

  • readInputs()
  • updateTimersAndTasks()
  • updateOutputs()

Innerhalb von updateTimersAndTasks() prüfst du dann die einzelnen Intervalle. So bleibt loop kurz und verständlich.

Typische Fehler bei millis() und wie du sie vermeidest

Millis ist einfach, aber kleine Details entscheiden darüber, ob dein Projekt stabil läuft. Diese Fehler tauchen besonders häufig auf:

  • Falscher Datentyp: int statt unsigned long für Zeitstempel oder Intervalle
  • Falscher Vergleich: direkte Vergleiche mit festen Zeitwerten statt Differenzrechnung
  • Zeitstempel falsch gesetzt: lastTime wird nicht aktualisiert oder an falscher Stelle überschrieben
  • Zu viele Serial-Ausgaben: Debugging blockiert indirekt durch langsame serielle Ausgabe
  • Zu große Aufgaben im Timer: Wenn eine Aktion lange dauert, hilft auch millis nicht

Gerade Serial.print kann die Loop merklich verlangsamen. Wenn du debuggen musst, gib in Intervallen aus oder schalte Debug-Ausgaben über ein Flag. Grundlagen zur seriellen Schnittstelle findest du in der Referenz: Serial.

Millis und „echtes“ Multitasking: Was möglich ist – und was nicht

Millis macht aus dem Arduino keinen Multithreading-Computer. Stattdessen nutzt du kooperatives Scheduling: Jede Aufgabe bekommt kurze Rechenzeit und gibt sie sofort wieder frei. Das funktioniert hervorragend, solange du dich an ein Prinzip hältst: Jede Aufgabe sollte schnell fertig werden.

Gute Kandidaten für millis-Tasks

  • LED blinken, Statussignale
  • Sensorwerte lesen und glätten
  • Display in Intervallen aktualisieren
  • Regelungen mit festen Takten
  • Protokollausgaben in sinnvollen Abständen

Schwierige Kandidaten

  • Lange blockierende Bibliotheksfunktionen (z. B. langsame Displays, schlechte Treiber)
  • Aufgaben, die aktiv warten (while-Schleifen ohne Exit)
  • Aufwändige Berechnungen in jeder Loop-Runde

Wenn du solche blockierenden Elemente hast, musst du sie entweder reduzieren, entkoppeln oder in kleinere Schritte zerlegen.

Praktische Intervallwahl: Wie oft ist „oft genug“?

Viele Maker wählen Intervalle zu kurz, weil sie denken, „mehr ist besser“. In Wirklichkeit solltest du Intervalle nach Bedarf wählen:

  • Status-LED: 250–1000 ms, je nach Signal
  • Sensoren: 50–500 ms, je nach Sensor und Anwendung
  • Displays: 200–2000 ms, je nach Inhalt und Performance
  • Debug-Ausgaben: eher selten, z. B. alle 500–2000 ms

Das Ziel ist ein reaktives System ohne unnötige Last. Wenn du Interaktion brauchst, müssen Eingaben häufig geprüft werden – aber nicht jedes Subsystem muss im Millisekunden-Takt laufen.

Millis im Zusammenspiel mit Zuständen: Der nächste Schritt für größere Projekte

Wenn Projekte größer werden, kombinierst du millis fast immer mit einer Zustandslogik: Standby, Aktiv, Fehler, Menü. In jedem Zustand laufen andere Timer oder Intervalle. Das macht Projekte übersichtlich, weil du nicht „alles immer“ prüfst, sondern nur das, was im aktuellen Modus relevant ist.

  • Standby: nur Taster und Status-LED
  • Aktiv: Sensor-Timer, Display-Timer, Regelungs-Timer
  • Fehler: Blinkmuster, sichere Abschaltung, reduzierte Aktivität

Mit dieser Struktur bleibt dein Code auch bei vielen Komponenten nachvollziehbar und wartbar.

Mini-Checkliste: Multitasking ohne delay() erfolgreich umsetzen

  • delay() in interaktiven Projekten vermeiden und durch millis-Intervalle ersetzen.
  • Zeitstempel und Intervalle als unsigned long speichern.
  • Immer mit Zeitdifferenzen arbeiten: (millis() – lastTime) >= interval.
  • Mehrere Aufgaben über separate Timer organisieren, nicht über verschachtelte delays.
  • Aufgaben kurz halten und blockierende while-Schleifen vermeiden.
  • Serial-Ausgaben dosieren und in Intervallen ausgeben.
  • Komplexe Projekte über Funktionen und Zustände strukturieren.

Weiterführende Referenzen

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.

 

Related Articles