February 8, 2026

State Machines (Zustandsautomaten) für sauberen Code

State Machines – auf Deutsch oft Zustandsautomaten genannt – sind eine der wirksamsten Methoden, um Mikrocontroller- und IoT-Projekte langfristig stabil, verständlich und erweiterbar zu halten. Viele Programme beginnen harmlos: Eine LED blinkt, ein Button schaltet ein Relais, ein Sensor sendet Werte. Mit jedem neuen Feature wächst aber die Komplexität: Es gibt mehrere Betriebsmodi, Zeitfenster, Sonderfälle, Fehlerzustände, manuelle Übersteuerungen, WLAN-Verbindungslogik, OTA-Updates oder Sleep-Zyklen. Wer das alles „einfach in die loop()“ packt, landet schnell bei verschachtelten if-else-Ketten, schwer nachvollziehbaren Flags und Bugs, die nur „manchmal“ auftreten. Genau hier schaffen Zustandsautomaten Ordnung. Eine State Machine modelliert das Verhalten eines Systems als klar definierte Zustände (z. B. STARTUP, CONNECTING, RUNNING, ERROR, SLEEP) und Übergänge (z. B. „WLAN verbunden“, „Timeout“, „Sensorfehler“, „Button gedrückt“). Der Code wird dadurch nicht nur lesbarer, sondern auch testbarer und robuster. In diesem Artikel lernen Sie, wie State Machines funktionieren, wann sie sich lohnen, welche Typen es gibt, wie Sie sie im Arduino-/ESP32-Alltag umsetzen, wie Timer und Interrupts sauber integriert werden und welche Muster Ihnen helfen, „spaghettifreien“ Code zu schreiben, der auch nach Monaten noch verständlich ist.

Table of Contents

Warum Mikrocontroller-Code ohne Zustandsautomaten schnell „unsauber“ wird

In Mikrocontroller-Projekten laufen viele Dinge gleichzeitig: Eingaben (Buttons, Sensor-Interrupts), Ausgaben (LEDs, Motoren, Displays), Kommunikation (WLAN, BLE, MQTT), Zeitsteuerung (Delays, Timer) und Fehlerbehandlung. In einer linearen Programmlogik konkurrieren diese Aufgaben, und man beginnt, mit Flags und verzweigten Bedingungen zu arbeiten. Das Ergebnis ist oft ein System, das schwer zu erweitern ist, weil jede Änderung an einer Stelle unbeabsichtigt eine andere Stelle beeinflusst.

  • Verschachtelte Logik: viele if-else-Blöcke und Sonderfälle
  • „Magic Flags“: globale Variablen, die überall gesetzt werden
  • Timing-Probleme: delay() blockiert, Ereignisse werden verpasst
  • Fehlerzustände: keine klare Strategie für „was passiert bei Ausfall?“

Typisches Symptom: „Es funktioniert, aber ich traue mich nicht, etwas zu ändern“

Wenn Änderungen Angst machen, ist das ein Signal, dass Struktur fehlt. Zustandsautomaten ersetzen „Angst vor Nebenwirkungen“ durch ein klares Modell: Was darf in welchem Zustand passieren, und was nicht?

Grundidee: Zustand + Ereignis = definierter Übergang

Ein Zustandsautomat beschreibt ein System als Menge von Zuständen und Übergängen. Ein Zustand repräsentiert eine „Betriebsart“ mit klaren Regeln. Ein Übergang wird durch ein Ereignis ausgelöst: Zeitablauf, Buttondruck, Sensorwert, Netzwerkstatus, Fehlercode. In der Praxis denken Sie weniger in „was mache ich als Nächstes“, sondern in „wo bin ich gerade und was darf jetzt passieren“.

  • Zustand: definierter Modus (z. B. CONNECTING)
  • Ereignis: Auslöser (z. B. „WLAN verbunden“)
  • Übergang: Wechsel in neuen Zustand (z. B. RUNNING)
  • Aktion: was beim Wechsel oder im Zustand ausgeführt wird

Als allgemeine Grundlage eignet sich die Einordnung zu endlichen Automaten.

Welche Arten von State Machines es gibt

Im Maker-Alltag begegnen Ihnen vor allem endliche Zustandsautomaten (Finite State Machines, FSM). Je nach Komplexität gibt es Varianten: flache FSM (einfach), hierarchische Zustandsautomaten (HSM) und Zustandsautomaten mit getrennten Zustands- und Ereignis-Handling-Tabellen. Für viele Projekte reicht eine flache FSM völlig aus. Sobald Sie jedoch viele Modi haben, lohnt sich eine hierarchische Struktur (z. B. „NETZWERK“ als Oberzustand mit Unterzuständen).

  • Flache FSM: wenige Zustände, einfache Übergänge
  • Hierarchische State Machine: Zustände in Ebenen, reduziert Wiederholung
  • Event-getrieben: Ereignis-Queue, klare Trennung zwischen Input und Logik

Wann eine hierarchische Struktur sinnvoll wird

Wenn mehrere Zustände sehr ähnliche Logik teilen (z. B. RUNNING und RUNNING_NO_WIFI) oder wenn Fehlerzustände überall gleich behandelt werden sollen, kann eine Hierarchie doppelten Code vermeiden und die Logik stabiler machen.

State Machines vs. „Switch-Case in loop()“: Wo liegt der Unterschied?

Viele Einsteiger setzen bereits eine Art Zustandslogik um, ohne sie so zu nennen: ein switch(state) in der loop() und je Zustand ein Codeblock. Das ist ein guter Start. Der entscheidende Unterschied zu einer „sauberen“ State Machine liegt in der Disziplin: klare Zustandsdefinitionen, kontrollierte Übergänge, getrennte Zustandsaktionen (Entry/Exit/Do) und ein sauberes Ereigniskonzept. Ein switch-case wird dann wirklich mächtig, wenn Sie ihn nicht als „Monolith“ verwenden, sondern als Struktur.

  • Einfacher Switch: schnell gebaut, aber oft zu groß
  • Saubere FSM: kleine Zustandsfunktionen, definierte Events, klare Übergänge
  • Wartbarkeit: Zustände lassen sich unabhängig testen und erweitern

Das wichtigste Prinzip: Keine blockierenden Delays

Zustandsautomaten funktionieren am besten, wenn Ihr Programm nicht blockiert. Das bedeutet: keine langen delay()-Aufrufe, keine wartenden Schleifen, keine „warte bis verbunden“-Dauerschleifen. Stattdessen arbeiten Sie mit Zeitstempeln und prüfen in jedem loop-Durchlauf, ob Bedingungen erfüllt sind. So bleibt das System reaktionsfähig, und Zustandsübergänge können jederzeit stattfinden.

  • Nicht-blockierend: millis()-basierte Timer statt delay()
  • Reaktiv: Ereignisse können jederzeit verarbeitet werden
  • Stabil: weniger Timing-Bugs, bessere Integration von Interrupts

Der praktische Vorteil: Ihr Mikrocontroller wirkt „multitaskingfähig“

Auch ohne echtes Multithreading fühlt sich ein nicht-blockierendes System so an, als würde es mehrere Dinge parallel erledigen. Genau das erwarten Nutzer im Smart Home: Button reagieren, Display aktualisieren, WLAN reconnecten – ohne Ruckeln.

Zustandsmodell aufbauen: Ein Vorgehen, das in der Praxis funktioniert

Der häufigste Fehler ist, zu früh Code zu schreiben. Ein Zustandsautomat wird stark, wenn Sie zuerst das Verhalten modellieren. Skizzieren Sie: Welche Zustände gibt es? Welche Ereignisse? Welche Übergänge? Welche Timeouts? Welche Fehler? Danach wird die Implementierung erstaunlich geradlinig.

  • Use Cases sammeln: normaler Betrieb, Setup, Fehler, Wartung
  • Zustände definieren: klare Namen, klare Verantwortung
  • Events definieren: Button, Timer, Sensor, Netzwerk, System
  • Übergänge zeichnen: „Wenn X passiert, dann nach Y“
  • Timeouts planen: z. B. CONNECTING darf max. 10 Sekunden dauern

Beispielhafte Zustände in IoT-Projekten

  • STARTUP: Initialisierung von Pins, Speicher, Sensoren
  • CONFIG_MODE: Access-Point oder Setup-Portal aktiv
  • CONNECTING: WLAN verbinden, ggf. MQTT verbinden
  • RUNNING: normaler Betrieb (Messen, Schalten, UI)
  • ERROR: definierte Fehlerstrategie (Retry, Fallback, Safe Mode)
  • SLEEP: Deep Sleep, Wake-up-Logik

Entry/Do/Exit: Die drei Bausteine für saubere Zustandslogik

Ein bewährtes Muster ist die Trennung in Entry/Do/Exit-Aktionen:

  • Entry: wird einmal beim Eintritt in den Zustand ausgeführt (z. B. Timer starten)
  • Do: wird während des Zustands regelmäßig ausgeführt (z. B. prüfen, ob Event da ist)
  • Exit: wird einmal beim Verlassen ausgeführt (z. B. Ressourcen freigeben)

Diese Struktur verhindert, dass Sie Initialisierungscode in jedem loop-Durchlauf erneut ausführen. Gleichzeitig macht sie Übergänge nachvollziehbar: Zustandswechsel sind klar definierte Punkte, an denen Sie gezielt Aktionen ausführen.

Praxisregel: Entry ist der Ort für „Startbedingungen“

Wenn ein Zustand eine Aktion starten soll (z. B. LED-Blinkmuster, Verbindungsversuch, Sensor-Conversion), gehört das in Entry. Das Looping und Prüfen gehört in Do. Das Aufräumen gehört in Exit.

Ereignisse richtig handhaben: Flags, Queues und Entkopplung

In kleinen Projekten reichen Flags: Ein Interrupt setzt ein Flag, die loop() liest es aus und verarbeitet es. Sobald mehr Ereignisse zusammenkommen, ist eine Event-Queue sinnvoll: Ereignisse werden gesammelt und nacheinander verarbeitet. Dadurch werden schnelle Ereignisse nicht „überschrieben“, und Sie behalten Reihenfolge und Nachvollziehbarkeit.

  • Flag-basiert: einfach, gut für seltene Events
  • Queue-basiert: robust bei vielen oder schnellen Events
  • Entkopplung: ISR erzeugt nur Events, Logik arbeitet im Hauptcode

Warum Event-Queues Debugging erleichtern

Wenn Sie Ereignisse als Codes in einer Queue speichern, können Sie später ausgeben: „Event A kam, dann B, dann Timeout“. Damit lassen sich Timing-Probleme und seltene Fehler oft viel schneller finden als mit verstreuten Serial-Prints.

Interrupts und State Machines: So passt das zusammen

Interrupts liefern Ereignisse – State Machines verarbeiten Ereignisse. Das ist eine ideale Kombination, solange Sie die Regeln beachten: Die ISR bleibt kurz und schreibt nur in eine sichere Struktur (Flag oder Queue). Die State Machine reagiert im nächsten loop-Durchlauf auf das Ereignis und entscheidet über den Übergang. So bleibt Ihr System echtzeitnah, ohne instabil zu werden.

  • ISR: setzt Event „BUTTON_PRESSED“ oder erhöht Zähler
  • FSM: verarbeitet Event und wechselt z. B. von RUNNING nach MENU
  • Vorteil: keine Logik in ISR, keine Nebenwirkungen

Timeouts und Timer: Zeit wird in Zustandsautomaten elegant

Time-basiertes Verhalten ist einer der größten Gründe für Spaghetti-Code. Zustandsautomaten lösen das sauber: Jeder Zustand kann einen eigenen Startzeitpunkt haben. In Do prüfen Sie mit millis() oder einem Timer, ob eine Frist abgelaufen ist. Damit bekommen Sie nachvollziehbare Regeln: „Wenn CONNECTING länger als 10 Sekunden dauert, gehe zu ERROR.“

  • Zustandsstart merken: timestamp beim Entry speichern
  • Regel prüfen: if (now – start > timeout) → Übergang
  • Keine Warte-Schleifen: System bleibt reaktionsfähig

WLAN- und MQTT-Reconnects profitieren besonders

Netzwerklogik ist voller Sonderfälle: verbunden, getrennt, DNS langsam, Broker down. Mit Zustandsautomaten lassen sich diese Fälle klar modellieren: CONNECTING, CONNECTED, RETRY_WAIT, OFFLINE_MODE – statt unübersichtlicher if-Kaskaden.

Fehlerzustände und Recovery: Das unterschätzte Erfolgsmerkmal

Viele Projekte behandeln Fehler implizit: „Wenn etwas nicht klappt, probiere ich es einfach nochmal.“ Das kann funktionieren, kann aber auch zu Endlosschleifen führen, die Energie fressen (Batteriebetrieb) oder Geräte „hängen lassen“. Eine State Machine zwingt Sie zu einer klaren Fehlerstrategie: Wie viele Retries? Wie lange warten? Was ist der Fallback? Gibt es einen Safe Mode? Wird der Nutzer informiert?

  • Retry begrenzen: z. B. 3 Versuche, dann Wartemodus
  • Fallback definieren: z. B. lokale Steuerung ohne Cloud
  • Safe Mode: minimaler Betrieb (z. B. OTA + Grundfunktion)
  • Beobachtbarkeit: Status-LED, Fehlercode, Telemetrie

Besonders wichtig bei OTA und Batteriegeräten

OTA-Updates und Deep Sleep sind empfindlich gegenüber Fehlern: Ein falscher Ablauf kann Geräte unbrauchbar machen oder Batterien schnell leeren. Zustandsautomaten helfen, diese Prozesse in kontrollierte Schritte zu zerlegen, die jeweils überprüfbar sind.

Saubere Struktur im Code: Zustände als Funktionen oder Klassen

Wie Sie implementieren, hängt vom Stil ab. Für viele Projekte reicht ein Enum für Zustände und ein switch-case, der pro Zustand eine kleine Funktion aufruft. Wichtig ist, dass die Zustandslogik nicht in einem riesigen Block endet. Alternativ können Sie Zustände als Klassen modellieren (State Pattern). Das ist umfangreicher, kann aber bei großen Projekten sehr übersichtlich werden.

  • Enum + Funktionen: leicht, schnell, gut für Arduino-Projekte
  • Tabellengetrieben: Übergänge als Tabelle, sehr sauber bei vielen Zuständen
  • State Pattern: objektorientiert, gut skalierbar, aber mehr Boilerplate

Eine Einordnung zum Zustandsmuster (State Pattern) kann helfen, wenn Sie objektorientiert arbeiten möchten.

Typische State-Machine-Muster für Maker-Projekte

Einige Muster tauchen immer wieder auf und haben sich bewährt. Sie bieten Ihnen „Baupläne“ für häufige Probleme, ohne dass Sie jedes Mal neu erfinden müssen.

  • Setup-Portal-Muster: CONFIG_MODE → CONNECTING → RUNNING
  • Retry-Backoff-Muster: CONNECTING → RETRY_WAIT → CONNECTING
  • UI-Menü-Muster: IDLE → MENU → EDIT → SAVE
  • Deep-Sleep-Muster: WAKE → MEASURE → SEND → SLEEP
  • Safety-Muster: RUNNING → EMERGENCY_STOP → SAFE_IDLE

Backoff: Warum „sofort wieder verbinden“ oft schlecht ist

Wenn WLAN oder ein Server gerade ausfällt, ist sofortiges Reconnecten energieintensiv und belastet das Netz. Ein Backoff-Zustand mit wachsender Wartezeit macht Systeme stabiler und schont Ressourcen.

Häufige Fehler bei Zustandsautomaten – und wie Sie sie vermeiden

Auch State Machines können unübersichtlich werden, wenn man Zustände zu fein aufteilt oder Übergänge „überallhin“ zulässt. Ein guter Zustandsautomat bleibt begrenzt, konsistent und dokumentiert. Die folgenden Fehler treten besonders oft auf.

  • Zu viele Zustände: unnötige Komplexität, ohne Mehrwert
  • Unklare Zustandsnamen: „STATE1“, „RUN2“ – später nicht mehr verständlich
  • Übergänge ohne Regeln: jeder Zustand kann in jeden wechseln
  • Seiteneffekte: Zustandswechsel lösen versteckte Aktionen in fremden Modulen aus
  • Blockierender Code: delay() oder Warteschleifen untergraben die Reaktivität

Qualitätskriterium: Sie können das Verhalten als Diagramm erklären

Wenn Sie Ihre Logik nicht als Zustandsdiagramm skizzieren können, ist sie wahrscheinlich nicht sauber genug strukturiert. Ein kurzes Diagramm zwingt zur Klarheit: Zustände, Events, Übergänge, Timeouts.

Praxis-Checkliste: State Machines für sauberen Code

  • Zustände klar benennen: was bedeutet der Zustand konkret?
  • Events definieren: Button, Timer, Netzwerk, Sensor, Fehler
  • Entry/Do/Exit trennen: Initialisierung nicht in jedem Loop-Durchlauf
  • Nicht-blockierend arbeiten: millis()-Timer statt delay()
  • ISR nur als Event-Quelle: keine Logik in Interrupts
  • Timeouts festlegen: keine endlosen „warte bis…“ Zustände
  • Fehlerstrategie: Retries begrenzen, Fallback/Safe Mode planen
  • Dokumentation: kleines Diagramm oder Tabelle der Übergänge pflegen

Weiterführende Ressourcen

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