Multitasking am Leonardo: Zeitsteuerung ohne delay()

Multitasking am Leonardo: Zeitsteuerung ohne delay() ist eines der wichtigsten Themen, sobald Projekte über ein simples Blinklicht hinausgehen. Der Arduino Leonardo (ATmega32U4) kann zwar kein „echtes“ Multitasking wie ein Betriebssystem, aber er kann mehrere Aufgaben nahezu gleichzeitig abarbeiten, wenn Sie Ihre Abläufe zeitbasiert und nicht blockierend strukturieren. Genau hier ist delay() der typische Stolperstein: Während delay() wartet, passiert sonst nichts. Taster werden zu spät gelesen, Sensorwerte „frieren ein“, LED-Animationen ruckeln und USB-HID-Funktionen (Keyboard/Mouse) reagieren unzuverlässig. Mit einer sauberen Zeitsteuerung auf Basis von millis() oder micros(), kleinen Zustandsautomaten (State Machines) und einem kooperativen Scheduling-Ansatz erreichen Sie hingegen eine reaktionsschnelle Firmware. In diesem Artikel lernen Sie praxisnah, wie Sie am Leonardo mehrere Aufgaben parallel laufen lassen: Eingaben entprellen, Sensoren periodisch abfragen, Displays aktualisieren, serielle Kommunikation bedienen und gleichzeitig USB-Events stabil halten. Als Grundlage eignen sich die Arduino-Referenzen zu millis() und micros() sowie das offizielle Beispiel Blink Without Delay.

Warum delay() Multitasking verhindert

delay() blockiert die Ausführung des Sketches für eine feste Zeit. Währenddessen werden keine neuen Eingaben gelesen, keine Ausgaben aktualisiert und keine Kommunikationspuffer gepflegt. Das ist bei einfachen Demo-Sketches unkritisch, aber in der Praxis führt es zu spürbaren Nebenwirkungen:

  • Träge Bedienung: Tasterdruck wird erst nach Ende der Wartezeit erkannt.
  • Verpasste Ereignisse: Kurze Impulse (Encoder, Sensortrigger) gehen verloren.
  • Unruhige Ausgabe: Display- oder LED-Updates erscheinen ruckelig, weil sie nicht in konstanter Frequenz laufen.
  • Kommunikationsprobleme: Serielle Daten, I2C-Events oder USB-HID-Interaktionen können ins Stocken geraten.

Die Lösung ist nicht „weniger delay()“, sondern ein anderer Denkansatz: Sie planen Aufgaben in kleinen Schritten, die jederzeit unterbrechbar sind und sich selbst über Zeitstempel steuern.

Grundprinzip der nicht blockierenden Zeitsteuerung

Im Kern ersetzen Sie „Warten“ durch „Prüfen, ob Zeit vergangen ist“. Sie speichern einen Zeitpunkt (Start) und vergleichen ihn in jeder loop()-Iteration mit der aktuellen Zeit. Das Muster basiert auf der Differenz zweier Zeitstempel und ist bewusst so gestaltet, dass es auch beim Überlauf (Overflow) von millis() funktioniert.

Die zentrale Zeitdifferenz-Formel

Das Grundprinzip lässt sich als Differenz formulieren:

Δt = tnow tlast

Eine Aktion wird ausgeführt, wenn Δt ≥ Intervall. Mit diesem Muster bleiben Ihre Aufgaben reaktiv, weil die loop() ständig durchläuft.

millis() vs. micros(): Welche Zeitbasis ist die richtige?

Für die meisten Multitasking-Aufgaben am Arduino Leonardo ist millis() ideal: Es liefert Millisekunden seit Programmstart und ist ausreichend präzise für Taster, Sensorpolling, Display-Refresh, Status-LEDs oder USB-Makros. micros() ist sinnvoll, wenn Sie sehr kurze Zeiten oder hohe Frequenzen messen bzw. steuern wollen, etwa Pulsbreiten, schnelle Encoder-Auswertung oder Timing bei Protokollen.

  • millis(): Grob, robust, leicht zu handhaben. Referenz: millis()
  • micros(): Feiner, für kurze Intervalle. Referenz: micros()

In der Praxis kombinieren viele Projekte beides: millis() für „grobe“ Aufgaben und micros() für wenige, besonders zeitkritische Messungen.

Das bewährte Muster: Blink Without Delay als Blaupause

Das offizielle Arduino-Beispiel „Blink Without Delay“ ist der Einstieg in nicht blockierende Abläufe. Es zeigt, wie eine LED periodisch geschaltet wird, ohne die loop() anzuhalten. Dieses Muster lässt sich direkt auf „mehrere LEDs“, „Sensorabfragen“, „Seriell-Ausgaben“ oder „HID-Events“ erweitern. Das Beispiel finden Sie hier: Blink Without Delay.

Kooperatives Multitasking: Viele kleine Aufgaben statt einer langen

„Multitasking“ auf dem Leonardo funktioniert am besten kooperativ: Jede Aufgabe bekommt in loop() regelmäßig CPU-Zeit, aber nur in kurzen Portionen. Typische Aufgaben (Tasks) sind:

  • Input-Task: Taster lesen, entprellen, Events erzeugen
  • Sensor-Task: Analoge Werte abfragen, Mittelwert bilden
  • Output-Task: LEDs/Display aktualisieren
  • Kommunikation: Serial, I2C, SPI bedienen
  • USB-HID-Task: Keyboard/Mouse Aktionen steuern (falls genutzt)

Wichtig ist die Regel: Jeder Task muss schnell zurückkehren. Wenn eine Aufgabe „lange“ dauert, wird sie in Schritte zerlegt, die über Zustände gesteuert werden.

Zustandsautomaten: Komplexe Abläufe ohne Blockade

Ein Zustandsautomat (State Machine) beschreibt einen Ablauf als Folge von Zuständen, z. B. „Warten“, „Starten“, „Ausführen“, „Fertig“. Statt in einer Funktion zu warten, wechseln Sie den Zustand und speichern Zeitstempel. So lässt sich selbst eine mehrstufige Sequenz (z. B. Display-Intro, danach Messmodus, danach Alarmmodus) ohne delay() abbilden.

Typische Zustandsmuster in Leonardo-Projekten

  • Debounce-State: Taster wird erst nach stabiler Zeit als „gedrückt“ akzeptiert.
  • Animation-State: LED-Animation läuft in Frames, jeder Frame ist ein kleiner Schritt.
  • HID-Safety-State: USB-Tastatur/Maus erst nach „Arming“ aktivieren, um Fehlverhalten zu vermeiden.

Entprellen ohne delay(): Saubere Taster-Events

Taster prellen mechanisch. Mit delay() wird oft „einfach“ nach dem Lesen kurz gewartet. Besser ist eine Zeitprüfung: Sie übernehmen einen neuen Tastenzustand erst, wenn er über ein kleines Intervall stabil bleibt (z. B. 20–50 ms). Damit bleibt die loop() frei, und Sie erhalten zuverlässige Events.

  • Stabilitätsfenster: Nur wenn der gelesene Zustand länger als X ms gleich bleibt, gilt er als gültig.
  • Edge-Events: Erzeugen Sie „Pressed“ nur bei Flanke (von 0 auf 1), nicht solange der Taster gehalten wird.
  • Repeat optional: Für langes Halten kann ein separater Repeat-Timer genutzt werden.

Mehrere Timer parallel: Das „Task-Tabelle“-Prinzip

Ein skalierbarer Ansatz ist, für jede Aufgabe einen eigenen Zeitstempel zu führen. Viele Projekte nutzen eine kleine „Tabelle“ im Kopf: Task A alle 10 ms, Task B alle 50 ms, Task C alle 250 ms usw. Damit definieren Sie bewusst Frequenzen:

  • Inputs: 5–10 ms (schnell genug für Reaktionsgefühl)
  • Analoge Sensoren: 20–100 ms (abhängig von Rauschen und Dynamik)
  • Display/OLED: 50–200 ms (je nach Inhalt)
  • Status-LEDs: 250–1000 ms

Der Vorteil: Sie entkoppeln Aufgaben. Ein langsamer Display-Refresh bremst nicht die Tasterabfrage, und ein schneller Input-Loop blockiert nicht die Kommunikation.

Überlauf (Rollover) von millis(): Keine Angst vor 49 Tagen

millis() läuft nicht „unendlich“, sondern überläuft nach einer gewissen Zeit (bei 32-bit etwa nach knapp 50 Tagen). Das ist kein Problem, wenn Sie Zeitdifferenzen mit unsigned-Arithmetik bilden, also nach dem Schema „now – last“. Dieses Muster bleibt korrekt, weil die Subtraktion modulo 232 funktioniert.

Mathematisch ist das eine Rechnung im Modulo-Raum:

t t   ( mod   232 )

Praktisch heißt das: Verwenden Sie konsequent Differenzen und vermeiden Sie direkte Vergleiche wie „if (now > last + interval)“ mit gemischten Datentypen.

Multitasking und Kommunikation: Serial, I2C und SPI ohne Stau

Viele Leonardo-Projekte hängen an serieller Kommunikation (Debugging), I2C-Sensoren oder SPI-Displays. Auch hier gilt: Vermeiden Sie lange Blockaden.

  • Serial: Schreiben Sie nicht „endlos“ in einem Rutsch. Besser: Nur bei Bedarf ausgeben und Ausgabeintervalle definieren.
  • I2C: Sensorabfragen takten (z. B. alle 50 ms) statt ständig zu poll’en. So bleibt die loop() gleichmäßig.
  • SPI: Display-Updates in sinnvolle Frequenzen packen; große Vollbild-Refreshs können spürbar Zeit kosten.

Die Zeitsteuerung sorgt dafür, dass Kommunikationsaufgaben regelmäßig und planbar laufen. Dadurch wirken Systeme „stabil“, selbst wenn einzelne Operationen kurz dauern.

USB-HID am Leonardo: Warum nicht blockierende Logik besonders wichtig ist

Der Leonardo ist beliebt für Keyboard- und Mouse-Projekte, weil der ATmega32U4 USB nativ unterstützt. Gerade hier ist delay() riskant: Eingaben werden unter Umständen zu spät oder zu lange gesendet, und Fehler im Ablauf können den Upload erschweren. Verwenden Sie daher für HID-Funktionen grundsätzlich Zustände und Timer: eine Aktion auslösen, danach wieder in einen sicheren Ruhezustand gehen. Offizielle Referenzen: Keyboard-Library und Mouse-Library.

  • Einmal-Events: Shortcuts nur bei Flanke senden, nicht „solange Taste gedrückt“.
  • Cool-down: Nach einem HID-Event kurze Sperrzeit (z. B. 100–300 ms), um Wiederholungen zu verhindern.
  • Arming: HID erst aktivieren, wenn ein definierter Taster gehalten oder ein Modus gewählt wurde.

Typische Multitasking-Szenarien am Leonardo und wie Sie sie strukturieren

Damit die Zeitsteuerung greifbar wird, helfen typische Anwendungsfälle, die am Leonardo häufig zusammenkommen:

  • Makro-Controller: Tastermatrix lesen (10 ms), Status-LEDs aktualisieren (50 ms), HID-Ausgabe bei Event (sofort, aber nicht blockierend).
  • Sim-Racing-Buttonbox: Inputs schnell erfassen (5–10 ms), Encoder auswerten (1–5 ms), Display/OLED (100 ms), Telemetrie seriell (20–100 ms).
  • Sensor-Panel: I2C-Sensoren zyklisch (50–200 ms), Glättung per Filter, Anzeige in konstantem Rhythmus (100–250 ms).

Der gemeinsame Nenner: Jede Aufgabe hat ein eigenes Intervall und läuft in kurzen Schritten. So bleibt das System reaktionsschnell, auch wenn mehrere Funktionen gleichzeitig aktiv sind.

Best Practices: Stabil, wartbar, erweiterbar

  • Ein Intervall pro Aufgabe: Klar definieren, nicht „irgendwo“ warten.
  • Keine langen Schleifen in Tasks: Wenn eine Aufgabe länger wirkt, in Zustände aufteilen.
  • Zeiten dokumentieren: Warum läuft Task X alle 20 ms? Notieren Sie es im Code-Kommentar.
  • Datentypen konsistent: Für millis()-Zeitstempel typischerweise unsigned long verwenden.
  • Messwerte puffern: Sensorwerte zwischenspeichern und Ausgaben (Display/Serial) aus dem Puffer bedienen.

Wenn Sie das konsequent umsetzen, entsteht ein Leonardo-Projekt, das sich „professionell“ anfühlt: Tasten reagieren sofort, Anzeigen aktualisieren sauber, und Erweiterungen (z. B. zusätzlicher Sensor) lassen sich hinzufügen, ohne alles umzubauen.

Outbound-Links: Verlässliche Grundlagen für Zeitsteuerung ohne delay()

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