TwinCAT 3 Tutorial: Writing your own Functions and Function Blocks
This chapter is part of the TwinCAT 3 Tutorial.
There are three types of Program Organization Units (POUs):
- Function Blocks
When you use the wizard to create a new TwinCAT 3 PLC project, it will create a program for you called
MAIN. TwinCAT 3 also comes with several libraries full of Function Blocks and Functions for you to use (the most common being timers, counters, and math functions like addition and subtraction).
Let’s start by examining the differences between Functions and Function Blocks. The basic difference is that a function block can hold state, and a function cannot. For instance, a counter function block has to “remember” the value of the counter internally, and it must also remember the previous value of the increment input because it needs to detect a rising edge. To the programmer using a function or a function block, this means that an instance of a function block needs to be declared as a variable and referenced when you call it.
Another difference is that a function always returns a value. The
ADD function returns a number, for instance. You have to declare a return type for a function when you define it. Most functions only return one value, but you can define a function that has more outputs.
Here is an example of an
Here is an example of a delay-on timer (
TON) function block:
I’ve highlighted the variable above the
TON block. As you can see, to use a function block you have to declare the variable to hold the internal state, but to use a function, you don’t. This means a function is easier to use, but the functionality is limited to what it can compute from the current inputs of the function.
A program is like a function block (it has internal state), but there is only one global copy of each program, so you can’t create “instances” of programs like you can with a function block.
You can’t use a function block (like a timer) inside of a function, because the timer’s state will be re-initialized very time the function is called. TwinCAT 3 will let you do it, but the timer won’t work.
Build a Maintenance Counter Function Block
TwinCAT 3 includes counter function blocks in the standard library, but these have some limitations. The primary limitation is that the internal counter uses a 16-bit
WORD variable, so it can only count up to 65535. What if we wanted to create a maintenance counter for our machine that recorded the number of parts produced. It’s easy to imagine a machine that could produce millions of parts in its lifetime. A counter with a 32-bit value (DWORD) would solve this.
Since this is a maintenance counter, we also want to make this value persistent so it doesn’t reset to zero when the program is restarted. We can do this by making the instance variable of the timer persistent, but we can also do it by making the internal counter persistent (which makes all instances of this counter persistent automatically).
Start by right clicking on the folder where you want to add your function block (typically a folder called Common, or Helpers, or Utilities, etc.). Then choose Add->POU… from the context menu. In the Add POU dialog, enter
MaintenanceCounter as the function block name and choose Function Block under Type. Finally, make sure Ladder Logic Diagram (LD) is selected under Implementation Language:
Click Open to create the function block:
Now define the inputs and outputs of the function block. I copied these from the count-up (
CTU) function block, but I changed the
CV values from
WORD (16-bit) to
DWORD (32-bit) variables:
Now let’s add internal variables. Since we want these variables to be persistent, we need to change the variable declaration from
VAR PERSISTENT. Then add two variables, as shown:
RisingEdge variable is actually an instance of another function block, called an
R_TRIG. This is a function block that detects a rising edge on an input.
R_TRIG is actually defined in the IEC-61131-3 programming language standard for automation systems. Internally it “remembers” the last state of the input value, and by making it persistent, it will remember that value even through a power cycle or program restart.
CurrentValue variable is just the internal representation of the
CV output value. At the end of our function block, we’ll copy the
CV. Again, we’re making it persistent so the value won’t be reset on a power cycle or program restart.
Now add the ladder logic for the
In rung 1, the
R_TRIG function block detects a rising edge on the
CU input. In rung 2, it adds 1 to the counter value on the rising edge. Rung 3 zeros the counter on a reset, and since it executes after the ADD instruction, the
RESET input will be “dominant”. That is, if the
RESET input is on while the
CU input turns on, the
RESET input will keep the counter at zero. Rung 4 copies the internal value to the output, and rung 5 sets the
Q (counter done) output based on the
I’m sure you can see some other interesting possibilities. For instance, if you needed a counter that stopped counting when the current value reached the preset value, you could easily make one. I’ll leave that as a homework exercise for you.
Build a Function to Convert Celsius to Fahrenheit
Most industrial sensors will report the temperature in Celsius, but some US customers might want their machine to have setpoints entered in Fahrenheit. This is a good application for a function because the output (temperature in Fahrenheit) can be computed directly from the input (temperature in Celsius).
Start by right clicking on the folder where you want to add your new function. Select
Add->POU... from the context menu. In the
Add POU dialog, enter
CelsiusToFahrenheit as the Name, choose Function under Type, and make sure to enter
REAL in the Return type box. Make sure Ladder Logic Diagram (LD) is selected in the Implementation language drop down:
Click Open to create the empty function:
Note that the return type (
REAL) is defined after the function name at the top. This is actually an implicitly declared output variable, where the variable name is the name of the function itself. At some point in the function you have to set this implicit variable to some value.
Now declare an input variable (
The formula for converting from Celsius to Fahrenheit is to multiply by 9, divide by 5, and then add 32. Since we’re doing this in ladder logic, let’s break it down into 3 steps and use an internal (temporary) value. Add a variable called
Remember that any variables declared in a function will be re-initialized to their default values (typically zero or false) at the beginning of each call to the function. If you’re familiar with other languages like C/C++, BASIC, or Pascal, these are like local variables.
Now create the ladder logic to compute the temperature in Fahrenheit:
In rung 1, it takes the input temperature, multiplies by 9 and stores the result in the temporary variable. Rung 2 takes the temporary result and divides by 5, storing it back into the same temporary location. Rung 3 takes the result of rung 2 and adds 32, returning it as the result of the function.
Note that a purely mathematical function like this is better implemented in the Structured Text language, but this is a good introductory example of a function with local variables implemented in Ladder Logic. We can revisit this in a later section on Structured Text.
The VAR_IN_OUT Variable Type
In the examples above, you can see three types of variable declarations: inputs (
VAR_INPUT), output (
VAR_OUTPUT) and local (
VAR). There is another optional block you can add, called
VAR_IN_OUT. In other languages like C/C++, etc., this other type would be called “pass by reference”.
With the normal inputs and output types, the value of the input is copied into the function or function block and copied out of it into an output. For a BOOL or INT type, this happens very fast and it also prevents a function block from inadvertently modifying an input value (which would normally be unexpected by someone using a function block). However, there are cases where you want a function block to do something to a variable, or you don’t want to incur the overhead of copying a very large variable structure into a function block. In these cases you can use the
As an example, look at this function block that increments a number:
You would call it like this:
Notice that the
Number input has a little two-directional arrow next to it indicating that it’s an in/out variable.
Also note that we still had to declare an variable for this function block, even though the function block itself doesn’t have any internal state. If you were the user of this function block, it might be a bit annoying. We can actually use a trick to fix this. Instead of declaring
Increment as a function block, we can declare it as a program:
A program’s internal state is global, but in this case it has no internal state. That means you can use it like this:
Note that you have to add the
VAR_IN_OUT blocks manually, since the wizard that creates a program POU only adds the
VAR_IN_OUT block is an advanced topic, but it does come up enough that you need to be aware of it. For instance, let’s say you had a long list of measurements stored in an array and you wanted to compute the average. Copying the array into function could be an expensive operation (causing a higher than necessary scan time), but passing a reference to the array by declaring it as an in/out variable is as fast as passing an integer.
As a matter of understanding, when you create a function block and you declare a variable for it to represent the internal state (like a timer variable), that internal state variable is implicitly being passed to the function block by reference, which is why the function block can read and write to that variable.
The overall structure of your automation project should be broken up into programs, with one program representing one functional unit of your machine. The more you break it up into logical pieces, the easier it will be to find the part of the logic you’re looking for. Average programs might have 5 to 15 rungs of ladder logic, but this isn’t a hard and fast rule, and you should group your logic however it makes the most sense for someone reading through your logic. Try to keep related things together and unrelated things apart.
Use functions and function blocks to build the “domain specific language” of your application. Use descriptive names for your functions and function blocks that clearly describe what they do. If you see the same 5 rungs of logic repeated over and over, that’s a good indication that perhaps you should be looking at creating a function or a function block (but only if it improves readability).
Functions and function blocks are particularly useful to contain identical bits of logic that are likely to change. Putting logic like that in a single place makes it easy to change. However, just because two bits of logic are identical now doesn’t mean they aren’t likely to diverge in the future. This is actually quite common in automation programming. You might have a conveyor system with 5 identical zones, but the customer may some day want to add some feature to zone 4 that you can’t anticipate. Making a
ConveyorZone function block seems like a great idea at the beginning, but may be a bad decision in the long run.
Making these kinds of decisions can only be informed by experience. As a general rule, I don’t think machine control logic (e.g. contacts and coils that make the machine start and stop) belongs in a re-used function or function block, but common elemental operations like logging an event, computing common formulas, or communicating with some piece of hardware should be contained in a re-usable POU. That’s because the physical and electrical configuration of the machine is likely to change on a piece-by-piece basis, but the way you log events, or compute an average, is only likely to change simultaneously across your entire project (or is unlikely to change at all).
Note that functions and function blocks can access global variables too. For example, I would expect a function block that logs an event to access a global event array where the event history is kept. Accessing physical inputs is also fairly common. Be careful not to do something unexpected, such as setting a physical output (each output should be driven by exactly one rung in a program somewhere, not in a function block).
Finally, since each POU can be implemented in a different programming language, using functions and function blocks is a great way to write parts of your project in a language that’s more appropriate to the task at hand.