Home > IEC 61131-3 > IEC 61131-3: Das ‘State’ Pattern

IEC 61131-3: Das ‘State’ Pattern

Besonders in der Automatisierungstechnik finden Zustandsautomaten regelmäßig Anwendung. Mit Hilfe des State Pattern steht ein objektorientierter Ansatz zur Verfügung, der insbesondere bei größeren Zustandsautomaten wichtige Vorteile bietet.

Die meisten Entwickler haben schon Zustandsautomaten in IEC 61131-3 realisiert. Der eine bewusst, der andere vielleicht unbewusst. Im Folgenden soll ein einfaches Beispiel drei verschiedene Ansätze vorstellen:

  1. CASE-Anweisung
  2. Zustandsübergänge in Methoden
  3. Das ‚State‘ Pattern

Unser Beispiel beschreibt einen Automaten, der nach Einwurf einer Münze und nach dem Drücken eines Knopfes ein Produkt ausgibt. Die Anzahl der Produkte ist begrenzt. Wird eine Münze eingeworfen und der Knopf betätigt, obwohl der Automat leer ist, so wird die Münze wieder zurückgegeben.

Der Automat soll durch den Funktionsblock FB_Machine abgebildet werden. Eingänge nehmen die Ereignisse entgegen und über Ausgänge wird der aktuelle Zustand und die Anzahl der noch verfügbaren Produkte ausgegeben. Bei der Deklaration des FBs wird die maximale Anzahl der Produkte festgelegt.

FUNCTION_BLOCK PUBLIC FB_Machine
VAR_INPUT
  bButton           : BOOL;
  bInsertCoin       : BOOL;
  bTakeProduct      : BOOL;
  bTakeCoin         : BOOL;    
END_VAR
VAR_OUTPUT
  eState            : E_States;
  nProducts         : UINT;    
END_VAR

UML-Zustandsdiagramm

Zustandsautomaten lassen sich sehr gut als UML-Zustandsdiagramm (englisch: state diagram) darstellen.

Picture01

Ein UML-Zustandsdiagramm beschreibt einen Automaten, der sich zu jedem Zeitpunkt in genau einem Zustand einer endlichen Menge von Zuständen befindet.

Die Zustände in einem UML-Zustandsdiagramm werden durch Rechtecke mit abgerundeten Ecken (englisch: vertices) dargestellt (in anderen Diagrammformen häufig auch als Kreis). Zustände können Aktivitäten ausführen, die z.B. beim Eintritt in den Zustand (entry) oder beim Verlassen (exit) ausgeführt werden. Mit entry / n = n – 1 wird beim Eintritt in den Zustand die Variable n dekrementiert.

Die Pfeile zwischen den Zuständen symbolisieren mögliche Zustandsübergänge (englisch: transitions). Sie sind mit den Ereignissen beschriftet, die zu dem jeweiligen Zustandsübergang führen. Ein Zustandsübergang erfolgt, wenn das Ereignis eintritt und eine optionale Bedingung (guard) erfüllt ist. Bedingungen werden in eckigen Klammern angegeben. Hierdurch lassen sich Entscheidungsbäume realisieren.

Erste Variante: CASE-Anweisung

Häufig findet man CASE-Anweisungen für die Umsetzung von Zustandsautomaten. Die CASE-Anweisung fragt jeden möglichen Zustand ab. Innerhalb der jeweiligen Bereiche, für die einzelnen Zustände, werden die Bedingungen abgefragt. Ist die Bedingung erfüllt, wird die Aktion ausgeführt und die Zustandsvariable angepasst. Um die Lesbarkeit zu erhöhen, wird die Zustandsvariable gerne als ENUM abgebildet.

TYPE E_States :
(
    eWaiting := 0,
    eHasCoin,
    eProductEjected,
    eCoinEjected
);
END_TYPE

Somit sieht die erste Variante vom Zustandsautomat wie folgt aus:

FUNCTION_BLOCK PUBLIC FB_Machine
VAR_INPUT
  bButton             : BOOL;
  bInsertCoin         : BOOL;
  bTakeProduct        : BOOL;
  bTakeCoin           : BOOL;
END_VAR
VAR_OUTPUT
  eState              : E_States;
  nProducts           : UINT;
END_VAR
VAR
  rtrigButton         : R_TRIG;
  rtrigInsertCoin     : R_TRIG;
  rtrigTakeProduct    : R_TRIG;
  rtrigTakeCoin       : R_TRIG;
END_VAR
rtrigButton(CLK := bButton);
rtrigInsertCoin(CLK := bInsertCoin);
rtrigTakeProduct(CLK := bTakeProduct);
rtrigTakeCoin(CLK := bTakeCoin);

CASE eState OF
  E_States.eWaiting:
    IF (rtrigButton.Q) THEN
      ; // keep in the state
    END_IF
    IF (rtrigInsertCoin.Q) THEN
      ADSLOGSTR(ADSLOG_MSGTYPE_HINT, 'Customer has insert a coin.', '');
      eState := E_States.eHasCoin;
    END_IF

  E_States.eHasCoin:
    IF (rtrigButton.Q) THEN
      IF (nProducts > 0) THEN
        nProducts := nProducts - 1;
        ADSLOGSTR(ADSLOG_MSGTYPE_HINT, 'Customer has pressed the button. Output product.', '');
        eState := E_States.eProductEjected;
      ELSE
        ADSLOGSTR(ADSLOG_MSGTYPE_HINT, 'Customer has pressed the button. No more products. Return coin.', '');
        eState := E_States.eCoinEjected;
      END_IF
    END_IF

  E_States.eProductEjected:
    IF (rtrigTakeProduct.Q) THEN
      ADSLOGSTR(ADSLOG_MSGTYPE_HINT, 'Customer has taken the product.', '');
      eState := E_States.eWaiting;
    END_IF

  E_States.eCoinEjected:
    IF (rtrigTakeCoin.Q) THEN
      ADSLOGSTR(ADSLOG_MSGTYPE_HINT, 'Customer has taken the coin.', '');
      eState := E_States.eWaiting;
    END_IF

  ELSE
    ADSLOGSTR(ADSLOG_MSGTYPE_ERROR, 'Invalid state', '');
    eState := E_States.eWaiting;
END_CASE

Ein kurzer Test zeigt, dass der FB das macht, was er machen soll:

Picture02

Doch wird auch schnell deutlich, dass größer Anwendungen so nicht umsetzbar sind. Die Übersichtlichkeit geht nach wenigen Zuständen komplett verloren.

Beispiel 1 (TwinCAT 3.1.4022) auf GitHub

Zweite Variante: Zustandsübergänge in Methoden

Das Problem ist reduzierbar, wenn alle Zustandsübergänge als Methode realisiert werden.

Picture03

Tritt ein bestimmtes Ereignis auf, so wird die jeweilige Methode aufgerufen.

FUNCTION_BLOCK PUBLIC FB_Machine
VAR_INPUT
  bButton             : BOOL;
  bInsertCoin         : BOOL;
  bTakeProduct        : BOOL;
  bTakeCoin           : BOOL;
END_VAR
VAR_OUTPUT
  eState              : E_States;
  nProducts           : UINT;
END_VAR
VAR
  rtrigButton         : R_TRIG;
  rtrigInsertCoin     : R_TRIG;
  rtrigTakeProduct    : R_TRIG;
  rtrigTakeCoin       : R_TRIG;
END_VAR
rtrigButton(CLK := bButton);
rtrigInsertCoin(CLK := bInsertCoin);
rtrigTakeProduct(CLK := bTakeProduct);
rtrigTakeCoin(CLK := bTakeCoin);

IF (rtrigButton.Q) THEN
  THIS^.PressButton();
END_IF
IF (rtrigInsertCoin.Q) THEN
  THIS^.InsertCoin();
END_IF
IF (rtrigTakeProduct.Q) THEN
  THIS^.CustomerTakesProduct();
END_IF
IF (rtrigTakeCoin.Q) THEN
  THIS^.CustomerTakesCoin();
END_IF

Je nach aktuellem Zustand, wird in den Methoden der gewünschte Zustandsübergang ausgeführt und die Zustandsvariable angepasst:

METHOD INTERNAL CustomerTakesCoin : BOOL
IF (THIS^.eState = E_States.eCoinEjected) THEN
  ADSLOGSTR(ADSLOG_MSGTYPE_HINT, 'Customer has taken the coin.', '');
  eState := E_States.eWaiting;
END_IF

METHOD INTERNAL CustomerTakesProduct : BOOL
IF (THIS^.eState = E_States.eProductEjected) THEN
  ADSLOGSTR(ADSLOG_MSGTYPE_HINT, 'Customer has taken the product.', '');
  eState := E_States.eWaiting;
END_IF

METHOD INTERNAL InsertCoin : BOOL
IF (THIS^.eState = E_States.eWaiting) THEN
  ADSLOGSTR(ADSLOG_MSGTYPE_HINT, 'Customer has insert a coin.', '');
  THIS^.eState := E_States.eHasCoin;
END_IF

METHOD INTERNAL PressButton : BOOL
IF (THIS^.eState = E_States.eHasCoin) THEN
  IF (THIS^.nProducts > 0) THEN
    THIS^.nProducts := THIS^.nProducts - 1;
    ADSLOGSTR(ADSLOG_MSGTYPE_HINT, 'Customer has pressed the button. Output product.', '');
    THIS^.eState := E_States.eProductEjected;
  ELSE                
    ADSLOGSTR(ADSLOG_MSGTYPE_HINT, 'Customer has pressed the button. No more products. Return coin.', '');
    THIS^.eState := E_States.eCoinEjected;
  END_IF
END_IF

Auch dieser Ansatz funktioniert tadellos. Der Zustandsautomat befindet sich allerdings weiterhin in nur einem Funktionsblock. Die Zustandsübergänge werden zwar in Methoden ausgelagert, jedoch handelt es sich um einen Lösungsansatz, der strukturierten Programmierung. Dieser ignoriert weiterhin die Möglichkeiten der Objektorientierung. Dies führt zu dem Ergebnis, dass der Quellcode weiterhin schlecht erweiterbar und unleserlich ist.

Beispiel 2 (TwinCAT 3.1.4022) auf GitHub

Dritte Variante: Das ‚State‘ Pattern

Zur Umsetzung des State Pattern sind einige OO-Entwurfsprinzipien hilfreich:

Kohäsion (= Grad, inwiefern eine Klasse einen einzigen konzentrierten Zweck hat) und Delegation

Kapsele jede Verantwortlichkeit in ein eigenes Objekt und delegiere Aufrufe an diese Objekte weiter. Eine Klasse, eine Verantwortlichkeit!

Identifiziere jene Aspekte, die sich ändern und trenne diese von jenen, die konstant bleiben

Wie werden die Objekte aufgeteilt, damit Erweiterungen am Zustandsautomat an möglichst wenigen Stellen notwendig sind? Bisher musste bei jeder Erweiterung FB_Machine angepasst werden. Gerade bei umfangreichen Zustandsautomaten, an denen mehrere Entwickler arbeiten, ist dieses ein großer Nachteil.

Betrachten wir noch einmal die Methoden CustomerTakesCoin(), CustomerTakesProduct(), InsertCoin() und PressButton(). Diese haben alle einen ähnlichen Aufbau. In If-Anweisungen wird der aktuelle Zustand abgefragt und die gewünschten Aktionen werden ausgeführt. Bei Bedarf wird außerdem der aktuelle Zustand angepasst. Dieser Ansatz skaliert jedoch nicht. Jedes Mal, wenn ein neuer Zustand hinzugefügt wird, müssen mehrere Methoden angepasst werden.

Das State Pattern verstreut den Status auf mehrere Objekte. Jeder mögliche Status wird durch einen FB repräsentiert. Diese Status FBs beinhalten das gesamte Verhalten für den jeweiligen Zustand. Dadurch kann ein neuer Status eingeführt werden, ohne dass der Quellcode der ursprünglichen Bausteine geändert werden muss.

Auf jeden Zustand kann jede Aktion (CustomerTakesCoin(), CustomerTakesProduct(), InsertCoin() und PressButton()) ausgeführt werden. Somit besitzen alle Status FBs die gleiche Schnittstelle. Aus diesem Grund wird ein Interface für alle Status FBs eingeführt:

Picture04
 
FB_Machine aggregiert dieses Interface (Zeile 9), welches die Methodenaufrufe an die jeweiligen Status FBs delegiert (Zeile 30, 34, 38 und 42).

FUNCTION_BLOCK PUBLIC FB_Machine
VAR_INPUT
  bButton            : BOOL;
  bInsertCoin        : BOOL;
  bTakeProduct       : BOOL;
  bTakeCoin          : BOOL;
END_VAR
VAR_OUTPUT
  ipState            : I_State := fbWaitingState;
  nProducts          : UINT;
END_VAR
VAR
  fbCoinEjectedState    : FB_CoinEjectedState(THIS);
  fbHasCoinState        : FB_HasCoinState(THIS);
  fbProductEjectedState : FB_ProductEjectedState(THIS);
  fbWaitingState        : FB_WaitingState(THIS);

  rtrigButton           : R_TRIG;
  rtrigInsertCoin       : R_TRIG;
  rtrigTakeProduct      : R_TRIG;
  rtrigTakeCoin         : R_TRIG;
END_VAR

rtrigButton(CLK := bButton);
rtrigInsertCoin(CLK := bInsertCoin);
rtrigTakeProduct(CLK := bTakeProduct);
rtrigTakeCoin(CLK := bTakeCoin);

IF (rtrigButton.Q) THEN
  ipState.PressButton();
END_IF

IF (rtrigInsertCoin.Q) THEN
  ipState.InsertCoin();
END_IF

IF (rtrigTakeProduct.Q) THEN
  ipState.CustomerTakesProduct();
END_IF

IF (rtrigTakeCoin.Q) THEN
  ipState.CustomerTakesCoin();
END_IF

Doch wie kann in den jeweiligen Methoden, der einzelnen Status FBs, der Status geändert werden?

Als erstes wird von jedem Status FB eine Instanz innerhalb von FB_Machine deklariert. Per FB_init() wird an jeden Status FB ein Pointer auf FB_Machine übergeben (Zeile 13 – 16).

Jede einzelne Instanz kann per Eigenschaft aus FB_Machine gelesen werden. Zurückgegeben wird jedes Mal ein Interface Pointer auf I_State.

Picture05

Des Weiteren erhält FB_Machine eine Methode zum Setzen des Status,

METHOD INTERNAL SetState : BOOL
VAR_INPUT
  newState : I_State;
END_VAR
THIS^.ipState := newState;

sowie eine Methode zum Ändern der aktuellen Produktanzahl:

METHOD INTERNAL SetProducts : BOOL
VAR_INPUT
  newProducts : UINT;
END_VAR
THIS^.nProducts := newProducts;

FB_init() erhält eine weitere Eingangsvariable, damit bei der Deklaration die maximale Anzahl der Produkte vorgegeben werden kann.

Da der Anwender der Zustandsmaschine nur FB_Machine und I_State benötigt, wurden die vier Eigenschaften (CoinEjectedState, HasCoinState, ProductEjectedState und WaitingState), die beiden Methoden (SetState() und SetProducts()) und die vier Status FBs (FB_CoinEjectedState, FB_HasCoinState, FB_ProductEjectedState und FB_WaitingState) als INTERNAL deklariert. Befinden sich die FBs der Zustandsmaschine in einer compilierten Bibliothek, so sind diese von außen nicht sichtbar. Auch im Library Repository sind diese nicht vorhanden. Das Gleiche gilt auch für Elemente die als PRIVATE deklariert werden. FBs, Interfaces, Methoden und Eigenschaften, die nur innerhalb einer Bibliothek Verwendung finden, können so vor dem Anwender der Library versteckt werden.

Der Tests der Zustandsmaschine ist in allen drei Varianten gleich:

PROGRAM MAIN
VAR
  fbMachine      : FB_Machine(3);
  sState         : STRING;
  bButton        : BOOL;
  bInsertCoin    : BOOL;
  bTakeProduct   : BOOL;
  bTakeCoin      : BOOL;
END_VAR

fbMachine(bButton := bButton,
          bInsertCoin := bInsertCoin,
          bTakeProduct := bTakeProduct,
          bTakeCoin := bTakeCoin);
sState := fbMachine.ipState.Description;

bButton := FALSE;
bInsertCoin := FALSE;
bTakeProduct := FALSE;
bTakeCoin := FALSE;

Die Anweisung in Zeile 15 soll das Testen vereinfachen, da für jeden Zustand ein lesbarer Text angezeigt wird.

Beispiel 3 (TwinCAT 3.1.4022) auf GitHub

Diese Variante wirkt bei der ersten Betrachtung recht aufwendig, da deutlich mehr FBs benötigt werden. Doch die Verteilung der Zuständigkeiten auf einzelne FBs macht diesen Ansatz sehr flexibel und deutlich robuster für Erweiterungen.

Dieses wird deutlich, wenn die einzelnen Status FBs sehr umfangreich werden. So könnte eine Zustandsmaschine einen komplexen Prozess steuern, bei dem jeder Status FB weitere Unterprozesse enthält. Eine Aufteilung auf mehrere FBs macht solch ein Programm erst überhaupt wartbar, insbesondere dann, wenn mehrere Entwickler daran beteiligt sind.

Bei sehr kleinen Zustandsmaschinen ist die Anwendung des State Pattern nicht unbedingt die optimalste Variante. Ich persönlich greife auch gerne auf die Lösung mit der CASE-Anweisung zurück.

Alternativ bietet die IEC 61131-3 mit der Ablaufsprache (AS) bzw. Sequential Function Chart (SFC) eine weitere Möglichkeit an Zustandsmaschinen umzusetzen. Aber das ist eine andere Geschichte.

Definition

In dem Buch “Entwurfsmuster. Elemente wiederverwendbarer objektorientierter Software” von Gamma, Helm, Johnson und Vlissides wird dieses wie folgt ausgedrückt:

”Ermögliche es einem Objekt, sein Verhalten zu ändern, wenn sein interner Zustand sich ändert. Es wird so aussehen, als ob das Objekt seine Klasse gewechselt hat.”

Implementierung

Es wird eine gemeinsame Schnittstelle definiert (State), die für jeden Zustandsübergang (Transistion) eine Methode enthält. Für jeden Zustand wird eine Klasse erstellt, die diese Schnittstelle implementiert (State1, State2, …). Da hierdurch alle Zustände die gleiche Schnittstelle besitzen, sind diese untereinander austauschbar.

Das Objekt, dessen Verhalten in Abhängigkeit vom Zustand geändert werden soll (Context), aggregiert (kapselt) ein solches Zustandsobjekt. Dieses Objekt repräsentiert den aktuellen internen Zustand (currentState) und kapselt das zustandsabhängige Verhalten. Der Context delegiert Aufrufe an das aktuell gesetzte Zustandsobjekt.

Die Zustandswechsel können durch die konkreten Zustandsobjekte selbst durchgeführt werden. Dazu benötigt jedes Zustandsobjekt eine Referenz auf den Context (context). Weiterhin muss der Context eine Methode anbieten, um den Zustand ändern zu können (setState()). Der Folgezustand wird der Methode setState() als Parameter übergeben. Hierzu bietet der Context alle möglichen Zustände als Eigenschaften an.

UML Diagramm


Picture06

Bezogen auf das obige Beispiel ergibt sich folgende Zuordnung:

Context FB_Machine
State I_State
State1, State2, … FB_CoinEjectedState, FB_HasCoinState, FB_ProductEjectedState, FB_WaitingState
Handle() CustomerTakesCoin(), CustomerTakesProduct(), InsertCoin(), PressButton()
GetState1, GetState2, … CoinEjectedState, HasCoinState, ProductEjectedState, WaitingState
currentState ipState
setState() SetState()
context pMachine

Anwendungsbeispiele

Ein TCP-Kommunikationsstack ist ein gutes Beispiel für die Verwendung des State Pattern. So kann jeder Zustand eines Verbindungs-Sockets durch entsprechende Zustandsklassen (TCPOpen, TCPClosed, TCPListen, …) abgebildet werden. Jede dieser Klassen implementiert das gleiche Interface (TCPState). Der Context (TCPConnection) beinhaltet das aktuelle Zustandsobjekt. Über dieses Zustandsobjekt werden alle Aktionen an die jeweilige Zustandsklasse übergeben. Diese bearbeitet die Aktionen und wechselt bei Bedarf in einen neuen Zustand.

Auch Textparser sind zustandsbasiert. So ist die Bedeutung eines Zeichens meistens abhängig von den zuvor gelesenen Zeichen.

Advertisements
  1. Wolfgang
    September 25, 2018 at 10:08 AM

    Wieder ein hervorragender Beitrag für ein weiteres “Pattern”. Ich persönlich habe die Erfahrung gemacht, dass das Implementieren eines (mindestens) high-level Zustandsautomaten die Entwicklung sehr vereinfacht. Das genannte Pattern ermöglicht die Implementierung eines solchen, sowohl als Mealy-, als auch als Moore-Automat.

    Ich dachte bisher immer auch an die genannte Ablaufsprache, bin jedoch mit dieser noch nicht auf ein ausreichendes Ergebnis gekommen. Sind Ihrerseits zu dieser auch Beiträge geplant?

    Grundsätzlich gibt es meiner Meinung noch einen weiteren Anwednungsfall: Wenn man mehrere Komplexe “Schrittketten” benötigt werden, die auch in “Unterabläufe/Unterprogramme” geliedert werden sollten, oder müssen. Hierbei reicht der klassische Case nicht aus und auch das State-Pattern würde ich hierfür nicht verwenden. Ich habe einmal eine Implementierung gesehen, bei der ein FB die Zustandsmaschine kapselt (ähnlich der Variante mit dem Case). Ein interner Stack ermöglichte auch die Wiederverwendung einzelner Schrittkettenpfade. Hierfür wurde die Zustandsvariable bei einem “Call” für den Rücksprung gespeichert.

    Ich freue mich auf weitere Beiträge

    Freundliche Grüße

  2. October 11, 2018 at 1:11 PM

    Stefan,

    as usual an excellent post!

    • October 19, 2018 at 9:31 AM

      Hi Jakob,
      thanks for your nice words. The English version will follow in a few days.

  1. No trackbacks yet.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: