Object-oriented programming (OOP) is a proven way of keeping the complexity of software systems in check. Until recently the preserve of languages such as C++, Java and C#, IEC 61131-3 introduces the concept to PLC programming.
Previously, a function block consisted of internal, input and output variables. There was only one opportunity to modify internal variables from outside the function block. Instances of the function block were called with the relevant input variables. Different sections within the function block would be executed depending on the value of these input variables, which in turn affected the value of internal variables. For example:
fbEngine(bStart := true, bStop := false, nGear := 1, fVelocity := 7.5);
fbEngine(bStart := false, bStop := true);
Code within the function block listens for the values of bStart and bStop to switch from FALSE to TRUE. The function block user needs to know that bStart needs to be set to FALSE when stopping the engine. Failure to do so would mean that there would be no switch from FALSE to TRUE on attempting to restart the engine. The user also needs to know that they need to specify a value for the nGear parameter when starting the engine (but not when stopping it).
The OOP extensions introduce a clearer separation between internal variables and the ability to modify them. It is now possible to define additional sub-functions (methods) within a function block, which are available to be called by the user.
Methods are comparable to actions, except that parameters can be passed to a method when it is called. Just like functions, methods can also contain local variables. Using methods, the above example can be realised thus:
fbEngine.Start(nGear := 1, fVelocity := 7.5);
The Start() and Stop() methods can access internal function block variables, but can also contain variables which can neither be modified from the outside nor be modified by other methods within the function block. This ensures that they cannot be inadvertently overwritten.
Methods can also include a return value, which is returned to the calling entity. If required, it is also possible to declare additional output variables between VAR_OUTPUT and END_VAR. Since TwinCAT 3, functions have also had this capability. Again since TwinCAT 3, it has also been possible to declare functions and methods with no return value.
Only FUNCTION_BLOCK and PROGRAM POUs can contain methods – this option is not available to FUNCTION POUs.
When calling a method, all parameters must be specified. Parameter names can be omitted if required:
Where parameter names are used, parameters can be given in any order:
fbEngine.Start(fVelocity := 7.5, nGear := 1);
In the graphical languages, methods are depicted using a separate box:
Some programming languages offer the ability to define multiple methods with the same name. Which method is executed is determined by the parameters specified when the method is called (signature). This is known as overloading. Currently, methods in IEC 61131-3 cannot be overloaded. It is not possible to differentiate between methods using signatures. Method names must be unique.
The method declaration may include an optional access specifier. This restricts access to the method.
|PUBLIC||The method can be called by anyone – there are no restrictions.|
|PRIVATE||The method is available from within the POU only. It cannot be called from outside the POU.|
|PROTECTED||Only its own POU or POUs derived from it can access the method. Derivation is discussed below.|
|INTERNAL||The method is accessible from within the same namespace only. This allows methods to be available from within a certain library only, for example.|
|FINAL||The method cannot be overwritten by another method. Overwriting of methods is described below.|
The default setting where no access specifier is defined is PUBLIC.
A method declaration therefore has the following structure:
METHOD <Access specifier> <Name> : <Datatype return value>
Previously, the only way of passing parameters was to call a function block with input variables. State information was returned to the calling entity via output variables. Properties offer a defined way of passing general parameters and state information outside of the function block call.
Properties are distinguished by the fact that access is via a pair of special methods. There is one method for writing and one for reading the property. These methods are designated setters (write) and getters (read). Together they are referred to as accessors. These accessor methods can also perform function such as range checking or unit conversion.
Setters and getters can contain local variables, but no additional inputs or outputs. On exiting the setter or getter method, the values of these local variables are, however, lost. In this respect they behave like functions. The setter therefore needs to provide an appropriate mechanism to ensure that the value of the property is preserved. This can, for example, be achieved by declaring a local variable in the function block.
FUNCTION_BLOCK PUBLIC FB_Engine VAR myPropertyInternalValue : LREAL := 50; END_VAR
The setter method assigns the value of the MyProperty property to the local variable.
myPropertyInternalValue := MyProperty;
The getter does the opposite and reassigns the value of the local variable to the property.
MyProperty := myPropertyInternalValue;
The local variable myPropertyInternalValue can be used within the function block for internal calculations. Access to this variable is exclusively via the MyProperty property.
Let us add three further properties to the above example, one to specify the maximum permissible velocity (MaxVelocity) and two more to output the current temperature (Temperature) and velocity (Velocity).
In the graphical languages, properties are not shown on the relevant box. To access a property, we use the name of the instance and of the property separated by a dot.
fbEngine(); bError := fbEngine.Temperature > 130;
There does not have to be both a getter and a setter. In the absence of a setter, for example, the property will be read-only. In TwinCAT 3, the function block definition, including methods and properties, looks like this:
As with methods, properties can also take the following access specifiers: PUBLIC, PRIVATE, PROTECTED, INTERNAL and FINAL. Where no access specifier is defined, the property is PUBLIC. In addition, an access specifier can also be specified for each setter and getter. This takes priority over the property’s own access specifier.
A property declaration therefore has the following structure:
PROPERTY <Access specifier> <Name> : <Datatype>
Function blocks are an excellent means of keeping program sections separate from each other. This improves software structure and significantly simplifies reuse. Previously, extending the functionality of an existing function block was always a delicate undertaking. This meant either modifying the code or programming a new function block around the existing block (i.e. the existing function block was effectively embedded within a new function block.) In the latter case, it was necessary to create all input variables anew and assign them to the input variables for the existing function block. The same was required, in the opposite direction, for output variables.
TwinCAT 3 introduces the concept of inheritance. Inheritance is one of the fundamental principles of object-oriented programming. Inheritance involves deriving a new function block from an existing function block. The new block can then be extended. To the extent permitted by the parent function block’s access specifiers, the new function block inherits all properties and methods from the parent function block. Each function block can have any number of child function blocks, but only one parent function block. Derivation of a function block occurs in the new function block declaration. The name of the new function block is followed by the keyword EXTENDS followed by the name of the parent function block. For example:
FUNCTION_BLOCK PUBLIC FB_NewEngine EXTENDS FB_Engine
The new, derived function block (FB_NewEngine) possesses all of the properties and methods of its parent (FB_Engine). Methods and properties are, however, only inherited where the access specifier permits.
The child function block also inherits all local variables, VAR_INPUT, VAR_OUTPUT, and VAR_IN_OUT from the parent function block. This behaviour cannot be modified using access specifiers.
If methods or properties in the parent function block have been declared as PROTECTED, the child function block (FB_NewEngine) is able to access them, but they cannot be accessed from outside FB_NewEngine.
Inheritance applies only to POUs of type FUNCTION_BLOCK.
FUNCTION_BLOCK, FUNCTION or PROGRAM declarations can include an access specifier. This restricts access and, where applicable, the ability to inherit.
|PUBLIC||Anyone can call or create an instance of the POU. In addition, if the POU is a FUNCTION_BLOCK, it can be used for inheritance. No restrictions apply.|
|INTERNAL||The POU can only be used within its own namespace. This allows POUs to be available from within a certain library only, for example.|
|FINAL||The FUNCTION_BLOCK cannot serve as a parent function block. Methods and properties in this POU cannot be inherited. FINAL is only permissible for POUs of type FUNCTION_BLOCK.|
The default setting where no access specifier is defined is PUBLIC. The access specifiers PRIVATE and PROTECTED are not permitted in POU declarations.
If you plan to utilise inheritance, the function block declaration will therefore have the following structure:
FUNCTION_BLOCK <Access specifier> <Name> EXTENDS <Name basis function block>
The new FUNCTION_BLOCK FB_NewEngine, which is derived from FB_Engine, can contain additional properties and methods. For example, we can add the property Gear. This property can be used to query and change the current gear. Getters and setters for this property need to be set up.
However, we also need to ensure that the nGear parameter from the Start() method is passed to this property. Because the parent function block FB_Engine does not have access to this new property, a new method with exactly the same parameters needs to be created in FB_NewEngine. We copy the existing code to the new method and add new code so that the nGear parameter is passed to the property Gear.
METHOD PUBLIC Start VAR_INPUT nGear : INT := 2; fVelocity : LREAL := 8.0; END_VAR IF (fVelocity < MaxVelocity) THEN velocityInternal := fVelocity; ELSE velocityInternal := MaxVelocity; END_IF Gear := nGear; // new
Line 12 copies the nGear parameter to the Gear property.
Where a method or property which is already present in the parent function block is redefined within the child function block, this is referred to as overwriting. The function block FB_NewEngine overwrites the Start() method.
FB_NewEngine therefore has the new property Gear and overwrites the Start() method.
calls the Start() method in FB_NewEngine, since this method has been redefined (overwritten) in FB_NewEngine.
calls the Stop() method from FB_Engine. The Stop() method has been inherited by FB_NewEngine from FB_Engine.
In addition to inheritance, another fundamental property of object-oriented programming is polymorphism (Greek for ‘having many forms’). This means that a variable can take different data types depending on how it is used. Previously, variables were always assigned a type. Polymorphism always occurs in the context of inheritance and interfaces.
We will illustrate polymorphism using the above example with inheritance. We create one instance each of FB_Engine and FB_NewEngine. A reference is assigned to these instances depending on a variable (bInput).
PROGRAM MAIN VAR fbEngine : FB_Engine; fbNewEngine : FB_NewEngine; bInput : BOOL; refEngine : REFERENCE TO FB_Engine; END_VAR IF (bInput) THEN refEngine REF= fbEngine; ELSE refEngine REF= fbNewEngine; END_IF refEngine.Start(2, 7.5);
A variable of type REFERENCE TO FB_Engine can take an instance of type FB_Engine (line 10), but can also take all function blocks which are derived from FB_Engine – including therefore FB_NewEngine (line 12).
Line 14 then calls the Start() method. It is not possible to determine from this line alone whether the Start() method from FB_Engine or from FB_NewEngine will be executed.
This ambiguity is frequently used in object-oriented programming to make programs more flexible and expandable. To this end, the parameters for a function may be defined as references to a function block. All FBs which are derived from this function block can then be passed to this function.
Interfaces are also important in this context and this is explored in my post IEC 61131-3: Object composition using interfaces.
In the above example, we created a new Start() method in FB_NewEngine and copied the existing code from FB_Engine into the new method. This is not always possible, and it also runs counter to the principle of reuse.
Consequently, every function block which is derived from another function block has access to a pointer called SUPER. This can be used to access elements (methods, properties, local variables, etc.) from the parent function block.
METHOD PUBLIC Start VAR_INPUT nGear : INT := 2; fVelocity : LREAL := 8.0; END_VAR SUPER^.Start(nGear, fVelocity); // calls Start() of FB_Engine Gear := nGear;
Instead of copying code from the parent function block to the new method, the SUPER pointer can be used to call the method from the FB_Engine function block. This does away with the need to copy the code.
In the CFC editor, SUPER is called as follows:
The SUPER pointer always has to be written in upper case.
The THIS pointer is available to all function blocks and points to the current function block instance. This pointer is required whenever a method contains a local variable which obscures a variable in the function block.
An assignment statement within the method sets the value of the local variable. If we want the method to set the value of the local variable in the function block, we need to use the THIS pointer to access it.
nTest := 1; // changes the value of the local variable in the method THIS^.nTest := 2; // changes the value of the variable in the function block
As with the SUPER pointer, the THIS pointer must likewise always be upper case.
Effect of FINAL on performance
A method or POU declared with the access specifier FINAL is not able to act as a parent function block. All calls to its methods are direct. This has the effect that there is no longer any need for polymorphism. The compiler is able to take this into account during code generation and optimise the code accordingly. Depending on the application, this optimisation can have a significant effect at runtime. Here, an example:
A function block has two completely identical methods. The only difference is the access specifier. One method has been declared as PUBLIC, the other as FINAL. In a PLC task, first one and then a little later the other method is called.
IF (bSwitch) THEN FOR n := 1 TO 50000 DO fbTest.MethodFinal(0.534, 1.78, -2.43); END_FOR ELSE FOR n := 1 TO 50000 DO fbTest.MethodPublic(0.534, 1.78, -2.43); END_FOR END_IF
As we can see, the execution time changes significantly.
If the method declared as FINAL is called 50,000 times, the running time for the PLC task on my test device is about 6.9 ms. This rises to about 7.5 ms for the method declared as PUBLIC.
Of course, our example program is rather abstract, as it does almost nothing but call the methods. Nonetheless, this is worth taking into account when selecting an access specifier.
The inheritance hierarchy can be depicted diagrammatically. Unified Modelling Language (UML) is the established standard in this area. UML defines various diagram types which describe both the structure and behaviour of software.
A good tool for describing the function block inheritance hierarchy is the class diagram.
UML diagrams can be created directly in TwinCAT 3. Changes to the UML diagram have a direct effect on the POUs. Function blocks can thus be modified and amended via the UML diagram.
Each box stands for one function block and is always divided into three horizontal sections. The top section shows the name of the function block, the middle section lists its properties and the lower section lists all its methods. In this example, the arrows show the direction of inheritance and always point towards the parent function block.