Part 7 - Instructions in ST (IEC 61131-3)

Video

#PLC #TwinCAT3 #Beckhoff #IEC_61131-3

🔙 Previous Part | Next Part 🔜

↩️ Go Back

Table of Contents:


A) Objectives

In the last part about interfaces there was a lot of information to take in, especially for an introduction tutorial.

Now we will focus on some more basic stuff. In this part we will cover instructions which are used to control the flow of our programs.

|350

We'll go through four different instructions to control flow:

We will end this part by finishing the implementation of the persistent event logging (that we started in the previous part) which we'll do by using our newly acquired knowledge.

This is going to be fun, let's get started!


B) Conditional Statements (IF, ELSIF, CASE)

We will start with conditional statements. Very often when you write code you want to perform different actions for different decisions. You can use conditional statements in your code to do this.

Conditional statements are achieved by either the IF statements or CASE switches.

  1. Use IF to specify a block of code to be executed if a specified condition is true.
    An example of this is shown here.

We simply check for a condition and:

The condition can be any boolean expression such as checking if a bool is true or checking whether an integer is larger than a value or even checking whether a string has a certain length.

  1. Use ELSE to specify a block of code to be executed if the same condition is false.
    An example of this is shown here.

By adding the ELSE keyword to the above example we simply provide a path of execution if the evaluation is false.

  1. Use ELSIF to specify a new condition to test if the first condition is false.
    An example of this is shown here.

By adding the ELSIF keyword as shown in the example here we simply provide additional paths of execution.

You can have as many ELSIFs as you want

Copy the PLC Code:

IF condition THEN
    // block of code to be executed if condition is true
ELSIF another_condition THEN
    // block of code to be executed if the first condition is false and another_condition is true
ELSE
    // block of code to be executed if both condition and another_condition are false
END_IF

  1. Use CASE to specify many alternative blocks of code to be executed.
    The CASE statement is used to perform different actions based on a condition variable.
    The value of the variable is compared with the value of each case.
    An example of this is shown here.

In the above example the variable "variable" is checked against the value x, y or z

If there is a match the associated block of code is executed.
If there is no match the ELSE code block is executed.
The ELSE statement is optional

Looking at the above example the flow of execution will look like follows:
|450

1 - First we enter the case statement by checking the condition variable. This variable could for example be of the type WORD or INT.
Note: You can't however use all data types, for example strings are not supported. For that you would need to use an IF and ELSIF instruction

2 - Next we check whether the variable has the constant value x.  
So if the variable was an INT this could be a check of any number (say 42).
If the value of the variable equals x then we execute the code block of x.

3 - If not, we check if the variable equals the value y. If it does we execute the code block of y.

4- If not, we check the next constant value in this case z. Again if the variable we are checking matches the value z, we execute the code block of z.

5 - If not, we execute the default code which is everything in the ELSE instruction.

Copy the PLC Code:

CASE variable OF
    x:
        // block of code to be executed if variable equals x
    y:
        // block of code to be executed if variable equals y
    z:
        // block of code to be executed if variable equals z
    // ... add more cases as needed ...
    ELSE
        // block of code to be executed if variable does not equal any of the above
END_CASE

Note

Note that instead of CASE instructions you can also use the IF and ELSIF instructions to achieve the same functionality. Although for cases where you want to test for many values of a variable the CASE-switch is generally preferred.


C) FOR Loops

FOR loops can execute a block of code a number of times. They are handy when you want to run the same code over and over again each time with a different value.

This is often the case when working with arrays...

Let's assume you are calculating the sum value of an array of size 5.

Without a FOR loop it could look like something like this example.
|400

Just imagine how this would look like if we had an array with a size of 100.

Instead you could write a FOR loop.
|400

In the FOR loop we use a variable in here called "nCounter" that is assigned a value at the start, in this case the value 1.

The FOR loop is executed up until it reaches the value 5.

The FOR loop automatically increments the variable and counter by 1 at the end of the FOR loop.

The step size defined by using the keyword BY is optional and if it's not defined it will implicitly be one.

Copy the PLC Code:

VAR
    nIndex : INT;
    nSum : INT;
    aValues : ARRAY[1..10] OF INT;
END_VAR

nSum := 0; // Initialize sum to zero before starting the loop

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

FOR nIndex := 1 TO 10 BY 1 DO
    nSum := nSum + aValues[nIndex]; // Add the value at the current index to the sum
END_FOR


D) WHILE Loops

The WHILE-loop loops through a block of code as long as a specified condition is TRUE.

If we take the same example as in the previous instruction where we want to sum all the elements of an array,

the equivalent WHILE-loop would look like this:
|450

The WHILE-loop consists of a test expression.
|450

This test expression could be anything.
For example:

As long as the expression is true, the WHILE-loop will continue to execute.

In this case as long as "nCounter" is less than 6 it will execute everything in the WHILE-loop.

As soon as "nCounter" will have the value 6 the expression will no longer be true and we will exit the WHILE-loop.

The flowchart would look like this:

Copy the PLC Code:

VAR
    nSum : INT;
    nCounter : INT;
    aArray : ARRAY[1..5] OF INT;
END_VAR

nSum := 0; // Initialize sum to zero
nCounter := 1; // Initialize counter to 1

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

WHILE nCounter <= 5 DO
    nSum := nSum + aArray[nCounter]; // Add the current array element to the sum
    nCounter := nCounter + 1; // Increment the counter
END_WHILE

E) TUTORAL 15 - Implementation of a Persistent CSV Event Data Storage FB //FILE -->TimeProject (part4)

Ok it's time to go back to a little bit of programming and we'll continue where we left off in the last part.

This is what we have from our TimeProject so far...
Full Diagram:

So now, we will actually be implementing the function block that implements this interface,

Diagram:
|500

Remember, it IMPLEMENTS an Interface:
|500

Remember that this interface only had one method, that was called StoreEvent(), which basically just passes an event that is to be stored persistently.

But now we're going to actually implement this to persistent storage.

Important Note: We will do a bit of changes on what we promised earlier...
XML can be a little cumbersome to use at a beginner level. So, we're going to make it a little bit easier for us and implement a CSV logger instead. So we will generate a comma separated value file.

So we'll have to rename this to CsvPersistentEventStorage.

|450

It's still implementing the same interface. The only difference is instead of storing the data in an XML file we're going to store the data into a CSV file instead.

New Diagram:
|500


E.1) Implementing the FB for CSV File Storage using State Machines

We will implement the function block FB_CsvPersistentEventStorage that implements the interface I_PersistentEventStorage that we defined in the previous part of this tutorial.

|500

The FB_CsvPersistentEventStorage function block will write our events into a comma separated value file.

E.1.1) Defining our State Machine

To do this we need to define a State Machine that consists of several steps to achieve our goal.

State Machines are very common in the world of PLC-programming and you will come across them many times.

First we need to think about what this function block is supposed to do and what steps we need to implement.

Step 1: Open File
Our first step is to open the file that we want to write to.
This is accomplished using the built-in function block FB_FileOpen.
|300

This will either open an existing file or if the file does not exist create it and open it.

Step 2: Wait Until Event Added
The second step in the state machine is to wait until the function block receives an event from outside.

|300

The event will be added by a call of a function block that has a reference to the interface I_PersistentEventStorage.

|300

This function block will call the StoreEvent() method which will provide all the necessary data for our CSV writer function block to continue to the next step.

Step 3: Write Event to File
In the third step we will write the actual event data to the file.
Every time we get a new event we will create a new line in the file for that event.

Writing a line of text is accomplished using the FB_FilePuts function block.
|300

Step 4: Close File?
In the last step we could close the file using the function block FB_FileClose.
|300

This is however unnecessary to do as we can just keep the file open all the time because we know that we will want to write more events, we will save some time and resources by not having to open and close the file every time an event is added.

Instead as soon as we are finished with writing an event to file we will just immediately go back to waiting for a new event.

So, our State Machine would look something like this,


E.1.2) STEP 1 - Creating the local eWriteState Enumeration

To implement this function block,

we need a couple of Beckhoff functions.

And the first one is that we need to be able to open a file.
|300

So, let just Google it and see what options we already have,

We can see we have the function block FB_FileOpen.

Note: make sure you are using the TwinCAT 3 documentation, and not TC2

So what we're going to do now, is to use a State machine to be able to store these CSV entries.

And to do this we're gonna create an "enumeration". The enumeration will define the state machine.

Important Note - Best Practices:

PLC programmers like to define state machine using CASE Statements like this...

This format is used quite often, not just by PLC programmers, but also if we look at all the Beckhoff examples you see this quite often.

And I would advise you NOT to do this, I think it is quite bad for Beckhoff to use this as example code because then new PLC developers see the example and think this is how it should be done as well.

The reason this is bad is because this is just "Magic Numbers". The number 10 doesn't say anything about what's happening, the 20 doesn't say anything either, you should avoid using magic numbers in your code, because you only create confusion for anyone readying your code for the first time. You should try to make your code describe as much as possible when you develop code.

What we will use instead...

Instead of using that format for enumeration, we're going to use something called the Local Enumerations and it's basically an Enumeration that's only visible in this particular function block so it has very local scope.

As I talked about Enumerations previously in Part 4 - Data types & arrays (IEC 61131-3), the enumerations are visible all over the whole project. While here they're only local so we're going to call this enumeration eWriteState and then we can define all the steps here.

Our ENUM will contain these Steps to Write Events to a CSV File:

  1. Open File:

    • Begin by opening the desired file.
    • This is essential as we will be writing CSV comma-separated values into it.
    • Each new event will be recorded as a new line in this file.
    • We'll refer to this stage as the "FILE_OPEN" state.
  2. Wait for Event:

    • Once the file is open, we won't write until there's an event to record.
    • This stage ensures that we only write meaningful data.
    • We'll remain in the "WAIT_FOR_EVENT" state until an event occurs.
  3. Write the Event:

    • On detecting an event, we will transition from the waiting state.
    • The event details will be written to the CSV file.
    • This stage is termed the "WRITE_EVENT" state.
  4. Close File (Optional):

    • After recording the event, the file can be closed.
    • This step may not be essential, as the file can remain open for continuous logging.
    • Consider this as a safety measure or for specific use-cases in the future.
    • This stage is termed the "FILE_CLOSE" state.

We're going to use the CASE switch over this enumeration.

I like these enumerations because they're only visible into this function block and they're not going to pollute the Namespace. We don't need them to be visible for anywhere else because all of this functionality is specific for this CSV persistent event storage function block.

You want to keep it as local as possible and not just pollute your stuff all over.
Now we're gonna define this...

E.1.3) STEP 2 - Instantiating the fbFileOpen to open the File

We're gonna do now FILE_OPEN. We are gonna use this function block FB_FileOpen

Link here

Note1: make sure you are using the TwinCAT 3 documentation, and not TC2.
Note2: this FB is available in the Tc2_System namespace.

This function block describes basically everything we need.

And that's what we need, so we're gonna use, so we instantiate our FB_FileOpen.

Just call it "fbFileOpen" and we can already assign some of these parameters.

Input 1 - sNetId
The first parameter is this sNetId. We want to open the file locally on the PLC, so we'll just leave this as default. If you don't put any AMSNetId you will use the the local PLC that is the PC that we're running this on.

Input 2 - sPathName
The next parameter is sPathName, the path name is interesting because this is the path to the file. So this one we can set and this is what we write...

Input 3 - nMode
Then nMode describes how the file should be opened, so we don't want to just read to it. We want to be able to write and append. So you have these modes,

and the way you do it is you operate them with an OR operation to combine them.

So, we are using:

FOPEN_MODEWRITE

OR
FOPEN_MODEAPPEND

OR
FOPEN_MODETEXT

Ok. I think that's it...

A small mistake was made

We only need FOPEN_MODEAPPEND, using both FOPEN_MODEWRITE OR FOPEN_MODEAPPEND will create an Error

Other Inputs
and for the other parameters we can just use the default values,


E.1.4) STEP 3 - Instantiating the fbFilePuts to write in the File

Okay so we need this function block now "FB_FilePuts".

The function block writes strings into a file....
(That sounds good. Right? We want to write a single line)
...The string is written to the file up to the null termination but without the null character.
The file must have been opened in text mode.
(And we have opened it in text mode, that's where we use the FB_FileOpen)

So this guy is what we want so we're gonna instantiate that function block as well,

and we're gonna assign some default parameters.

Link here

Input 1 - sNetId
The sNetId we leave it at default that's going to be localhost.

Input 2 - hFile
The hFile that's a handle to the file that we have opened, it is a unique identifier that we must know to use any file function block. And the handle to the file that we have opened will be provided as an output from the FB_FileOpen.

So if you look at definition of the FB_FileOpen,

You'll see that one of the outputs is this "File handle",

So that's like a "memory location" somewhere in the computer that provides you a handle to this file.

So this we don't want to do during declaration, this we want to do during runtime because we don't have it during the declaration. We will get the handle during runtime.

Input 3 - sLine
So sLine is the string to be written. So this we don't have either because this will be our event.

Other Inputs
"bExecute" we don't have right now, "tTimeout" we can leave at default so we leave everything at default which means we'll just do like this.

E.1.5) STEP 4 - Opening the File with fbFileOpen (using Triggers)

We're gonna start with opening the file.

|300

This we do by calling the function block "fbFileOpen",

And when we do that... every time you have a function block that has this "bExecute flag" you need to start this function block.

The way you have to think about it is:

Because what's happening now is that you're basically requesting a command to Windows to open this file (because we're still dependent on Windows, because all this file handling is done in Windows).

|300

And Windows is not running in Real time (like TwinCAT does) so we don't know exactly when will Windows do what we asked. So we need to have this asynchronous communication between TwinCAT and Windows so you set a rising edge trigger to start to tell Windows "Hey Windows I want you to open this file" and then we need to continuously call it until this file is actually opened.

I mean this might take one millisecond so it might even happen in the same cycle as this call to this function block but it might take 10 seconds. We don't know that's why we need to continuously call this FileOpen until the output bBusy is not true, so it's not busy anymore and we don't have any errors.

What we need to do is to split this FILE_OPEN into two steps.

It's one step that's FILE_OPEN_TRIGGER the function block and then FILE_OPEN just wait until the file is actually opened.

Note

And again you're gonna meet this type of stuff all the time when you have Asynchronous communication.

For example:
It doesn't have to be Windows stuff like this non-real-time stuff. It can also be that you're sending a command to a robot "please move there please to move to this position". If you have some motion function block.

Of course this is gonna happen independently of the PLC cycle time so that might take one second. It might take minutes if you have a filter wheel or something rotating it might take many minutes before it completes so then you need the state machine that where you say "please start now" and then you just wait for it t o actually finish.

So we're gonna split this FILE_OPEN into:

Note: It's going to be the same thing you'll see later with the WRITE_EVENT and FILE_CLOSE but we will do that later.

So we split the FILE OPEN on the CASE switch also,

So in FILE_OPEN_TRIGGER we just trigger the function block by bExecute true. That sets our rising edge trigger to this to execute this function block and then we just need to wait until it has actually opened the file

and we do that by FILE_OPEN with bExecute false,

so we don't need to send this bExecute to be TRUE every cycle. It's enough to do it in one cycle and then we need to wait for the result.

E.1.5.1) Error detection (from FB_FileOpen)

What we want to do now is to check whether we basically got an error or not whether it was successful or not.

And the way you do this is...

if we go back to FB_FileOpen, we need these outputs...

So we want to check whether it's busy or not. If it's not busy and doesn't have an error then everything is fine. If it's busy or has an error then we should wait.

So first of all we want to check if this FB has an error,

because if it has an error then that can be many reasons for why it has an error.

It might be that this file doesn't exist, or maybe we don't have access to this hard drive or many reasons....

THEN we need to have some error handling,

and right now I'm not going to make some very advanced error handling we're actually going to add our ERROR state step,

and if we end up in the ERROR step we're not going to do anything.

In a full program you of course want to have some sort of error handling either notify the operator or have a red blinking light or handle it. Maybe do a retry....

Right now we're gonna do nothing we're just gonna log in here and see if we actually end up here.

E.1.6) STEP 5 - Preparing to Write into the File

We say that THEN the "eWriteState" is ERROR and then we go to ERROR

Note: We forgot by the way that once we have triggered the first one we need to go to the next step, so we need to set that eWriteState is equal to FILE_OPEN,

E.1.6.1) Check if file is busy and has File Handle

And next check is ELSIF fbFileOpen is not busy...
If it's not busy it means that it was hopefully successful so we actually also need to check whether we actually have received a handle to the file. This we do by making sure that the handle to the file is just an unsigned integer not equals to zero then we know everything is fine,

then we can wait for the event (this is the situation we want to end up in),

Okay if we are waiting for an event, we don't do anything...

What we're going to do instead is that we're going to have some code that puts us out of this once we have received an event.

And what we're going to do is, do it when someone calls the store event here:

Remember: in the last part we ended up with just creating this "stub method" this method is part of the interface.

And here is where we're actually going do the code.

First what we're gonna do is to check that if our current write state is equal to WAIT_FOR_EVENT

THEN we're gonna put the eWriteState to WRITE_EVENT.

A small mistake was made

The eWriteState should actually be equal to WRITE_EVENT_TRIGGER

We will see more about this when we update the state machine.

Copy PLC Code:

METHOD StoreEvent
VAR_INPUT
	stEvent	: ST_Event;
END_VAR
----------------------------------------------------------------
// Here is where the CSV-writing will be done

// Are we ready to Write? 
IF eWriteState = WAIT_FOR_EVENT THEN
	// Go to next step
	eWriteState := WRITE_EVENT_TRIGGER;
	// Take Event to Write from Input
	stEventWrittenToFile := stEvent;
END_IF

Diagram:
|500

So, if we receive an event and we are waiting for one then we just want to put the state machine into please write this event.

And I should notice something here....There is a problem with this is of course...

With this implementation we can only write one event at a time

Note: if I would to do this properly for a customer or for any real machine, I would create some sort of circular buffer so you can just add as many events as you want up to the size of a buffer and then you would have the function block just go through with this buffer.

In this one we're going to keep it simple because I have a feeling it's going to be complicated enough, so we can only have one event at a time. So, what this means is if we have an event that's being written and someone else calls StoreEvent() at the same time then we're not going to be here and then we're going to lose the event and this of course in a real scenario would have to have code to handle this.

We are not done yet, we actually need to store this Event somewhere so that the function block FB_CsvPersistentEventStorage can process it.

So I'm gonna just gonna call it stEventWrittenToFile and that's an ST_Event.

When we receive this event we're gonna store it.

And that means that if this StoreEvent method is called then we will automatically go to WRITE_EVENT and then we're down here.

Okay fun! then we're gonna use this fbFilePuts. That's where we actually gonna write the event.

E.1.7) STEP 6 - Converting ST_Event to a CSV String (T_MaxString)

There's two things we need to do first.

The event is just a structure right now, so everything is just in this memory location of the PLC,

but we want to put it down as a string. Like a long line of text data.

But we'll start with just calling fbFilePuts and trigger it with bExecute equals TRUE

So we're gonna have to do it the same way as we did with FILE_OPEN_TRIGGER so we need to have a WRITE_EVENT_TRIGGER, and we need to have a WRITE_EVENT where we actually just wait for the event to be actually written because it's the same as with opening a file.

Writing to a file it's done in Windows so it's asynchronous so it can take one millisecond but it can take several seconds for the file to be actually written, you have no control whatsoever when stuff is done in Windows because it's not deterministic.

Windows can decide to do something completely different that's why we need to have this communication, this messaging, (or whatever you want to call it).

I'm just going to rename to WRITE_EVENT_TRIGGER...
and then we need to have a WRITE_EVENT,

Notice that you can just look at the code and then it's obvious what's happening in each in one of these states instead of having a number.

I just mentioned that if we're gonna write the data then we need to convert this structure into some string that we can just write ,because FB_FilePuts...

...doesn't want a structure. What it wants is just a string of type T_MaxString.

Note: T_MaxString is just an alias. We haven't talked about alias, but an alias is just another name for something and T_MaxString is just a string of size 255.
So it means we can at max write a string that's not bigger than 255 characters.

So, we need to actually convert that and what we're going to do is that we're going to create a helper method for this so we're going to create a method that takes this structure of ST_Event and converts it into our CSV string so with commas and everything.

So we're going to add a method,

and we can call it CreateCsvString().


And it's returning T_MaxString because that's what the what the FilePuts wanted and it can be PRIVATE because no one else outside of this function block needs to have access to this method.

And as input it's going to have a REFERENCE TO an ST_Event.

First of all we need to create a temporary variable to work with.

And for that we're gonna use TempString (that's also a T_MaxString).

And this event,

which consists of the event type, the event severity and some other data. We have to convert all of these into strings...

so first of all the temp string is gonna be the E_EventType.

But to convert this there's actually a trick. You can use something called an attribute TO_STRING

And what you can do then is...

So what will happen now is that if for example the event is Message

then we're just gonna get a string that's where it's written Message.

Note

To make this work, you need to include the attribute to the Enum

It's super convenient so you don't have to create any special conversion functions from enumeration to a string, TwinCAT will do this automatically for you if you add this attribute (this pragma).

We start with that and then the next thing we do is we add a comma to this string. And what you do it by concatenating and for that there's a very convenient function,

With this you provide two strings and out you get another string (for example, with SUSI and WILLI you will get SUSIWILLI)

And what we want to concatenate is...we want to add this enumeration with a comma so we add,

And this is how we're gonna create a string, we're gonna add field by field by field and between every field we're gonna have a comma.

So we just continue.

Next, we are doing the TcEventSeverity.

This one is a TwinCAT type2, which doesn't have the string attribute, so we just simply store the integer value, but that's fine.

So again this "temporary string", we just use it to build up the string field by field so it just grows and grows and grows until we get the full the complete string with all the data that we're going to write into this file.

Now we have to add another comma so we can just copy from the previous line,

Next stuff is the Event identity, it is just a unsigned double integer,

We convert it to a STRING, and then we want another comma after that,

Then we want the Event text,

And you can already see a thing here. The sEventText string of 255 and we are saving it inside a T_MaxString which is also a string of 255...

So this could potentially be something unfortunate, if we get a sEventText that is too big, we could run out of memory from our T_MaxString.

We're not going to do anything fancy right now but as a warning you should be aware of this potential problem, it is an additional possibility of failure

Note

Professionally we never write code like this. What is usually done is something that's called "Test Driven Development". But that is something we will learn at the advanced part of this tutorial.

And then you generally take care of all these very fuzzy use cases, these dangerous use cases before you actually write the code. It just generally gives much better architecture.

Okay so we continue to concatenate it, and we add the coma,

And then we only have the DATE_AND_TIME.

Which is the dtTimestamp and then we want to do a TO_STRING because that's of DATE_AND_TIME type.

Note: I don't know exactly how the string will look like when you convert a date and time into a string but probably something that's more or less human readable. We hope...

Finally, we want to add a new line at the end, so once we added a whole line of data we also want to make sure to add a new line characters so that we get a new line for the next event.

Otherwise all of these strings will just create a huge long infinitely long line in the file.

And to do this we can look for....

So basically the STRING constants are constants that you can use when you want to do something special with string.

So here we want a new line then we need this special character "$n"

and if you're a C developer you probably recognize this although it uses a "\n" instead

So we need to add the special character at the end of our temporary string,

Then finally we can return this to our METHOD, the return of this method is going to be the TempString so now we have a nice and neat helper method

Copy PLC Code:

METHOD PRIVATE CreateCsvString : T_MaxString
VAR_INPUT
	stEvent : REFERENCE TO ST_Event;	
END_VAR

VAR
	TempString : T_MaxString; 
	
END_VAR
----------------------------------------------------------------
TempString := TO_STRING(stEvent.eEventType);
TempString := CONCAT(STR1 := TempString, STR2 := ',');
TempString := CONCAT(STR1 := TempString, STR2 := TO_STRING(stEvent.eEventSeverity));
TempString := CONCAT(STR1 := TempString, STR2 := ',');
TempString := CONCAT(STR1 := TempString, STR2 := TO_STRING(stEvent.nEventIdentity));
TempString := CONCAT(STR1 := TempString, STR2 := ',');
TempString := CONCAT(STR1 := TempString, STR2 := stEvent.sEventText);
TempString := CONCAT(STR1 := TempString, STR2 := ',');
TempString := CONCAT(STR1 := TempString, STR2 := TO_STRING(stEvent.dtTimestamp));
TempString := CONCAT(STR1 := TempString, STR2 := '$n');

CreateCsvString := TempString; 

Diagram:
|500


E.1.8) STEP 7 - Passing the CSV String (T_MaxString) to be written by sLine

Now we go back to the WRITE_EVENT_TRIGGER and what we want to provide to fbFilePuts is the STRING with the data, so first we want to create this STRING.

And this is the string that we want to provide to this function block (fbFilePuts).

So the sLine is equal to CsvString

A small mistake was made

We should not forget the File Handle

And now we just need to wait for it to actually be written.

So what we're going gonna do is we're gonna update the state machine to move to WRITE_EVENT.


E.1.9) STEP 8 - Call FilePuts and Write the Event to File

Now we just have to call the FilePuts until there's either an error or until it's done.

So we just call bExecute equals to FALSE.

So that's everything....

Now we're going to do it exactly the same way as the FILE_OPEN

First, we will just check whether we have an error,

THEN we want to put the state machine into ERROR

Otherwise, if fbFilePuts is not busy (remember the busy flag will be high as long as we're actually writing to this file).

So, if we're not busy then everything is fine

And we are gonna be a little brave now, and actually not go to FILE_CLOSE.

We are just going to go back immediately into WAIT_FOR_EVENT. So we write our line, our string, and then we go back to WAIT_FOR_EVENT

I don't see any reason right now why we would need to close the file. I might change my mind very soon but right now I can't see a reason for it. It doesn't make sense to open and close the file all the time you can just open it once when TwinCAT starts and then we can leave it  open.

And that's about it for this function block!

Spoiler Alert: In the end, we will realize that we need to close the File. Once we start testing and troubleshooting we will learn why.


E.1.10) STEP 9 - Implementing the FB_CsvPersistenteEventStorage in the MAIN Program

What we need to do now is in our main program, we need to create an instance of this.

And now we're actually use CSV persistent event storage,

and we provide this through dependency injection into the event logger.

Remember, the event logger (as we saw in the previous part) through dependency injection has a reference to an interface I_PersistentEventStorage.

And that's exactly what we provide and in this case we provide it to a CSV writer.

But again it could be anything else. It could be:

Now what we also need to do is to actually call this function block because the actual writing of the data is done through calling this function block.

So that's what we do. We call it.

By calling it we know that all of this is called cyclically, every PLC cycle and then the state machine takes care for everything else for us. Really neat.

Copy PLC code:

PROGRAM MAIN
VAR
	(* 
	  --------------- PART 1 ---------------------
	stThisIsOurFirstInstanceOfAStructure : ST_Event;
	bBoolean : BOOL := FALSE; 
	  --------------------------------------------
	*)
	
	bBoolean : BOOL := FALSE; 
	
	// Initialize the variables needed to add an Event with sample values
	eEventType1 : E_EventType := E_EventType.Alarm; //user defined ENUM
	eEventSeverity1 : TcEventSeverity := TcEventSeverity.Error; // library ENUM
	nEventIdentity1 : UDINT := 42;
	sEvenText1 : STRING(255) := 'This is a sample event';
	
	// Create an instance of the FB that defines the format for Persistent Event Storage
	CsvPersistentEventStorage1 : FB_CsvPersistentEventStorage;
	
	// Create one instance of our FB EventLogger, and provide interface parameter for FB_init
	EventLogger1 : EventLogger(CsvPersistentEventStorage1); 
	
	
END_VAR

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

IF bBoolean THEN
	// Add an event (in one cycle of the PLC) using the AddEvent Method
	EventLogger1.AddEvent(eEventType1, eEventSeverity1, nEventIdentity1, sEvenText1); 
	bBoolean := FALSE;
END_IF

CsvPersistentEventStorage1();

Diagram:

Let's compile it. Build, rebuild solution

Okay it worked, it complied with no errors... what a surprise!

Let's recap how this works...

We created our test event,

and if we set this boolean online to true then we're gonna trigger an AddEvent

and again if we go back to the previous part the add event will add it into our event buffer, where we keep all our events,

and then we will actually call the interface StoreEvent()

which in turn will call the actual implementation CSV persistent storage which is this guy,

This is quite modular. Very nice!


E.1.11) STEP 10 - Run the code and troubleshoot

Let's activate the configuration. And see how it works,

Next what we can do is that we can log in,

and have the program running,

Okay we immediately got to ERROR so there's some error with fbFileOpen

And why do we have an error?

We see that the Error id is 1803,

Let me show you another trick, you have this ID in decimal, but you can right click display mode, and choose hexadecimal,

now we see it is 70b,

A small mistake was made

We only need FOPEN_MODEAPPEND, using both FOPEN_MODEWRITE OR FOPEN_MODEAPPEND will create an Error

so let's try again ...

Now it is working, we got the WAIT_FOR_EVENT so that's fantastic,

And what we're gonna do now is that we're gonna do a bBoolean this set to TRUE,

so let's write that

And we got into ERROR (again)...
so it means that the FilePuts Error for some reason, let's check why....

We missed a very very important thing. The fbFilePuts doesn't have any reference to a file, we are missing the file handle. We didn't provide it.

So, we need to provide it here.

That's how we link the fbFilePuts together with the fbFileOpen.

Let's try again...

Okay, so we see the state machine is at WAIT_FOR_EVENT,

And let's try again,

And there's no error, there's no busy and we're back at WAIT_FOR_EVENT !!!! That is good.

So, now let's see if we got the log written,

Let's open the file, and we see....

no event written. 😥 we are close, but no finished yet...

I have a slight feeling about this we might have to close the file, but what we can do meanwhile is that we can just turn off TwinCAT. That should "implicitly" close the file.

So we shut down the TwinCAT kernel. We'll put it into config mode.

Let's reopen the Events file, and there we go!!!

So we actually need to close the file for the line to be written to the file. For some reason....

But here we got it. This is the the data we put.


E.1.12) STEP 11 - Closing the File (using FileClose)

Now we know we should also close the file so what we're gonna do is that we're gonna instantiate FB_FileClose.

We leave several setting at default, so we need to remove the comment from FILE_CLOSE and just as with the other FILE_OPEN and FILE_WRITE, we need a FILE_CLOSE_TRIGGER, where we trigger the function block and then a FILE_CLOSE where we check that it's actually closed.

So we also add to our enumeration FILE_CLOSE_TRIGGER and FILE_CLOSE

When we have written the event we tell it to close the file,

And we need to instantiate fbFileClose

and then we call it. We set the rising edge trigger execute TRUE.

And we need to provide which file we want to close.

And then we set the eWriteState to FILE_CLOSE, where we verify that the file is actually closed.

Then we set bExecute to FALSE, this is the same as with the open and file write,

and then we check..

so we wait until the file close function block is not busy anymore which means if it's not busy it has successfully closed the file.

And now, if the file is closed then we need to go back and open it again. Which means basically means we'll start all the way from the beginning. So if it's successfully being closed then we open it again.

There might be another way to do it because I'm actually not entirely happy with TwinCAT not writing to the file immediately. There's probably a reason for it I can't come up with it now but we just do it this way.

We can always ask the Beckhoff support or StackOverflow.

Remember...

Remember that this current implementation has a problem if we call the StoreEvent METHOD, while being in another state different to the WAIT_FOR_EVENT, and that is bad becuase we lose the event so I would encourage you if you want to do some exercises to add a buffer, to here. So we can have several events at the same time that are written.

Copy PLC code:

FUNCTION_BLOCK FB_CsvPersistentEventStorage IMPLEMENTS I_PersistentEventStorage
VAR
	// Instantiate FB to create/open file for editing
	fbFileOpen : FB_FileOpen := (sPathName := 'C:\Events.log' , nMode := FOPEN_MODEAPPEND OR FOPEN_MODETEXT);
	
	// Instantiate FB to write into a file
	fbFilePuts : FB_FilePuts;
	
	
	// Instantiate FB to close the file
	fbFileClose : FB_FileClose;
	
	
	// create local ENUM for our State Machine
	eWriteState : (FILE_OPEN_TRIGGER, FILE_OPEN,
				                 WAIT_FOR_EVENT, 
			   WRITE_EVENT_TRIGGER, WRITE_EVENT, 
				 FILE_CLOSE_TRIGGER, FILE_CLOSE,
										 ERROR); 
	// Event to Write
	stEventWrittenToFile : ST_Event;
	
	// Variable to store the STRING to write into the file
	CsvString : T_MaxString; 	
	
END_VAR


------------------------------------------------------------------
CASE eWriteState OF
	FILE_OPEN_TRIGGER : 
		// send rising edge to request Windows to Open the File
		fbFileOpen(bExecute := TRUE);
		eWriteState := FILE_OPEN; // next step
	
	FILE_OPEN : 
		fbFileOpen(bExecute := FALSE);
		
		// Error Detection from FB OUTPUT
		IF fbFileOpen.bError THEN
			eWriteState := ERROR;
			
		// Check if we are ready to Open with Busy Flag and File Handle
		ELSIF NOT fbFileOpen.bBusy AND fbFileOpen.hFile <> 0 THEN
			eWriteState := WAIT_FOR_EVENT; // next step
			
		END_IF
	
	WAIT_FOR_EVENT :
		// Do nothing, we are waiting for the user to call us
	
	WRITE_EVENT_TRIGGER : 
		// Convert ST_Event into a CSV String (use method)
		CsvString := CreateCsvString(stEvent := stEventWrittenToFile);
		
		// send rising edge to request Windows to Write into the File
		// then send CSV String and pass the File Handle
		fbFilePuts(bExecute := TRUE,
					sLine := CsvString, 
					hFile := fbFileOpen.hFile);	
		
		eWriteState := WRITE_EVENT; // next step
	
	WRITE_EVENT : 
		fbFilePuts(bExecute := FALSE);
		
		// Error Detection from FB OUTPUT
		IF fbFilePuts.bError THEN
			eWriteState := ERROR;
		
		// Check if we are ready to write with Busy Flag 
		ELSIF NOT fbFilePuts.bBusy THEN
			eWriteState := FILE_CLOSE_TRIGGER; // next step
		END_IF
	
	FILE_CLOSE_TRIGGER : 
		// send rising edge to request Windows to Close the File
		// then pass the File Handle
		fbFileClose(bExecute := TRUE,
					hFile := fbFileOpen.hFile);	
		
		eWriteState := FILE_CLOSE; // next step
	
	FILE_CLOSE : 
		fbFileClose(bExecute := FALSE);
		
		// Error Detection from FB OUTPUT
		IF fbFileClose.bError THEN
			eWriteState := ERROR;
		
		// Check if we are ready to close with Busy Flag 
		ELSIF NOT fbFileClose.bBusy THEN
			eWriteState := FILE_OPEN_TRIGGER; // restart state machine
		END_IF
	
	ERROR :
		// Do nothing (for now)
	
END_CASE

Diagram:


E.1.13) STEP 12 - Final Testing

Let's see if this makes any difference.

Let's try again, remember...

Okay, so we see the state machine is at WAIT_FOR_EVENT,

And let's try again,

And there's no error, there's no busy and we're back at WAIT_FOR_EVENT !!!! That is good.

So, now let's see if we got the log written,

Let's open the file, and we see....

Yeah! Now it works! We got the line immediately you see we got the second line immediately.

Just to be absolutely sure let's change the Event,

Note: you have to close the file before you try to write a new line

And let's try again...

And it worked again!

Final Thoughts:
Okay there we go. Yeah so it seems to be working we simply have to close the file in between every time. And that's fine but I would say that this is working right now and yeah and that's basically it for the CSV persistent event storage function. And the good thing here is that we have been able to exercise the usage of state machines and you're going to get back to state machines a lot in the future.

I think this was a good way to exercise them and again I just want to remind you to try to use this local enumerations and try to keep the code as clean as you can... That was fun!

In the next part we will look into some of the powerful features that every IEC 61131-3 compliant PLC has. See you in the next video!

D) Summary

D.1) Conditional Statements (IF, ELSIF, CASE)

Copy the PLC Code:

IF condition THEN
    // block of code to be executed if condition is true
ELSIF another_condition THEN
    // block of code to be executed if the first condition is false and another_condition is true
ELSE
    // block of code to be executed if both condition and another_condition are false
END_IF

Copy the PLC Code:

CASE variable OF
    x:
        // block of code to be executed if variable equals x
    y:
        // block of code to be executed if variable equals y
    z:
        // block of code to be executed if variable equals z
    // ... add more cases as needed ...
    ELSE
        // block of code to be executed if variable does not equal any of the above
END_CASE

D.2) Loops (FOR, WHILE)

Copy the PLC Code:

VAR
    nIndex : INT;
    nSum : INT;
    aValues : ARRAY[1..10] OF INT;
END_VAR

nSum := 0; // Initialize sum to zero before starting the loop

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

FOR nIndex := 1 TO 10 BY 1 DO
    nSum := nSum + aValues[nIndex]; // Add the value at the current index to the sum
END_FOR

Copy the PLC Code:

VAR
    nSum : INT;
    nCounter : INT;
    aArray : ARRAY[1..5] OF INT;
END_VAR

nSum := 0; // Initialize sum to zero
nCounter := 1; // Initialize counter to 1

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

WHILE nCounter <= 5 DO
    nSum := nSum + aArray[nCounter]; // Add the current array element to the sum
    nCounter := nCounter + 1; // Increment the counter
END_WHILE


D.2) TimeProject (part4) Event logger and Interface for Persistent Storage

You can find the Github Repo here: https://github.com/Maurilio97-P/TC3-TimeProjectTutorial-PersistentEventLogger

D.2.1) DUTs

  1. Enum
{attribute 'qualified_only'}
{attribute 'strict'}
{attribute 'to_string'}
TYPE E_EventType :
(
	Alarm := 0,
	Message := 1
);
END_TYPE
  1. Structure
TYPE ST_Event :
STRUCT
	eEventType : E_EventType;
	eEventSeverity : TcEventSeverity;
	nEventIdentity : UDINT;
	sEventText : STRING(255);
	dtTimestamp : DATE_AND_TIME;
END_STRUCT
END_TYPE

Remember:

Diagram:


D.2.2) POUs

D.2.2.1) FUN
  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

B) T_FILETIME Structure as output of the FB

C) Function to convert FILETIME to DATE_AND_TIME (DT)

Diagram:

D.2.2.2) ITF
  1. Interface (FB_CsvPersistentEventStorage)
INTERFACE I_PersistentEventStorage

4.1 Public Method

METHOD StoreEvent
VAR_INPUT
	stEvent : ST_Event;
END_VAR

------------------------------------------------------------------
// There is no implementation here, it is just an establish requirement for any FB that implements the Interface

Diagram:
|500


D.2.2.3) FB
  1. Function block (EventLogger)
FUNCTION_BLOCK EventLogger
VAR_INPUT
	// inputs to the function block. 
	// These are read-only, meaning you cannot modify their values within the block.
END_VAR


VAR_OUTPUT
	// outputs from the function block. 
	// The function block can modify the values of these 
END_VAR


VAR
	// local to the function block and to its methods
	// internal (instance) variables
	
	aEventBuffer : ARRAY[1..MAXIMUM_SIZE_OF_EVENT_BUFFER] OF ST_Event;
	nCurrentEventBufferIndex : INT := 0;
	
	//Create local reference of our Interface
	// FB_EventLogger USES I_PersistentEventStorage
	_PersistentEventStorage : I_PersistentEventStorage;
	
END_VAR

VAR CONSTANT
	// local to the function block and to its methods
	// these variables cannot be changed during program execution.
	
	MAXIMUM_SIZE_OF_EVENT_BUFFER : INT := 100;
	
END_VAR
----------------------------------------------------------------
// code

5.1 FB_init Methods

METHOD FB_init : BOOL
VAR_INPUT
	bInitRetains : BOOL; // if TRUE, the retain variables are initialized (warm start / cold start)
	bInCopyCode : BOOL;  // if TRUE, the instance afterwards gets moved into the copy code (online change)
	
	// Making a reference to the Interface I_PersistentEventStorage
	// This Interface is for Storing Events Persistently	
	iPersistentEventStorage : I_PersistentEventStorage; 
END_VAR

------------------------------------------------------------------
// store reference locally in the FB when FB_init method executes
// this allows all methods in the FB to have access to it
_PersistentEventStorage := iPersistentEventStorage;

5.2 Private Method

METHOD PRIVATE IsEventBufferFull : BOOL


------------------------------------------------------------------
//Check if the event buffer is full
IF (nCurrentEventBufferIndex + 1) > MAXIMUM_SIZE_OF_EVENT_BUFFER THEN
	IsEventBufferFull := TRUE; //method returns TRUE, it is full
ELSE 
	IsEventBufferFull := FALSE; //method returns FALSE, it is not full
END_IF

5.3 Public Method

(* Returns TRUE if add was succesful and FALSE if buffer is full *)
METHOD PUBLIC AddEvent : BOOL
VAR_INPUT
	// user input
	eEventType : E_EventType; //user defined ENUM
	eEventSeverity : TcEventSeverity; // library ENUM
	nEventIdentity : UDINT;
	sEvenText : STRING(255);
	
END_VAR

------------------------------------------------------------------
//Check if the event buffer is full using a private method
IF NOT IsEventBufferFull () THEN
	
	nCurrentEventBufferIndex := nCurrentEventBufferIndex + 1;
	
	// from user input, fill buffer
	aEventBuffer[nCurrentEventBufferIndex].eEventType := eEventType;
	aEventBuffer[nCurrentEventBufferIndex].eEventSeverity := eEventSeverity;
	aEventBuffer[nCurrentEventBufferIndex].nEventIdentity := nEventIdentity;
	aEventBuffer[nCurrentEventBufferIndex].sEvenText := sEvenText;
	
	// using our function, fill the date and time stamp
	// Step1: input current event to the function (aEventBuffer[nCurrentEventBufferIndex])
	// Step2: updates ONLY date and type stamp of the input event
	UpdateEventTimestampWithSystemTime(stEvent := aEventBuffer[nCurrentEventBufferIndex]);
	
	AddEvent := TRUE; // add was succesful
ELSE
	AddEvent := FALSE; // buffer is full
END_IF

// Here we want to store the event persistently
_PersistentEventStorage.StoreEvent(stEvent := aEventBuffer[nCurrentEventBufferIndex]);

Diagram:


  1. Function Block (FB_CsvPersistentEventStorage)
FUNCTION_BLOCK FB_CsvPersistentEventStorage IMPLEMENTS I_PersistentEventStorage
VAR_INPUT
END_VAR
VAR_OUTPUT
END_VAR

VAR
	// Instantiate FB to create/open file for editing
	fbFileOpen : FB_FileOpen := (sPathName := 'C:\Events.log' , nMode := FOPEN_MODEAPPEND OR FOPEN_MODETEXT);
	
	// Instantiate FB to write into a file
	fbFilePuts : FB_FilePuts;
	
	
	// Instantiate FB to close the file
	fbFileClose : FB_FileClose;
	
	
	// create local ENUM for our State Machine
	eWriteState : (FILE_OPEN_TRIGGER, FILE_OPEN,
				                 WAIT_FOR_EVENT, 
			   WRITE_EVENT_TRIGGER, WRITE_EVENT, 
				 FILE_CLOSE_TRIGGER, FILE_CLOSE,
										 ERROR); 
	// Event to Write
	stEventWrittenToFile : ST_Event;
	
	// Variable to store the STRING to write into the file
	CsvString : T_MaxString; 	
	
END_VAR

------------------------------------------------------------------------
CASE eWriteState OF
	FILE_OPEN_TRIGGER : 
		// send rising edge to request Windows to Open the File
		fbFileOpen(bExecute := TRUE);
		eWriteState := FILE_OPEN; // next step
	
	FILE_OPEN : 
		fbFileOpen(bExecute := FALSE);
		
		// Error Detection from FB OUTPUT
		IF fbFileOpen.bError THEN
			eWriteState := ERROR;
			
		// Check if we are ready to Open with Busy Flag and File Handle
		ELSIF NOT fbFileOpen.bBusy AND fbFileOpen.hFile <> 0 THEN
			eWriteState := WAIT_FOR_EVENT; // next step
			
		END_IF
	
	WAIT_FOR_EVENT :
		// Do nothing, we are waiting for the user to call us
	
	WRITE_EVENT_TRIGGER : 
		// Convert ST_Event into a CSV String (use method)
		CsvString := CreateCsvString(stEvent := stEventWrittenToFile);
		
		// send rising edge to request Windows to Write into the File
		// then send CSV String and pass the File Handle
		fbFilePuts(bExecute := TRUE,
					sLine := CsvString, 
					hFile := fbFileOpen.hFile);	
		
		eWriteState := WRITE_EVENT; // next step
	
	WRITE_EVENT : 
		fbFilePuts(bExecute := FALSE);
		
		// Error Detection from FB OUTPUT
		IF fbFilePuts.bError THEN
			eWriteState := ERROR;
		
		// Check if we are ready to write with Busy Flag 
		ELSIF NOT fbFilePuts.bBusy THEN
			eWriteState := FILE_CLOSE_TRIGGER; // next step
		END_IF
	
	FILE_CLOSE_TRIGGER : 
		// send rising edge to request Windows to Close the File
		// then pass the File Handle
		fbFileClose(bExecute := TRUE,
					hFile := fbFileOpen.hFile);	
		
		eWriteState := FILE_CLOSE; // next step
	
	FILE_CLOSE : 
		fbFileClose(bExecute := FALSE);
		
		// Error Detection from FB OUTPUT
		IF fbFileClose.bError THEN
			eWriteState := ERROR;
		
		// Check if we are ready to close with Busy Flag 
		ELSIF NOT fbFileClose.bBusy THEN
			eWriteState := FILE_OPEN_TRIGGER; // restart state machine
			
		END_IF
	
	ERROR :
		// Do nothing (for now)
		
END_CASE

6.1 Public Method

{warning 'add method implementation '}
METHOD StoreEvent
VAR_INPUT
	stEvent	: ST_Event;
END_VAR


------------------------------------------------------------------
// Here is where the CSV-writing will be done

// Are we ready to Write? 
IF eWriteState = WAIT_FOR_EVENT THEN
	// Go to next step
	eWriteState := WRITE_EVENT_TRIGGER;
	// Take Event to Write from Input
	stEventWrittenToFile := stEvent;
END_IF

6.2 Private Method

METHOD PRIVATE CreateCsvString : T_MaxString
VAR_INPUT
	stEvent : REFERENCE TO ST_Event;	
END_VAR

VAR
	TempString : T_MaxString; 
	
END_VAR

------------------------------------------------------------------
TempString := TO_STRING(stEvent.eEventType);
TempString := CONCAT(STR1 := TempString, STR2 := ',');
TempString := CONCAT(STR1 := TempString, STR2 := TO_STRING(stEvent.eEventSeverity));
TempString := CONCAT(STR1 := TempString, STR2 := ',');
TempString := CONCAT(STR1 := TempString, STR2 := TO_STRING(stEvent.nEventIdentity));
TempString := CONCAT(STR1 := TempString, STR2 := ',');
TempString := CONCAT(STR1 := TempString, STR2 := stEvent.sEvenText);
TempString := CONCAT(STR1 := TempString, STR2 := ',');
TempString := CONCAT(STR1 := TempString, STR2 := TO_STRING(stEvent.dtTimestamp));
TempString := CONCAT(STR1 := TempString, STR2 := '$n');

CreateCsvString := TempString; 

Diagram:

Remember, it IMPLEMENTS an Interface:
|500


D.2.2.3) MAIN
  1. Main
PROGRAM MAIN
VAR
	(* 
	  --------------- PART 1 ---------------------
	stThisIsOurFirstInstanceOfAStructure : ST_Event;
	bBoolean : BOOL := FALSE; 
	  --------------------------------------------
	*)
	
	bBoolean : BOOL := FALSE; 
	
	// Initialize the variables needed to add an Event with sample values
	eEventType1 : E_EventType := E_EventType.Alarm; //user defined ENUM
	eEventSeverity1 : TcEventSeverity := TcEventSeverity.Error; // library ENUM
	nEventIdentity1 : UDINT := 42;
	sEvenText1 : STRING(255) := 'This is a sample event';
	
	// Create an instance of the FB that defines the format for Persistent Event Storage
	XmlPersistentEventStorage : FB_XmlPersistentEventStorage;
	
	// Create one instance of our FB EventLogger, and provide interface parameter for FB_init
	EventLogger1 : EventLogger(XmlPersistentEventStorage); 
	
	
END_VAR

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

IF bBoolean THEN
	// Add an event (in one cycle of the PLC) using the AddEvent Method
	EventLogger1.AddEvent(eEventType1, eEventSeverity1, nEventIdentity1, sEvenText1); 
	bBoolean := FALSE;
END_IF

// call instance every cycle
CsvPersistentEventStorage1();

Diagram:

Full Diagram:


Z) Glossary

File Definition

🔙 Previous Part | Next Part 🔜

↩️ Go Back