Part 5 - Structures and functions (IEC 61131-3)

Video

#PLC #TwinCAT3 #Beckhoff #IEC_61131-3

🔙 Previous Part | Next Part 🔜

↩️ Go Back

Table of Contents:


A) Objectives

In the previous part we, amongst many other things, looked at arrays. Arrays allow us to define types of variables that can hold several data items of the same kind.

In this part of the tutorial we will look at a data unit type that allows us to hold several data items of DIFFERENT kind. The Structure.

We will also look at one of the basis of modularization and re-use in software development called functions.

We will look at how we can get data in and out of functions and we will also look at the difference between passing parameters by value and by reference.

We will finish this part by writing our very first function.


B) TUTORIAL 7 - Structures

First, let's look at a neat way to organize data.


B.1) Example - Making an Alarm and Event System

Suppose you wanted to implement an alarm and event system in your PLC software, so that you can trigger warnings, errors, information or status messages.

To keep track of these alarms and messages, you would need to have a data definition of such a message.

One single event could for example have the following data:

1. The event type:
|250

For example:

2. Event severity:
|300

so in the case of an alarm, examples could be:

3. Event identity:
|250

For example:

4. Event text:
|250

For example:

5. And a timestamp:
|280

Now assume we want to keep track of what has happened in our system over time. This would mean that we would need to store several instances of events. One way of implementing this would be to store this data in arrays.
|400

We could create several arrays, each and one holding every piece of information and every array being indexed from 1 to the amount that we want to be able to store, for example 100.

This is not very user-friendly though.

Although data might be declared this way, it might not be obvious that the data elements are related to each other.

Just imagine if this data would be used at several places in the software. Then this definition would have to be updated on every single place in the software to be consistent.

We also have 5 separate arrays to iterate over when we want to use the complete data for an event.

Just as we can have arrays to hold data items of the same kind, we can use something called structures to structure data of DIFFERENT kinds.


B.2) What is a Structure?

Structures is a user defined data type available in IEC 61131-3 that allows us to combine data items of different types.

Structures are used to represent a record or logical group.

If we want to achieve what we did in the previous slide of storing 100 records of events, we now only need to create one array of 100 instances of this struct.
|400

In this way all data is now grouped into one single array instead of five arrays with different data types.

To access a structures components, you use the following syntax.
|400

In other words, you write the name of the structure, a dot, and the component inside of the structure.

In this example we are accessing the event text and we are storing it in another string variable.
|400

Copy the PLC Code:

TYPE ST_Event :
STRUCT
	eEventType : E_EventType;
	eEventSeverity : TcEventSeverity;
	nEventIdentity : UDINT;
	sEvenText : STRING(255);
	dtTimestamp : DATE_AND_TIME;
END_STRUCT
END_TYPE
PROGRAM MAIN
VAR
	aEvents : ARRAY[1..100] OF ST_Event;
	sTempString : STRING(255);
END_VAR

-----------------------------------------------------------------------

sTempString := aEvents[33].sEventText;,


B.3) How to create a Structure?

To create a new structure data unit type, right-click on DUTs, select Add, next select DUT.
|400

Enter the name of the DUT, in this case ST_Event and make sure that "Structure" is selected.

This will create the STRUCT so that we can fill the members of the struct.

Just as with enumerations, the scope of structures is global which means that all the code in your project will see the structure definition.

You can't do like in for example C++ and create a local structure within a class.


C) TUTORIAL 8 - Functions

Now we will look at functions, which are used to perform certain actions and they are important for reusing code.

Define the code once, and use it many times.

C.1) Example 1 - Making an Celsius to Fahrenheit function

|400

A function consists of:

Assume we have a program and we want to use this function.

We need a variable to store our result, in this case the fFahrenheit variable.
|400

Next, we simply call the function and assign it the value that we want to convert to Fahrenheit.
If we did this call with the value of 5 degrees Celsius, we would get the value 41 Fahrenheit

Copy the PLC Code:

FUNCTION Celsius_To_Fahrenheit : REAL
VAR_INPUT
	fCelsius : Real;
END_VAR
VAR

END_VAR
----------------------------------------------------------------------
Celsius_ToFahrenheit := fCelsius * 1.8 + 32.0;
PROGRAM MAIN
VAR
	fFahrenheit : REAL;
END_VAR

-----------------------------------------------------------------------

fFahrenheit := Celsius_To_Fahrenheit(fCelsius := 5.0);


C.2) Example 2 - Making a comparison function with multiple outputs (Greater/Smaller than function)

There are also ways to have more than one output value from a function.

In this case we have a function that takes in two numbers, nNumber1 and nNumber2 and returns the greater and smaller of the two.
|400

Note that the actual implementation of the function is very simple. We simply look whether nNumber1 is greater than nNumber2 and if so assign nNumber1 to nGreater, and if not, we do it the other way around.

To use a function like this, we first need to declare two variables in order for us to have somewhere to store the greater and smaller value. In this case the two variables nReturnGreater and nReturnSmaller.
|400

Next we call the function by its name Greater_Smaller and we provide it with the two numbers we want to evaluate

Note the way to assign output values from a function to variables. We use =>, which is the assignment operator for outputs. You can learn more here.
|400

Now the two variables will have the correct values.

Copy the PLC Code:

FUNCTION Greater_Smaller
VAR_INPUT
	nNumber1 : INT;
	nNumber2 : INT;
END_VAR
VAR_OUTPUT
	nGreater : INT;
	nSmaller : INT;
END_VAR
----------------------------------------------------------------------
IF nNumber1 > nNumber2 THEN
	nGreater := nNumber1;
	nSmaller := nNumber2;
ELSE
	nGreater := nNumber2;
	nSmaller := nNumber1;
END_IF
PROGRAM MAIN
VAR
	nReturnGreater : INT;
	nReturnSmaller : INT;
END_VAR

-----------------------------------------------------------------------

Greater_Smaller(nNumber1 := 8,
				nNumber2 := 15,
				nGreater => nReturnGreater,
				nSmaller => nReturnSmaller);


C.3) Example 3 - Swap Numbers function (read and write to the same variable)

Functions can also have variables that are inputs and outputs at the same time This means that we can both read and write from and to the same variable.

These are declared with the VAR_IN_OUT keyword.

Let's assume we write a function SwapNums that swaps two numbers with each other.
|400

And we use it in out main program, so in this case we have the two variables nA with value 5 and nB with value 15 and we want to swap the variables with each other.
|250

If we provide these two numbers to the function, they will first go in as input parameters. Once in the function, we can write to the variables just like output parameters. To swap the numbers, we need to create a temporary variable called nNumberTemp, and store the nNumber1 in it. Next, we store the value of nNumber2 in nNumber1. Finally we store the value of nNumberTemp in nNumber2.
|400

Now we have successfully swapped the values of the variables.

Copy the PLC Code:

FUNCTION SwapNums
VAR_IN_OUT
	nNumber1 : INT;
	nNumber2 : INT;
END_VAR
VAR
	nNumberTemp : INT;
END_VAR
----------------------------------------------------------------------
nNumberTemp := nNumber1;
nNumber1 := nNumber2;
nNumber2 := nNumberTemp;
PROGRAM MAIN
VAR
	nA : INT := 5;
	nB : INT := 15;
END_VAR

-----------------------------------------------------------------------

SwapNums(nNumber1 := nA,
		 nNumber2 := nB);


C.4) How to create a Function

To create a function, right-click on POUs, select Add, Select POU.
|400

Write the name of the function and fill in the data type of the return value. Make sure that structured text is selected.

This will create a shell for your function and now you can fill in the implementation.


C.5) Pass by value or by reference

When passing values into your function, you can do so either by value or by reference.


C.5.1) Pass by value vs Pass by reference (ChangeColor function example)

Let's assume we have a function called ChangeColor, and it takes some sort of object as input parameter, in this case a colored circle.

1. Pass by value:

When you pass a parameter to a function by value it means that the changes you make to that parameter inside the function will only be affected while inside that function. There will be no effect on the original data that is stored in the argument value. In essence, when you pass by value, you are sending a copy of the variable into the function. The copied variable can get changed inside of the function; however, the value of the original data will remain preserved.
|200

2. Pass by reference:

On the other hand, when you pass a parameter to a function by reference the changes you make to that parameter inside the function will change the original data. When you pass by reference, you are passing the memory location of the variable to the function and any changes you make inside a function will affect the location in memory and will therefore persist after the function completes.
|200

Let's look at some examples of how it could look like when we are passing parameters by value and by reference.


C.5.2) Pass by value vs Pass by reference (UpdateEventTimestampWithSystemTime function example)

1. Pass by value:

Let's imagine we are writing a function UpdateEventTimestampWithSystemTime which takes our structure event that we previously defined as an input parameter,
|400

Remember, this is the input:
|400

and changes the DATE_AND_TIME field of it to the current system time.

In this example we are providing the structure by value, and thus a copy is made internally in the function.

To return a structure with a modified DATE_AND_TIME value, we need to provide an output parameter, here called stEventOut.
|400

First we would need to copy the complete content of the input parameter to the output parameter and next we would set the timestamp of the output structure to the system time of the PLC.
|400

As you can see this is quite inefficient as the input parameter stEvent is a copy of the original structure and then we are making an additional copy by creating an output parameter.

2. Pass by reference:

We can pass the value by reference instead, and then we are passing the memory location of the variable instead, so no copying is made.This can be achieved by either using the VAR_IN_OUT keyword for passing the parameter OR alternatively, provide the parameter as a REFERENCE TO type
400

By providing the value as reference, we can write the date_and_time timestamp directly into the structure.
400

When should we use pass by value and when should we use pass by reference?

By definition, pass by value means you are making a copy in memory of the parameters value that is passed in a copy of the contents of the actual parameter.
|120

The penalty for this can get quite extensive if the data that has to be copied is big.

We can draw some rules, and to the rules there will sometimes be exceptions, but three low hanging fruits are the following:

  1. Pass by value when the function does not want to modify the parameter and the value is easy to copy so for example ints, floats, bools, etc... So by easy to copy I mean that the parameter is small, at most a few WORDs

  2. Pass by reference when the function does not want to modify the parameter and the value is expensive to copy, so for example our structure event example

  3. Pass by reference when the function does want to modify the parameter and the value is expensive to copy


D) TUTORIAL 9 - Structure & Function Application ("UpdateEventTimestampWithSystemTime" - fill the DATE_AND_TIME value of the Structure with the current system time of the PLC) //FILE -->TimeProject (part1)

Now we have learned what structures and functions are good for.

Let's now use our new knowledge to create something useful. Why not implement the function "UpdateEventTimestampWithSystemTime", which will do exactly what the function name is called that is, it will take our structure, and fill the date_and_time value of the structure with the current system time of the PLC.

Remember, this is the input:
|400

D.1) Step1 - Making an Alarm and Event STRUCTURE with ENUMERATIONS

Ok so what I've done here is that I've created a completely new TwinCAT project and I've called it TimeProject.
|500

Before we create the function we of course need to define the structure, so we:

  1. create a DUT
  2. Select structure,
  3. and call it ST_EVENT. (ST in front of structures is just a naming convention that Beckhoff uses)

|400

|250

Now let's just fill the structure with the data that we previously talked about, the different data fields that we want in our event structure.

1. The event type:

The first one we wanted was the eEventType. Which just defines whether it's an alarm or a message. So it's of type E_EventType.

Note: This enumeration (E_EventType) is not created, so we need to create that.

So let's create the enumeration using DUTs.

E_EventType can be either an alarm or message, finally Save & close.

TYPE E_EventType :
(
	Alarm := 0,
	Message := 1
);
END_TYPE

2. Event severity:

The structure also needs to hold an EventSeverity. So if it's an alarm, it will define whether it's a very critical error or less critical error.

Note: For this we don't need to make up our own, there is actually one pre-defined in TwinCAT that is used for for something that Beckhoff calls the EventLogger, it is basically already an application programming interface setup for creating events in TwinCAT 3.

This enumeration is called TcEventSeverity.
|500

We can actually check it out by going to the definition.
|500

And you see that this one is also an enumeration and you have VERBOSE, INFO, WARNING, ERROR or CRITICAL.
|500

3. Event identity:

Next we need some form of event identity, so just a number.

This of course can mean whatever, it's up to you to define what it is. For example:

And this can be just a number so we will do it UDINT
|600

4. Event text:

Next we need an event message, or event text, and this is just a STRING where we can write 'too high pressure' or just some message. We make it a STRING with 255 characters (which is a good size because that's the maximum size that a lot of build-in functions in TwinCAT 3 can handle).
|600

5. And a timestamp:

And then we need the date and time, so we need the date and time of when this event was created so we call this dtTimestamp. And it's of type DATE_and_TIME
|600

So now we have our event.

TYPE ST_Event :
STRUCT
	eEventType : E_EventType;
	eEventSeverity : TcEventSeverity;
	nEventIdentity : UDINT;
	sEvenText : STRING(255);
	dtTimestamp : DATE_AND_TIME;
END_STRUCT
END_TYPE

D.2) Step2 - Making the function "UpdateEventTimestampWithSystemTime"

Next we need to create our function.

And to create our function we go to POUs, Add POU,

Then select Function, and we call it UpdateEventTimestampWithSystemTime
|250

And the return type... actually we're not going to have any return type. Because this one is going to take the structure by reference and we will write directly to that structure.

But... we NEED to define always a return type in TC3. This is a silly thing, so you can just write 'blablablabla'. And then you can just remove the return type afterwards.
|600

Then as an input we need to have the event so I just call it stEvent which is of type ST_Event (which is just the structure that we just defined).

And we take it in as a REFERENCE TO.
|600

Note: if we didn't pass it by reference it would make a local copy here in the function which we don't need.

And we want to update this timestamp (dtTimestamp : DATE_AND_TIME) with the current system time of the PLC.

To do that we will use our friend Google and check what existing functions exist already in TwinCAT for this purpose. Googling getsystemtime twincat


D.2.1) Step2.1 - Using the GETSYSTEMTIME Function Block

Ok so this is a function block, (I'm going to get into what a function block is in the next episode of this tutorial). Right now we're only going to use it.

We need to instantiate it to use it. You do that here, in your local variables.
|500

You instantiate it in this way. You create an instance of this function block.

And then if we call the function block,
|500

we get this timeLoDW and timeHiDW. It's just two parts of a timestamp. For some reason Beckhoff decided to split into two 32-bit values. So it's 8 bytes for the whole timestamp.

Note: So this only gives us the system time in some weird format, so we're probably going to have to convert the system time into a DATE_TIME so hopefully there is going to be another function for this.

Now we need to store this value somewhere. Right now we just save these values into two local variables. So we just call them nTimeLow and nTimeHigh,
|500

Now we need to look into some function to convert that into a DT (date_and_time)

What I usually do is ... ok let's go back to Google (googling date_and_time systemtime twincat {INCORRECT})


D.2.2) Step2.2 - Using FILETIME_TO_DT Function Block

OK.... (after some reading and corrections)

So FILETIME is what we have from the GETSYSTEMTIME Function Block.

It's two 32-bit values. And this can be converted into a DATE_AND_TIME using the function FILETIME_TO_DT.

So what we're going to do is that we're going to take this value that we got from SYSTEMTIME and we're going to store it inside a this type T_FILETIME which is a structure. So let's instantiate this structure. We're going to create a T_FILETIME and just call it stFileTime.

Using the "Utilities" Library

Ok and here is the next problem. This structure T_FILETIME exists in a library called "Utilities"

And when you create a new project of TwinCAT you don't have that library included as a reference.

And here you can add all the libraries that you have dependencies to.

Note: Normally you get the STANDARD, SYSTEM and MODULE. These are just the very basic stuff that you need in order to be able to compile a TwinCAT program.

But we need UTILITIES. So what we're going to do is right-click on references, add library,
|300

search for "utilities" (Tc2_Utilities)
|400

Now we got T_FILETIME to work.
|500

Now we can actually double-click on it and check.
|500

You see you have the structure, and it exists in Tc2_Utilities

We have the structure and now instead of using the two variables we created, we can remove them (we don't need them), because we are going to store the systemtime inside the structure to convert the filetime to DATE_AND_TIME.

|500

So by calling fbGETSYSTEMTIME we'll get the system time from the function block GETSYSTEMTIME that Beckhoff or someone else has already done for us, so we don't need to worry too much about how this is working.

We could look at the documentation for it and see what it does and everything.

But we don't actually need to know exactly how it is working, we just need to know what this particular function block provides for us.

And we store the data that this function block provides into our T_FILETIME structure.

Next we need to convert this filetime into a DATE_AND_TIME.

This we do by using the function FILETIME_TO_DT,

Its output of DATE_AND_TIME, we can store it inside the structure that we provided by REFERENCE (stEvent)

And now we can run this function and see whether we get what we want.

Copy the PLC Code:

FUNCTION UpdateEventTimestampWithSystemTime
VAR_INPUT
	stEvent : REFERENCE TO ST_Event;
END_VAR
VAR
	fbGETSYSTEMTIME : GETSYSTEMTIME;
	stFileTime : T_FILETIME;
END_VAR

------------------------------------------------------------------
// The function block writes on stFileTime (VAR_OUTPUT) in T_FILETIME format 
fbGETSYSTEMTIME(timeLoDW => stFileTime.dwLowDateTime,
				timeHiDW => stFileTime.dwHighDateTime);

// Overwrites the Timestamp of stEvent using the FILETIME_TO_DT function
stEvent.dtTimestamp := FILETIME_TO_DT(fileTime := stFileTime);

Note: In Structured Text, function calls can have parameters provided in two ways:

  1. Positional Parameters:

    The parameters are provided in a positional manner.

  2. Named Parameters:

    The parameters are provided using named notation. This way of passing parameters is particularly useful when the function has multiple parameters, and you want to ensure clarity and readability in the code.


D.3) Step3 - Running the function "UpdateEventTimestampWithSystemTime"

First we are going to create an instance of this structure.

Note:

But for now this simple example we'll just create one single event and we'll fill it with the data from this function .

And then, to fill it, we need to call our new function, and then we provide the event that we just created

Remeber: This is how our function works,

Now what's going to happen is this one is going to get called at every cycle so this event will just get a new timestamp all the time.

Maybe we don't want that. Maybe we just want it to be filled once, so we'll do that.

So we'll just create a very simple boolean to run it only once.

So again, this is how it works:

  1. we call the function that we just created (UpdateEventTimestampWithSytemTime).
  2. We provide the function with our event (stThisIsOurFirstIntstanceOfAStructure).
  3. The function takes the event as an input, but the input is passed as a REFERENCE to. So no copying is done. It's just provided a REFERENCE of it into the function.
  4. And then we use a Beckhoff function GETSYSTEMTIME to get the system time of the PLC.
  5. We store the output of this function block into a structure called T_FILETIME
  6. Then we need to convert it into a date_and_time, which we do with FILETIME_TO_DT
  7. And then we store the value in the timestamp of our structure

EASY PEASY! Right?
Copy the PLC Code:

PROGRAM MAIN
VAR
	stThisIsOurFirstInstanceOfAStructure : ST_Event;
	bBoolean : BOOL := FALSE;
END_VAR

------------------------------------------------------------------
IF NOT bBoolean THEN
	UpdateEventTimestampWithSystemTime (stEvent := stThisIsOurFirstInstanceOfAStructure);
	bBoolean := TRUE;
END_IF

Let's build this and compile!

Ok, so let's run it

Yes! There we go! It ran only once and the timestamp was filled with the right time

Note: the time is probably in UTC or something like this, so it might be different than the one on you laptop. But the day is correct and the adjustment can be made for different timezones.

Final Thoughts:
So it worked! You might have noticed that there is a lot of stuff happening here and this is usually what happens when you develop software, right? You don't quite know how to solve the problem but that's the nice thing... I mean this is one of the things I love with writing software. You know where you want to get to... you have a vision, you have a picture in your head of where you want to get to... But you don't know HOW. That's what so fun, that's the absolutely best thing with software development, the learning process... to just go around, look for information, do some thinking, and solve the problem. It's just a very enjoyable feeling.

Once you get stuff to work. And again, you never have all the answers in advance, that's the great thing, it's just a constant discovery. This was a very simple example but you'll quickly see that once you get to work with real projects the complexity is going to skyrocket and things are going to get really interesting.

That's it for this part! We managed to create our first structure and a function that's actually quite useful In the next episode we will look at one of the building blocks of object oriented programming, the function block See you in the next part!


E) Summary

In this part we learnt about Structures and Functions and the diference between passing by value and passing by reference. Also, we made our first useful function using all that we know and included some libraries and function blocks.

The functions that we made were:


E.1) Example 1 - Making an Celsius to Fahrenheit function

FUNCTION Celsius_To_Fahrenheit : REAL
VAR_INPUT
	fCelsius : Real;
END_VAR
VAR

END_VAR
----------------------------------------------------------------------
Celsius_ToFahrenheit := fCelsius * 1.8 + 32.0;
PROGRAM MAIN
VAR
	fFahrenheit : REAL;
END_VAR

-----------------------------------------------------------------------

fFahrenheit := Celsius_To_Fahrenheit(fCelsius := 5.0);


E.2) Example 2 - Making a comparison function with multiple outputs (Greater/Smaller than function)

FUNCTION Greater_Smaller
VAR_INPUT
	nNumber1 : INT;
	nNumber2 : INT;
END_VAR
VAR_OUTPUT
	nGreater : INT;
	nSmaller : INT;
END_VAR
----------------------------------------------------------------------
IF nNumber1 > nNumber2 THEN
	nGreater := nNumber1;
	nSmaller := nNumber2;
ELSE
	nGreater := nNumber2;
	nSmaller := nNumber1;
END_IF
PROGRAM MAIN
VAR
	nReturnGreater : INT;
	nReturnSmaller : INT;
END_VAR

-----------------------------------------------------------------------

Greater_Smaller(nNumber1 := 8,
				nNumber2 := 15,
				nGreater => nReturnGreater,
				nSmaller => nReturnSmaller);


E.3) Example 3 - Swap Numbers function (read and write to the same variable)

FUNCTION SwapNums
VAR_IN_OUT
	nNumber1 : INT;
	nNumber2 : INT;
END_VAR
VAR
	nNumberTemp : INT;
END_VAR
----------------------------------------------------------------------
nNumberTemp := nNumber1;
nNumber1 := nNumber2;
nNumber2 := nNumberTemp;
PROGRAM MAIN
VAR
	nA : INT := 5;
	nB : INT := 15;
END_VAR

-----------------------------------------------------------------------

SwapNums(nNumber1 := nA,
		 nNumber2 := nB);


E.4) Useful Function - "UpdateEventTimestampWithSystemTime" - fill the DATE_AND_TIME value of the Structure with the current system time of the PLC

Diagram:

E.4.1) DUTs

  1. Enum
TYPE E_EventType :
(
	Alarm := 0,
	Message := 1
);
END_TYPE
  1. Structure
TYPE ST_Event :
STRUCT
	eEventType : E_EventType;
	eEventSeverity : TcEventSeverity;
	nEventIdentity : UDINT;
	sEvenText : STRING(255);
	dtTimestamp : DATE_AND_TIME;
END_STRUCT
END_TYPE

Remember:
|500

Diagram:


E.4.2) POUs

  1. Function
FUNCTION UpdateEventTimestampWithSystemTime
VAR_INPUT
	stEvent : REFERENCE TO ST_Event;
END_VAR
VAR
	fbGETSYSTEMTIME : GETSYSTEMTIME;
	stFileTime : T_FILETIME; // requires Tc2_Utilities
END_VAR

------------------------------------------------------------------
// The function block writes on stFileTime (VAR_OUTPUT) in T_FILETIME format 
fbGETSYSTEMTIME(timeLoDW => stFileTime.dwLowDateTime,
				timeHiDW => stFileTime.dwHighDateTime);

// Overwrites the Timestamp of stEvent using the FILETIME_TO_DT function
stEvent.dtTimestamp := FILETIME_TO_DT(fileTime := stFileTime);

Remember:
A) GETSYSTEMTIME FB
|300

B) T_FILETIME Structure as output of the FB
|500

C) Function to convert FILETIME to DATE_AND_TIME (DT)
|250

  1. Main
PROGRAM MAIN
VAR
	stThisIsOurFirstInstanceOfAStructure : ST_Event;
	bBoolean : BOOL := FALSE;
END_VAR

------------------------------------------------------------------
IF NOT bBoolean THEN
	UpdateEventTimestampWithSystemTime (stEvent := stThisIsOurFirstInstanceOfAStructure);
	bBoolean := TRUE;
END_IF


🔙 Previous Part |Next Part 🔜

↩️ Go Back


Z) Glossary

File Definition