- What is a reference?
- Page fault code
- Option 1: Using __ISVALIDREF
- Option 2: Using VAR_IN_OUT
- Option 3: Using FB_init
Recently I was coding up a new function block and passed another function block by reference to it. Somewhere I forgot to check if the reference was valid before using it and 💥 Page Fault! After some thinking I came up with a few solutions how this can be prevented and even how you could catch mistakes like this at compile time instead of during run time. Let me show you what I did.
What is a reference?
Before diving into the subject it is good to briefly explain what a reference is and why you would want to use it. A reference is a link to an object, where an object can either be a data type (e.g. LREAL
), a function block (e.g. R_TRIG
) or a user defined data type (e.g. a STRUCT
). A reference just passes on a link to the original object, instead of making a copy of it. References are very much like pointers in that sense, but references are easier to use.
The advantages of pointers and references are that this is more memory efficient, since no copy of the object has to be created each time it is used somewhere. Furthermore changes are directly made to the original object, so there is no need to return anything.
That all sounds very good, but as you might have guessed from the introduction is that it is not all fun and games. There are some drawbacks as well. In this article I will treat one of them: how to make sure you referenced something before it is used.
Page fault code
First let’s set up the problem. We have an simple function block Foo
, which increments a counter each time it is called.
FUNCTION_BLOCK Foo
VAR
Counter : UINT := 0;
END_VAR
Counter := Counter + 1;
And another function block, UsesFoo1
, to which Foo
gets passed by reference. The implementation part of UsesFoo1
calls foo()
.
FUNCTION_BLOCK UsesFoo1
VAR_INPUT
foo : REFERENCE TO Foo;
END_VAR
foo();
Finally we make a program Runner1
which calls UsesFoo1
.
PROGRAM Runner1
VAR
usesFoo : UsesFoo1;
END_VAR
usesFoo();
If we run this code, we’ll get a Page fault.
And if we log into the PLC, we see that, surprise surprise, it all went tits up when it tried to call foo()
:
__ISVALIDREF
Option 1: Using In order to prevent the Page Fault, we can use __ISVALIDREF
to check if the reference is valid before calling it. So the body of UsesFoo1
will change into:
IF __ISVALIDREF(foo) THEN
foo();
END_IF
If we now run the new code, we will see that it no longer crashes. However, when we login we see that the foo.Counter
doesn’t increment. It just shows that there is no valid pointer. This makes sense of course, because foo
is not assigned.
Concluding
This solution prevents the PLC from crashing, but the code still doesn’t do what we want. In order for this solution to work requires you to remember that you need to first check the reference each time before calling it. Furthermore it is now possible the reference is not valid and thus foo()
is not called at all, as we saw above. It might take you a minute (or hour 😛) to figure out why foo()
is not doing anything.
VAR_IN_OUT
Option 2: Using A better solution would be to use VAR_IN_OUT
. By using VAR_IN_OUT
, we’re also passing the variable by reference to the function block. So any changes you make to foo
inside UsesFoo2
will affect the state of the original foo
instance.
FUNCTION_BLOCK UsesFoo2
VAR_IN_OUT
foo : Foo;
END_VAR
foo();
Now when we build the code with foo
unassigned, the compiler will start to complain.
In order for the program to compile, we have to initialize an instance of Foo
in our Runner2
program and pass the foo
instance to usesFoo
:
PROGRAM Runner2
VAR
foo : Foo;
usesFoo : UsesFoo2;
END_VAR
usesFoo(foo:=foo);
When we activate the successfully compiling code and login, we see that the foo.Counter
is now increasing with every call.
Concluding
This is a much better solution than using __ISVALIDREF
everywhere. The VAR_IN_OUT
solution is a form of defensive programming the code is designed in such a way that making a mistake is impossible, or at least very difficult. Some minor drawbacks of this solution are that bit variables can’t be transferred directly to a VAR_IN_OUT
variable (see InfoSys for a workaround) and the fact that each time usesFoo
is called it needs foo
passed to to it. The latter might get annoying if usesFoo
is called multiple times.
FB_init
Option 3: Using A third option would be to use FB_init
. FB_init
is a function block initializer which gets implicitly called when the code is started. Implicit means that this method gets called automatically when the code is executed. So there is no need to call it explicitly by saying usesFoo.FB_init()
. For more information there is also an article by Stefan Henneken on FB_init
usage.
Let me show you how to use FB_init
with our current example. First we add a new function block called UsesFoo3
. To this function block we add an internal variable _foo
which is a reference to Foo
. Finally _foo
gets called in the implementation part.
FUNCTION_BLOCK UsesFoo3
VAR
_foo : REFERENCE TO Foo;
END_VAR
_foo();
At this point the code will also generate a Page Fault when we run it, since _foo
doesn’t reference to anything yet. In order to assign something to _foo
, we add a FB_init
method to our function block. To do so, first add a new method.
Then from the drop-down menu select FB_init
. The other methods in the list are outside the scope of this article, but you can always have a look at InfoSys for more info on FB_reinit
and FB_exit
.
The FB_init
already comes with some standard code. This code affects the behavior of FB_init
depending on the operating case: warm or cold starts and an online change. There is no need to explicitly assign these variables; they are implicitly assigned depending on the operating case, as explained here.
To the VAR_INPUT
of FB_init
add a new variable: foo
. This will temporarily hold a reference to the Foo
function block. In the implementation part you assign the foo
reference to the _foo
internal variable of the function block.
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)
foo : REFERENCE TO Foo;
END_VAR
_foo REF= foo;
To execute the code we add another program and create two instances: foo
and usesFoo
. The function block initializer of UsesFoo3
gets passed the foo
instance. Finally we call usesFoo()
in our implementation part.
PROGRAM Runner3
VAR
foo : Foo;
usesFoo : UsesFoo3(foo);
END_VAR
usesFoo();
Now when we activate the code and log in, we’ll see that the counter is increasing.
The advantage of the FB_init
solution is that in case you forget to pass foo
to UsesFoo3
, the compiler will raise an error, so any mistakes are caught early on.
Concluding
Although there are some drawbacks to the FB_init
method, such as the fact that finding errors in it can be more difficult and the fact that you can still get a Page Fault if you forget to assign the FB_init
input to a local function block variable as we saw above. I still think it provides some advantage, because you only need to pass a variable once to a function block and not each time you call the function block as with the VAR_IN_OUT
solution.
Discuss: Reddit/Plc, Reddit/TwinCAT.