Chapter 18 / 24 · Part 5 · Reusable code

Function blocks

Write FUNCTION_BLOCKs with inputs, outputs and memory, create instances, and reuse one motor block for many motors.

A function with a memory

Functions forget. A motor starter has to remember that it was started; a press has to hold the peak force of the last stroke. None of that fits in a function, and copying the logic for every motor brings back the twelve-copies problem.

A function block (FB) solves both. It is a unit of code with inputs, outputs and private memory that survives from scan to scan. You write it once as a type, then declare as many instances of it as you have machines. Each instance is a separate piece of memory with its own copy of every variable. You have used function blocks since chapter 9 without writing one: TON, CTU and R_TRIG are function blocks from the standard library, and T1 : TON; declares an instance.

Here is the smallest useful one — a start/stop seal-in, used for two conveyors:

FUNCTION_BLOCK FB_StartStop
VAR_INPUT
    Start : BOOL;   (* TRUE while the start button is pressed *)
    Stop  : BOOL;   (* TRUE while the stop button is pressed *)
END_VAR
VAR_OUTPUT
    Run   : BOOL;   (* contactor command, remembered between scans *)
END_VAR

IF Stop THEN
    Run := FALSE;          (* stop wins *)
ELSIF Start THEN
    Run := TRUE;
END_IF;

END_FUNCTION_BLOCK

PROGRAM Main
VAR
    Start1, Stop1 : BOOL;   (* inputs: infeed conveyor buttons *)
    Start2, Stop2 : BOOL;   (* inputs: outfeed conveyor buttons *)
    Infeed  : FB_StartStop; (* instance 1 *)
    Outfeed : FB_StartStop; (* instance 2 *)
END_VAR

Infeed(Start := Start1, Stop := Stop1);
Outfeed(Start := Start2, Stop := Stop2);

END_PROGRAM
Live example — open this page with JavaScript to run and edit it. Inputs you can drive: Start1, Stop1, Start2, Stop2.

Flip Start1 on and off. Infeed.Run stays on — the FB remembered — and Outfeed.Run does not move, because the outfeed has its own memory. The IF without an ELSE would be a latching bug in ordinary logic (chapter 5); here it is the whole point. The output keeps its value until something changes it.

Declaring, calling and reading a block

The declaration looks like a function's, with three differences:

  • FUNCTION_BLOCK FB_StartStop has no return type. A block hands results back through VAR_OUTPUT, and it can have as many outputs as it needs.
  • VAR_OUTPUT variables are written inside the block and read by the caller with dot notation: Infeed.Run. You can read an instance's outputs anywhere in the program, in an IF, in an expression, even several routines away.
  • VAR variables are the block's private memory. In IEC 61131-3 the caller cannot write them. A timer instance declared here — FbTimer : TON; — becomes part of every instance of the block.

The call runs the block's code once, against the memory of that one instance. Use named arguments, as with functions. An input you leave out keeps its value from the previous call — legal, occasionally handy, more often a bug. Pass every input every time.

Two habits separate tidy FB code from the other kind:

  1. Call every instance exactly once per scan, unconditionally. More on why below.
  2. Read outputs after the call, so the program sees this scan's result. Reading Infeed.Run above the call gives you last scan's value — sometimes that is what you want, usually it is an accident.

One instance per machine

The instance is the memory. Two machines therefore need two instances, even when the code is identical. The temptation to save a declaration produces one of the most confusing bugs in PLC work:

Each instance also carries its own copy of any timer inside it. Two motors built from the same type time completely independently:

A motor block worth reusing

The predict above used the block this whole chapter has been heading for. Here it is properly, driving three motors on a mixing skid: an infeed conveyor, a 15 kW agitator and a transfer pump. Each motor's contactor has an auxiliary contact wired back as feedback. If the contactor is commanded but the feedback does not arrive within the timeout, the block switches the motor off and latches a fault until it is reset.

FUNCTION_BLOCK FB_Motor
VAR_INPUT
    Start     : BOOL;           (* request to run *)
    Stop      : BOOL;           (* TRUE = stop; has priority *)
    Feedback  : BOOL;           (* contactor auxiliary contact *)
    Reset     : BOOL;           (* clears a latched fault *)
    FbTimeout : TIME := T#2s;   (* how long feedback may take *)
END_VAR
VAR_OUTPUT
    Run   : BOOL;               (* contactor coil command *)
    Fault : BOOL;               (* latched: feedback missing *)
END_VAR
VAR
    FbTimer : TON;              (* one per instance *)
END_VAR

IF Reset THEN
    Fault := FALSE;
END_IF;

IF Stop OR Fault THEN
    Run := FALSE;
ELSIF Start THEN
    Run := TRUE;
END_IF;

FbTimer(IN := Run AND NOT Feedback, PT := FbTimeout);
IF FbTimer.Q THEN
    Fault := TRUE;
    Run   := FALSE;
END_IF;

END_FUNCTION_BLOCK

PROGRAM Main
VAR
    StartAll, StopAll, ResetAll : BOOL;   (* inputs: skid buttons *)
    PumpFails  : BOOL;                    (* input: simulate a pump contactor that won't pull in *)
    Conveyor   : FB_Motor;
    Mixer      : FB_Motor;
    Pump       : FB_Motor;
    FbConveyor, FbMixer, FbPump : BOOL;   (* aux contacts *)
END_VAR

Conveyor(Start := StartAll, Stop := StopAll, Feedback := FbConveyor, Reset := ResetAll);
Mixer(Start := StartAll, Stop := StopAll, Feedback := FbMixer, Reset := ResetAll, FbTimeout := T#3s);
Pump(Start := StartAll, Stop := StopAll, Feedback := FbPump, Reset := ResetAll, FbTimeout := T#1s);

(* Plant simulation: each aux contact follows its coil one scan later. *)
FbConveyor := Conveyor.Run;
FbMixer    := Mixer.Run;
FbPump     := Pump.Run AND NOT PumpFails;

END_PROGRAM
Live example — open this page with JavaScript to run and edit it. Inputs you can drive: StartAll, StopAll, ResetAll, PumpFails.

Turn PumpFails on, then pulse StartAll. The conveyor and mixer run; the pump runs for 1 s, watch Pump.FbTimer.ET climb, then the pump trips and latches its fault while the other two carry on. Turn PumpFails off and pulse ResetAll: the fault clears, but the pump stays off until the next start. That is deliberate. A motor that restarts by itself when a fault is reset surprises the person standing next to it.

The program body is now three calls and three lines of simulation. Every motor gets identical supervision, and a fix to the block fixes all three. Note the per-instance parameter: the mixer gets 3 s because a big agitator's soft starter is slow to close its bypass contactor; the pump gets 1 s.

Call every instance, every scan

A function block only runs when it is called. Put the call inside an IF and, on the scans where the condition is false, the block's code — including any timer inside it — simply does not execute. Nothing inside it sees the inputs change.

FUNCTION_BLOCK FB_Delay
VAR_INPUT
    IN : BOOL;
END_VAR
VAR_OUTPUT
    Q  : BOOL;
    ET : TIME;
END_VAR
VAR
    T : TON;
END_VAR
T(IN := IN, PT := T#3s);
Q  := T.Q;
ET := T.ET;
END_FUNCTION_BLOCK

PROGRAM Main
VAR
    Enable : BOOL := TRUE;   (* input: the condition around the call *)
    Dly    : FB_Delay;
END_VAR

IF Enable THEN               (* the bug: a conditional call *)
    Dly(IN := TRUE);
END_IF;

END_PROGRAM
Live example — open this page with JavaScript to run and edit it. Inputs you can drive: Enable.

Let it run for a second, then switch Enable off. Dly.ET freezes — not reset, frozen. Switch it back on and ET jumps forward, because a TON measures elapsed time from when it started, but only looks at the clock when it is called. Neither behaviour is what anyone intended.

Passing a variable in by reference: VAR_IN_OUT

VAR_INPUT copies a value in; VAR_OUTPUT copies a value out. VAR_IN_OUT does neither. It hands the block a reference to the caller's own variable, so the block reads and writes the caller's variable directly. Use it when several instances must update one shared thing — a line total, a common alarm word — or when you pass a large STRUCT and do not want it copied every scan.

Two lanes of a bottle infeed, each with its own photo-eye, each counting its own bottles, both adding to one line total:

FUNCTION_BLOCK FB_Lane
VAR_INPUT
    Eye : BOOL;          (* photo-eye, TRUE while a bottle blocks it *)
END_VAR
VAR_IN_OUT
    Total : DINT;        (* the caller's line total, updated in place *)
END_VAR
VAR_OUTPUT
    Count : DINT;        (* this lane's own count *)
END_VAR
VAR
    Edge : R_TRIG;       (* each lane has its own edge memory *)
END_VAR

Edge(CLK := Eye);
IF Edge.Q THEN
    Count := Count + 1;
    Total := Total + 1;
END_IF;

END_FUNCTION_BLOCK

PROGRAM Main
VAR
    EyeA, EyeB   : BOOL;   (* inputs *)
    LaneA : FB_Lane;
    LaneB : FB_Lane;
    BottlesTotal : DINT;
END_VAR

LaneA(Eye := EyeA, Total := BottlesTotal);
LaneB(Eye := EyeB, Total := BottlesTotal);

END_PROGRAM
Live example — open this page with JavaScript to run and edit it. Inputs you can drive: EyeA, EyeB.

Pulse EyeA twice and EyeB once: the lanes read 2 and 1, the total 3. An in-out must be connected on every call, and to a variable — Total := 5 makes no sense when the block writes back. Use it sparingly: a block that writes its caller's variables is harder to reason about. If an output would do, use an output.

Function or function block?

The vendors line up like this. CODESYS and TwinCAT 3 use FUNCTION_BLOCK as written here. In TIA Portal an FB's instance memory is an instance data block: a separate DB per top-level call, or a multi-instance inside the caller's own DB — which is what you get when you declare Conveyor : FB_Motor; in another FB's static section. In Studio 5000 the reusable unit is the Add-On Instruction; each AOI tag is an instance. An AOI named Motor with a tag ConveyorMtr is called in ST as Motor(ConveyorMtr, ...) with positional arguments, and its outputs are read as ConveyorMtr.Run.

Modern CODESYS, TwinCAT and TIA also let a function block carry methods and implement interfaces — IEC 61131-3 3rd edition. A motor block might expose a Reset method instead of a Reset input, and several device types might share one I_Device interface so a sequencer can command them all the same way:

INTERFACE I_Device
    METHOD Reset : BOOL
    END_METHOD
END_INTERFACE

FUNCTION_BLOCK FB_Motor IMPLEMENTS I_Device
VAR_INPUT
    Start, Stop, Feedback : BOOL;
END_VAR
VAR_OUTPUT
    Run, Fault : BOOL;
END_VAR

METHOD Reset : BOOL
    Fault := FALSE;
    Reset := TRUE;
END_METHOD
END_FUNCTION_BLOCK

That is CODESYS-style 3rd-edition syntax; this page's engine runs the 2nd-edition core and does not execute it. Methods, properties, inheritance and interfaces are taught with working equivalents in the F · FB · OOP track. Instances, inputs, outputs and private memory are the foundation they sit on.

From blocks to real signals

You can now write a function block, give each machine its own instance, call every instance every scan and read its outputs with dot notation. You have seen why one instance cannot serve two motors, why a conditional call freezes a timer, and how VAR_IN_OUT updates a shared variable.

The next chapter puts both units to work on analog inputs. A 4–20 mA transmitter arrives as a raw integer; turning it into a trustworthy level — scaled, filtered, checked for a broken wire, alarmed with hysteresis and delay — is a job for exactly the functions and blocks you just learned to write.

Practise this chapter