State machines are used regularly, especially in automation technology. The state pattern provides an object-oriented approach that offers important advantages especially for larger state machines.
Most developers have already implemented state machines in IEC 61131-3: one consciously, the other one perhaps unconsciously. The following is a simple example of three different approaches:
- CASE statement
- State transitions in methods
- The state pattern
Our example describes a vending machine that dispenses a product after inserting a coin and pressing a button. The number of products is limited. If a coin is inserted and the button is pressed although the machine is empty, the coin is returned.
The vending machine shall be mapped by the function block FB_Machine. Inputs accept the events and the current state and the number of still available products are read out via outputs. The declaration of the FB defines the maximum number of products.
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 state diagram
State machines can be very well represented as a UML state diagram.
A UML state diagram describes an automaton that is in exactly one state of a finite set of states at any given time.
The states in a UML state diagram are represented by rectangles with rounded corners (vertices) (in other diagram forms also often as a circle). States can execute activities, e.g. when entering the state (entry) or when leaving the state (exit). With entry / n = n – 1, the variable n is decremented when entering the state.
The arrows between the states symbolize possible state transitions. They are labeled with the events that lead to the respective state transition. A state transition occurs when the event occurs and an optional condition (guard) is fulfilled. Conditions are specified in square brackets. This allows decision trees to be implemented.
First variant: CASE statement
You will often find CASE statements for the conversion of state machines. The CASE statement queries every possible state. The conditions are queried for the individual states within the respective areas. If the condition is fulfilled, the action is executed and the state variable is adapted. To increase readability, the state variable is often mapped as ENUM.
TYPE E_States : ( eWaiting := 0, eHasCoin, eProductEjected, eCoinEjected ); END_TYPE
Thus, the first variant of the state machine looks like this:
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
A quick test shows that the FB does what it is supposed to do:
However, it quickly becomes clear that larger applications cannot be implemented in this way. The clarity is completely lost after a few states.
Sample 1 (TwinCAT 3.1.4022) on GitHub
Second variant: State transitions in methods
The problem can be reduced if all state transitions are implemented as methods.
If a particular event occurs, the respective method is called.
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
Depending on the current state, the desired state transition is executed in the methods and the state variable is adapted:
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
This approach also works perfectly. However, the state machine remains in only one function block. Although the state transitions are shifted to methods, this is a solution approach of structured programming. This still ignores the possibilities of object orientation. This leads to the result that the source code is still difficult to extend and is illegible.
Sample 2 (TwinCAT 3.1.4022) on GitHub
Third variant: The state pattern
Some OO design principles are helpful for the implementation of the State Pattern:
Cohesion (= degree to which a class has a single concentrated purpose) and delegation
Encapsulate each responsibility into a separate object and delegate calls to these objects. One class, one responsibility!
Identify those aspects that change and separate them from those that remain constant
How are the objects split so that extensions to the state machine are necessary in as few places as possible? Previously, FB_Machine had to be adapted for each extension. This is a major disadvantage, especially for large state machines on which several developers are working.
Let’s look again at the methods CustomerTakesCoin(), CustomerTakesProduct(), InsertCoin() and PressButton(). They all have a similar structure. In IF statements, the current state is queried and the desired actions are executed. If necessary, the current state is also adjusted. However, this approach does not scale. Each time a new state is added, several methods have to be adjusted.
The state pattern scatters the status to several objects. Each possible status is represented by a FB. These status FBs contain the entire behavior for the respective state. Thus, a new status can be introduced without having to change the source code of the original blocks.
Every action (CustomerTakesCoin(), CustomerTakesProduct(), InsertCoin(), and PressButton()) can be executed on any state. Thus, all status FBs have the same interface. For this reason, one interface is introduced for all status FBs:
FB_Machine aggregates this interface (line 9), which delegates the method calls to the respective status FBs (lines 30, 34, 38 and 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
But how can the status be changed in the respective methods, the individual status FBs?
First of all, an instance within FB_Machine is declared by each status FB. Via FB_init(), a pointer to FB_Machine is transferred to each status FB (lines 13 – 16).
Each single instance can be read by property from FB_Machine. Each time an interface pointer to I_State is returned.
Furthermore, FB_Machine receives a method for setting the status,
METHOD INTERNAL SetState : BOOL VAR_INPUT newState : I_State; END_VAR THIS^.ipState := newState;
and a method for changing the current number of products:
METHOD INTERNAL SetProducts : BOOL VAR_INPUT newProducts : UINT; END_VAR THIS^.nProducts := newProducts;
FB_init() receives another input variable, so that the maximum number of products can be specified in the declaration.
Since the user of the state machine only needs FB_Machine and I_State, the four properties (CoinEjectedState, HasCoinState, ProductEjectedState and WaitingState), the two methods (SetState() and SetProducts()) and the four status FBs (FB_CoinEjectedState(), FB_HasCoinState(), FB_ProductEjectedState() and FB_WaitingState()) were declared as INTERNAL. If the FBs of the state machine are in a compiled library, they are not visible from the outside. These are also not present in the library repository. The same applies to elements that are declared as PRIVATE. FBs, interfaces, methods and properties that are only used within a library, can thus be hidden from the user of the library.
The test of the state machine is the same in all three variants:
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;
The statement in line 15 is intended to simplify testing, since a readable text is displayed for each state.
Sample 3 (TwinCAT 3.1.4022) on GitHub
This variant seems quite complex at first sight, since considerably more FBs are needed. But the distribution of responsibilities to single FBs makes this approach very flexible and much more robust for extensions.
This becomes clear when the individual status FBs become very extensive. For example, a state machine could control a complex process in which each status FB contains further subprocesses. A division into several FBs makes such a program maintainable in the first place, especially if several developers are involved.
For very small state machines, the use of the state pattern is not necessarily the most optimal variant. I myself also like to fall back on the solution with the CASE statement.
Alternatively, IEC 61131-3 offers a further option for implementing state machines with the Sequential Function Chart (SFC). But that is another story.
In the book (Amazon Advertising Link *) Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, Richard Helm, Ralph E. Johnson and John Vlissides, this is expressed as follows:
Allow an object to change its behavior when its internal state changes. It will look as if the object has changed its class.
A common interface (State) is defined, which contains a method for each state transition. For each state, a class is created that implements this interface (State1, State2, …). As all states have the same interface, they are interchangeable.
Such a state object is aggregated (encapsulated) by the object whose behavior has to be changed depending on the state (Context). This object represents the current internal state (currentState) and encapsulates the state-dependent behavior. The context delegates calls to the currently set status object.
The state changes can be performed by the specific state objects themselves. To do this, each status object requires a reference to the context (Context). The context must also provide a method for changing the state (setState()). The subsequent state is passed to the method setState() as a parameter. For this purpose, the context offers all possible states as properties.
Based on the example above, the following assignment results:
|State1, State2, …||FB_CoinEjectedState, FB_HasCoinState,|
|GetState1, GetState2, …||CoinEjectedState, HasCoinState,|
A TCP communication stack is a good example of using the state pattern. Each state of a connection socket can be represented by corresponding state classes (TCPOpen, TCPClosed, TCPListen, …). Each of these classes implements the same interface (TCPState). The context (TCPConnection) contains the current state object. All actions are transferred to the respective state class via this state object. This class processes the actions and changes to a new state if necessary.
Text parsers are also state-based. For example, the meaning of a character usually depends on the previously read characters.
18 thoughts on “IEC 61131-3: The State Pattern”
Good Day, Mr. Henneken.
It will be easier to implement State pattern using SFC language, isn’t it?
SFC is actually simpler and more intuitive in some use cases.
Personally, I prefer ST as I come from the C++ and C# range.
A further way to visually create Statechart is TwinCAT’s UML Statechart (SC) Editor.
Thanks for illustrating the different ways to implement state machines. When the size of the system grows, usually the best approach is to implement a generator that reads specifications and produces the code for state machines in IEC 61131-3 automatically.
This way the difficulty to extend directly the code, its clarity and unreadability are not anymore concerns for engineers defining automation. Also the produced code follows always precisely the selected approach (selection made by generator developer, like the ‘state’ pattern!) and the development speed is greatly improved.
Hello Juha-Pekka couldyou explain what is the “generator” that reads specifications and produces the code??
Is there any chance of getting your examples in a PLCOpenXML format? I am working with Native CoDeSys, as opposed to TwinCat. I would like to implement the State pattern for asynchronous file operations on another brand of PLC.
I am very pleased that you want to take my example as the basis for your project.
You can download TwinCAT 3 from the homepage of the Beckhoff Company for free and than you can export the building blocks you need in PLCOpenXML.
Or you send me your email address via LinkedIn and I send you the example in PLCOpnenXML.
When I run the example of the OOP approach, I see nested instances of ipState, and fbCoinEjectState, fbHasCoinState, fbProductEjectedState, fbWaitingState. This nesting goes more then 5 levels deep. Is this correct behaviour?
Each state is represented by an FB. Each of these FBs implements the interface I_State. Also, each state has a pointer to the State Machine (fbMachine). This pointer allows each state to change to a different state. The current state is stored in ipState. Where ipState is a reference pointer to the interface I_State. But I only recognize 3 levels. FB_Machine, the FBs for the states, where each of the states again contains a pointer on fbMachine.
Thank you for your great tutorials. I have used this pattern from your blog with great success. I have a small change that may be of interest. Instead of using SetState() with Property accessors for the states, I have taken to creating methods like EnterErrorStart() or EnterStartingState(). I can call then call these from my states.
Thx for your feedback. Good idea to use a method instead of a property.
Thank you for sharing all this information, I really enjoy reading your blog!
I have a PLC program which is built like the first variant, with multiple select case statements, and I would like to rewrite it to the Object Oriented state pattern.
I am wondering if this could have a negative effect on the cycle time. This will depend on the application and the controller, but do you have any information in general about the differences in effectivity of the two variants?
I would not expect that the cycle time is much higher. When using inheritance, the execution time is only slightly higher because methods are not called directly. In OOP mehods will be call via virtual tables. In the article ‘IEC 61131-3: Methods, Properties and Inheritance’ I briefly discussed this topic.
Really great example, thanks for showing the graded changes, its been really helpful to show people how you move from one programming style to another. The only thing i changed was passing an interface to the machine class rather than a pointer.
But i wonder how you handle cyclic code, for example if we used this pattern on an axis, and we have a state waitingforEnable, we trigger the endbale then wait for it to enable before moving to enabled state. you need to coninuously call the state object, which would mean you need your case statement back in the machine class to call the relevant state object.
I think you could also insert some clause in the states so they didnt execute if the machine wasnt in its state, but that feels wrong.
How would you handle this?
You are right. It could happen that in my example the case statement appears again in FB_Machine. Thank you for your comment. When introducing the State Machine Pattern, I always want to introduce the basics of the pattern. In a real application, it is often necessary to adjust the design patterns.
Do you like to present your solution approach in a short example. You could upload your example to GitHub and put the link to your solution here in a comment. I would be very happy to get to know other solutions for the state machine pattern under TwinCAT.
Hello @Stefan Henneken,
Could you make the TCP communication stack as an example of the state pattern? So that everything is clearer and we have a concrete and applicable example…
thanks you, Víctor.
Hallo @Stefan Henneken
deine Veröffentlichungen sind sehr hilfreich. Bei dieser verstehe ich nicht ganz den “Overhead” von vielen letztlich nicht implementierten Funktionen. Die eigentliche Umschaltung des Zustandes findet ja in der Logik des FB_Machine statt – dies kann ja ziemlich umfangreich sein. D.h. dort sind Variablen, Methoden etc. die innerhalb des Zustands – FBs regelgerecht angewandt werden müssen – somit sehe ich nicht die erwähnte Entkopplung für z.B. mehrere Entwickler.
Oder ich habe es nicht wirklich verstanden..
vielen Dank für deinen Kommentar und deine Anmerkung zu der State Machine.
Der Vorteil der gezeigten State Machine liegt darin, dass jeder Zustand durch einen eigenen Funktionsblock abgebildet wird. Somit kann jeder Zustand unabhängig voneinander bearbeitet werden. Du hast Recht mit der Anmerkung, dass die Umschaltung zwischen den einzelnen Zuständen weiterhin an einer Stelle (in FB_Machine) liegt. Hier könnte eine weitere Aufteilung auf verschiedene Bausteine von Vorteil sein. Was den Overhead angeht, so muss immer abgewogen werden, ob eine State Machine für den jeweiligen Anwendungsfall das richtige Mittel ist. Gerade bei sehr einfachen Anwendungen ist ein CASE vielleicht doch die bessere Wahl. Die Gefahr des Overengineering besteht auch bei allen anderen hier gezeigten Design Pattern oder auch bei den SOLID-Prinzipien.