Anwendungsentwurfmuster: Zustandsautomaten

Überblick

Der Zustandsautomat ist eine der grundlegenden Architekturen, die von LabVIEW-Entwicklern häufig zum schnellen Erstellen von Anwendungen verwendet wird. Die Architektur des Zustandsautomaten kann zur Implementierung komplexer Algorithmen zur Entscheidungsfindung verwendet werden, die durch Zustandsdiagramme oder Ablaufdiagramme dargestellt werden. Ein Zustandsautomat kann mithilfe von LabVIEW-Funktionen implementiert werden. Für die Architektur sind keine zusätzlichen Toolkits oder Module erforderlich.

 

In diesem Artikel wird beschrieben, was ein Zustandsautomat ist, er enthält ferner Anwendungsbeispiele, konzeptuelle Beispiele für einen Zustandsautomaten und Programmcodebeispiele für einen Zustandsautomaten.

 

Sehen Sie sich eine Anleitung für Entwickler für die Vorlage zum einfachen Zustandsautomaten an.

Inhalt

Was ist ein Zustandsautomat?

Ein Zustandsautomat ist eine Programmierarchitektur, die einen dynamischen Fluss zu Zuständen in Abhängigkeit von Werten aus vorigen Zuständen oder Benutzereingängen ermöglicht.

Diese Architektur eignet sich für Anwendungen, die eine Kombination aus folgenden Komponenten darstellen:

  • Zustände
  • Entscheidungsalgorithmen, die den Wechsel zu einem bestimmten Zustand festlegen


Ein Zustand kann als der Status innerhalb des Programms definiert werden, während der gesamte Task des Programms ausgeführt wird. Beispiele für Zustände sind Initialisierung, Warten, Ausführung einer Berechnung, Statusprüfung usw.

Eine logische Anweisung hilft bei der Bestimmung, wann und zu welchem Zustand gewechselt werden soll. Mit Hilfe von Ereignissen können Sie den Übergang von einem Zustand zum nächsten auslösen. Dabei kann es sich um programmatische Ereignisse oder um benutzerdefinierte Ereignisse handeln, etwa das Drücken einer Schaltfläche.

Jeder Zustand eines Zustandsautomaten bewirkt etwas anderes und ruft andere Zustände auf. Die Zustandskommunikation hängt von einer bestimmten Bedingung oder Sequenz ab. Um das Zustandsdiagramm in eine LabVIEW-Programmierarchitektur umzuwandeln, benötigen Sie die folgende Infrastruktur:

  1. While-Schleife — Führt kontinuierlich die verschiedenen Zustände aus
  2. Case-Struktur — Jeder Case enthält Programmcode, der für einen bestimmten Zustand ausgeführt werden soll
  3. Schieberegister — Enthält Angaben zum Zustandsübergang
  4. Übergangscode — bestimmt den nächsten Zustand in der Sequenz (siehe folgenden Abschnitt „Beispiele für Übergangscode“)

Warum einen Zustandsautomaten verwenden?

Zustandsautomaten werden in Anwendungen mit unterscheidbaren Zuständen verwendet. Jeder Zustand kann zu einem oder mehreren Zuständen oder aber auch zum Ende des gesamten Prozessablaufs führen. Ein Zustandsautomat stützt sich auf Benutzereingaben oder zustandsinterne Berechnungen, um zu bestimmen, zu welchem Zustand als nächstes gewechselt werden soll. Viele Applikationen haben einen „Initialisierungszustand“. Diesem folgt in der Regel ein Standardzustand, in dem verschiedene Aktionen durchgeführt werden können. Die Aktionen können von vorherigen und aktuellen Eingaben sowie Zuständen abhängig sein. Der Zustand „Beenden“ kann dann für Bereinigungsvorgänge verwendet werden.

Neben ihrer nützlichen Fähigkeit, Entscheidungsfindungsalgorithmen zu implementieren, bieten Zustandsmaschinen auch eine zweckmäßige Art der Anwendungsplanung. Mit zunehmender Komplexität von Anwendungen wird auch der Bedarf an adäquaten Entwürfen erhöht. Zustandsdiagramme und Ablaufdiagramme sind nützlich und manchmal für den Entwurfsprozess unerlässlich. Zustandsautomaten sind nicht nur für die Anwendungsplanung von Vorteilen, sondern auch einfach zu erstellen.

Anwendungsfälle

Das Entwurfsmuster für Zustandsautomaten eignet sich beispielsweise für folgende Anwendungen:

  • Dialogfelder mit einer Seite oder mehreren Registerkarten. Jede Registerkarte des Dialogs entspricht einem Zustand. Der Benutzer initiiert die Zustandsübergänge durch Auswahl einer bestimmten Registerkarte. Jeder Zustand umfasst alle Arbeitsschritte, die vom Benutzer auf der jeweiligen Registerkarte ausgeführt werden können.
  • Ein Bankautomat. Zustände einer solchen Anwendung sind beispielsweise das Warten auf eine Benutzereingabe, das Vergleichen des gewünschten Geldbetrags mit dem Kontostand, die Geldausgabe, das Drucken eines Belegs usw.
  • Eine Anwendung, die Messungen durchführt, Daten protokolliert und dann auf die nächste Benutzereingabe wartet. Zustände einer solchen Anwendung sind beispielsweise das Warten auf eine Benutzereingabe, die Durchführung der Messung, die Datenprotokollierung, die Datenanzeige usw.
  • Zustandsautomaten werden am häufigsten bei der Programmierung von Benutzeroberflächen verwendet. Beim Erstellen einer Benutzeroberfläche leiten verschiedene Benutzeraktionen die Benutzeroberfläche in verschiedene Verarbeitungssegmente. Jedes dieser Segmente fungiert als Zustände im Zustandsautomaten. Diese Segmente können entweder zu einem anderen Segment zur weiteren Verarbeitung führen oder auf ein weiteres Benutzerereignis warten. In diesem Beispiel versucht der Zustandsautomat fortlaufend zu ermitteln, welche Aktion der Benutzer als Nächstes ausführt.
  • Prozesstests sind eine weitere häufige Anwendung von Zustandsautomaten. In diesem Beispiel wird jedes Segment im Prozess durch einen Zustand dargestellt. Je nach Ergebnis eines Tests wird ein anderer Zustand aufgerufen. Das kann fortlaufend der Fall sein, wodurch Anwender eine detaillierte Analyse des Testprozesses erhalten.


Ein weiteres Entwurfsmuster zur Implementierung einer Benutzeroberfläche ist der Handler für Nachrichten-Queues. Ein Handler für Nachrichten-Queues ist eine anspruchsvollere Version des Zustandsautomaten und bietet zusätzliche Flexibilität, aber auch zusätzliche Komplexität. 

Erstellen eines Zustandsautomaten

Zum Erstellen eines effektiven Zustandsautomaten muss der Entwickler (1) eine Liste der möglichen Zustände erstellen. Mit dieser Liste kann der Entwickler (2) planen, wie jeder Zustand miteinander in Beziehung steht. Anschließend kann das Zustandsdiagramm (3) in die grafische Programmierarchitektur von LabVIEW umgewandelt werden.

Beispiel: Abfeuern einer Kanone

In diesem Beispiel möchten wir eine Anwendung erzeugen, die kontinuierlich eine Kanone abfeuert, ohne dass diese gefährlich heiß wird.

(1)   Mögliche Zustände auflisten
Zunächst erstellen wir eine Liste aller möglichen Zustände für unseren Task. Um die kontinuierliche Aktivierung der Kanone zu erledigen, müssen Sie:

  • das Programm initialisieren
  • die Kanone einschalten
  • die Kanone abfeuern
  • die Gerätetemperatur prüfen (Status)
  • das Gerät passiv kühlen (wenn die Temperatur immer noch im zulässigen Bereich liegt, aber steigt)
  • das Gerät aktiv kühlen (wenn die Temperatur außerhalb des zulässigen Bereichs liegt)
  • das Gerät herunterfahren
     

(2)   Beziehungen zwischen Zuständen in einem Zustandsdiagramm abbilden
Als Nächstes betrachten wir die Beziehung zwischen diesen Zuständen und erstellen ein Zustandsdiagramm. Überlegen Sie beim Erstellen des Zustandsdiagramms, was dazu führen würde, dass das Programm von einem Zustand zum nächsten wechselt – handelt es sich um einen automatischen Übergang? Gibt es einen externen Trigger vom Benutzer? Basiert der Übergang auf dem Ergebnis einer Berechnung?

Betrachten wir beispielsweise die Beziehung zwischen dem Initialisierungszustand und anderen Zuständen.

  1. Die Initialisierung ist der erste Schritt im Programm. Für diesen Zustand gibt es keine Eingänge.
  2. Wenn keine Fehler vorliegen, fahren wir mit dem Einschalten der Kanone fort.
  3. Wenn Fehler vorhanden sind, soll das Programm beendet werden
  4. Wenn der Benutzer während der Initialisierung eine Stopp-Schaltfläche drückt, möchten wir in den Zustand „Beenden“ wechseln.

Beachten Sie, dass wir, während wir über die Beziehung zwischen den Zuständen nachzudenken begonnen haben, damit begonnen haben, die Logik für den Wechsel zwischen diesen Zuständen zu definieren. Die Programmierlogik, die wir im Programmcode verwenden, hängt von (1) der Anzahl der möglichen Übergangszustände und (2) der Anzahl der Eingänge ab, die in der Logik berücksichtigt werden. Die Optionen für den Code-Übergang werden im Abschnitt Beispiele für den Übergangscode beschrieben.

Arbeiten Sie weiter durch, wie sich die Zustände zueinander verhalten, bis Sie ein vollständiges Zustandsdiagramm haben. Das Zustandsdiagramm enthält alle Zustände und deren Beziehung (Abbildung 1). In diesem Diagramm beschreiben die Zustände (ovale Knoten) die Aktionen, die ausgeführt werden, wenn sich der Steuerungsprozess in diesem Zustand befindet, während die Übergänge (Pfeile) lediglich beschreiben, wann und wie der Prozess von einem Zustand zum nächsten wechseln kann.

Abbildung 1: Zustandsdiagramm des Abfeuerns einer Kanone

Die Beziehung zwischen den einzelnen Zuständen hilft Ihnen bei der Programmierung der Logik, die für den Übergang von einem Zustand zum nächsten erforderlich ist.

(3) Erstellen eines Zustandsautomaten in LabVIEW
Nachdem Sie die Zustände und ihre Beziehung in einem Zustandsdiagramm definiert haben, können Sie dieses auf die Codierungsarchitektur in LabVIEW übertragen (Abbildung 2). Der Fluss zwischen den Zuständen des Zustandsdiagramms (Abbildung 1) wird von der Schleife implementiert. Die einzelnen Zustände werden durch Cases in der Case-Struktur ersetzt. Jeder Zustand im abgebildeten Diagramm entspricht einem Unterdiagramm der Case-Struktur. Jeder Zustand:

  1. führt einen Arbeitsschritt aus
  2. informiert den Zustandsautomaten über den nächsten Zustand, indem eine Anweisung an ein Schieberegister der While-Schleife übergeben wird (z. B. Übergangscode)


Ein Schieberegister an der While-Schleife verfolgt den aktuellen Zustand, der an den Eingang der Case-Struktur übergeben wird.

Abbildung 2: Zustandsautomat

Beispielprogramm für Zustandsautomaten

Ein Beispiel für die Anpassung dieser Vorlage an eine Messanwendung finden Sie im Beispielprojekt Einzelschussmessung, das im Dialogfeld Projekt erstellen verfügbar ist.

  • Nach der Initialisierung geht der Zustandsautomat in den Zustand „Warten auf Ereignis“ über. Dieser Zustand enthält eine Ereignisstruktur, die auf Änderungen des Frontpanels wartet. Wenn der Benutzer auf eine Schaltfläche klickt, erkennt LabVIEW das Ereignis und wechselt zum entsprechenden Unterdiagramm der Ereignisstruktur. Das Unterdiagramm initiiert dann einen Übergang zum nächsten Zustand.
  • Jeder Zustand hat Zugriff auf einen Cluster aus Daten. Die Datentypen in diesem Cluster sind in Data.ctl definiert.
  • Gültige Zustände sind in der Typdefinition State.ctl festgelegt. Durch die Verwendung einer Typdefinition für Zustandsübergänge werden mögliche Zustände eingeschränkt. Auf diese Weise kann verhindert werden, dass der Zustandsautomat einen nicht erkannten Zustand annimmt.
  • Die Anwendung kann nur mit dem Stoppzustand angehalten werden. Auf diese Weise wird ein unbeabsichtigtes oder unvollständiges Beenden verhindert, da
     

1.     der Programmcode zum Beenden nur ausgeführt wird, wenn der Benutzer die Anwendung beenden möchte.
2.     der Programmcode zum Beenden immer vollständig ausgeführt wird.

  • Es wird nur jeweils ein Zustand ausgeführt und mit einer einzigen While-Schleife wird erreicht, dass für alle Tasks dieselbe Ausführungsrate gilt. Wenn Sie verschiedene Raten oder parallele Tasks benötigen, sollten Sie die Vorlage "Handler für Nachrichten-Queues" oder "Akteur-Framework" in Erwägung ziehen. Sie können diese Vorlagen im Dialogfeld Projekt erstellen auswählen.
  • Der Zustand „Warten auf Ereignis“ ist der einzige Zustand, der einen Benutzereingang erkennt. Um Benutzereingänge zu verarbeiten, muss sich der Zustandsautomat in diesem Zustand befinden.

Erstellen Ihrer eigenen Zustandsautomaten-Anwendung

Weitere Informationen zu den ersten Schritten finden Sie in der Anleitung Anpassen der Vorlage für den einfachen Zustandsautomaten.  

Bestimmen Ihrer Bedürfnisse

Bevor Sie die Vorlage bearbeiten, stellen Sie sich folgende Fragen:

  • Aus welchen Zuständen besteht die Anwendung? Die Antwort auf diese Frage bestimmt die Zustände, die Sie hinzufügen.
  • Welcher Zustand soll welchem folgen? Die Antwort auf diese Frage bestimmt den Wert des Enum-Elements Nächster Zustand, den jeder Zustand an das Schieberegister der While-Schleife sendet.

    Ein Zustand kann je nach Bedingung in andere Zustände übergehen. So hängt der nächste Zustand von „Warten auf Ereignis“ in der Vorlage beispielsweise von der Eingabe des Benutzers ab.
  • Auf welche Art von Daten wird jeder Zustand Zugriff benötigen? Die Antwort auf diese Frage bestimmt, welche Datentypen Sie zu Data.ctl hinzufügen.
  • Welche Fehler können auftreten und wie sollte die Anwendung auf diese Fehler reagieren? Die Antworten auf diese Fragen bestimmen den Umfang der Fehlerbehandlung, die Sie benötigen.
     

Beispiele für Übergangscode

Es gibt verschiedene Verfahren, um zu bestimmen, in welchen Zustand als nächstes übergegangen werden soll, die unten erörtert werden.
Beachten Sie, dass die Beispielabbildungen den Zustand „Init“ anzeigen, aber diese Übergangsmöglichkeiten auf jeden Zustand angewandt werden können.

Eins zu eins: Wenn Sie immer von Zustand A in Zustand B wechseln, müssen Sie keine Logik programmieren. Geben Sie einfach den Namen des nächsten Cases (Case B) an das Schieberegister aus.

Abbildung 3a: Nur ein möglicher Übergangszustand

Einer auf zwei: Wenn Sie von Zustand A auf Zustand B oder Zustand C wechseln könnten, können Sie eine Select-Funktion verwenden, um den Status eines Anzeigeelements zu beurteilen. Sie sollten etwas evaluieren, das bestimmt, zu welchem Zustand Sie wechseln möchten. In Abbildung 3b sehen wir beispielsweise, dass die Benutzereingabe der Stopp-Schaltfläche bestimmt, ob wir den Einschaltzustand verlassen oder zum Herunterfahren übergehen.

Abbildung 3b: Zwei mögliche Übergangszustände

Einer auf mehrere mithilfe von Arrays: Wenn Sie mehrere Zustände haben, in die Sie wechseln können, können Sie einen booleschen Bereich verwenden, der einer Enum-Konstante zugeordnet ist, um den Übergang zu programmieren. In Abbildung 3c wird beispielsweise ein Programmcode ausgeführt, bei dem das Ergebnis des Programmcode den Übergang bestimmt, bei dessen Ausgabe es sich um ein boolesches Array handelt. Das boolesche Array korreliert mit einer Enum-Konstante, die eine Liste der möglichen Zustände enthält, auf die Sie übergehen können. Mit der Funktion „Array indizieren“ wird der Index des ersten booleschen „True“-Werts im booleschen Array ausgegeben. Anschließend können Sie mit der Funktion „Teil-Array“ den entsprechenden Wert aus der Enum-Konstante entnehmen, mit der er korreliert.

Abbildung 3c

Tipp : Denken Sie daran, dass Arrays mit 0 und Enums mit 1 indiziert werden. Um das boolesche Array mit der Enum-Konstante zu korrelieren, korrigieren Sie diesen Offset mit der Funktion Inkrementieren.

Einer auf mehrere mithilfe der While-Schleife: Eine weitere Option, wenn Sie mehrere mögliche Übergangszustände haben, besteht darin, eine While-Schleife innerhalb eines Cases zu verwenden. Der Programmcode in der While-Schleife wird so lange fortgesetzt, bis ein boolescher Status auf TRUE gesetzt wird und die Stopp-Schaltfläche ausgelöst wird. Auf diese Weise kann Programmcode so lange ausgeführt werden, bis ein Trigger-Ereignis auftritt.

Abbildung 3d zeigt einen „Init“-Zustand, der mit einer inneren Schleife und einer Case-Struktur zum nächsten Zustand wechselt. Die innere Case-Struktur enthält ein Diagramm für jeden Übergang, der den aktuellen Zustand verlässt. Jeder Case in der inneren Case-Struktur hat zwei Ausgänge: einen booleschen Wert, mit dem festgelegt wird, ob der Übergang übernommen werden soll oder nicht, und eine Enum-Konstante, mit der der Zustand festgelegt wird, zu dem der Übergang führt. Wenn der Schleifenindex als Eingang für die Case-Struktur verwendet wird, durchläuft dieser Programmcode jeden Übergangs-Case nacheinander, bis ein Diagramm mit dem booleschen Ausgang „True“ gefunden wird. Nachdem der boolesche Ausgang „True“ gefunden wurde, gibt der Case den neuen Zustand aus, zu dem der Übergang wechselt. Obwohl diese Methode etwas komplizierter erscheinen kann als die vorherigen Methoden, bietet sie die Möglichkeit, den Übergängen Namen hinzuzufügen, indem die Ausgabe des Schleifenindex in einen Enum-Typ umgewandelt wird. Mit diesem Vorteil können Sie Ihrem Übergangscode „automatische Dokumentation“ hinzufügen.

Abbildung 3d

Weitere Überlegungen zur Programmierung

 

Programmcode-Redundanz
Problem: Der schwierigste Teil beim Erstellen eines Zustandsautomaten ist die Unterscheidung zwischen möglichen Zuständen im Zustandsdiagramm. Im Zustandsdiagramm der Cola-Maschine (Abbildung 4) könnten wir z. B. 0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50 Cent als Zustände haben, anstatt einen Zustand „Warten auf Antwort“, der je nach Art der Münze von einem Zustand zum nächsten wechselt. Dadurch würden elf verschiedene Zustände mit demselben Case-Diagramm erzeugt. Redundanter Programmcode kann in einer größeren Anwendung ein schwerwiegendes Problem darstellen.

Abhilfe
: Wenn verschiedene Zustände das gleiche Case-Diagramm haben, versuchen Sie, diese in einem Zustand zu kombinieren. So wird z. B. der Zustand “Warten auf Antwort“ erstellt, um Code-Redundanz zu vermeiden.

Verwendung von Enums
Problem: Enums werden häufig als Case-Selektoren in Zustandsautomaten verwendet. Wenn der Benutzer versucht, dieser Enum einen Zustand hinzuzufügen oder zu löschen, werden die verbleibenden angeschlossenen Verbindungen mit den Kopien dieser Enum fehlerhaft. Dies ist das häufigste Problem bei der programmatischen Umsetzung von Zustandsautomaten mit Enums.

Lösung: Zwei mögliche Lösungen für dieses Problem sind:
1. Wenn alle Enums aus der geänderten Enum kopiert werden, verschwinden die Unterbrechungen.
2. Erstellen Sie ein neues Element mit der Enum und wählen Sie aus dem Untermenü die Option „Typdefinition“ aus. Bei Auswahl von Typdefinition werden alle Enum-Kopien automatisch aktualisiert, wenn der Benutzer einen Zustand hinzufügt oder entfernt.

Nächste Schritte

Wenn Sie mehr über Zustandsautomaten und andere fortgeschrittene Architekturen in LabVIEW erfahren möchten, sollten Sie den Kundenschulungskurs „LabVIEW-Grundlagen 2“ absolvieren.