The same five lines, written twelve times
Open any PLC project that has grown for a few years and you will find the same calculation pasted all over it. Tank 1 converts its level to litres. Tank 2 does the same with a different diameter. Tank 3 was copied from tank 2, and somebody fixed a typo in tank 3 but not in tank 1. Every copy is a place for a bug to live.
A function cures that. You write the calculation once, name it, declare what it needs and call it wherever the answer is needed. The rule that makes a function a function is strict and useful: it takes inputs, returns one result and remembers nothing. Same inputs, same answer, every time — which makes functions the easiest code in a project to test and trust.
Here is one for a vertical cylindrical tank with a flat bottom:
FUNCTION TankLitres : REAL
VAR_INPUT
LevelM : REAL; (* liquid height above the flat bottom, m *)
DiameterM : REAL; (* inside diameter of the tank, m *)
END_VAR
TankLitres := 3.14159 * DiameterM * DiameterM / 4.0 * LevelM * 1000.0;
END_FUNCTION
PROGRAM Main
VAR
Level : REAL := 1.2; (* input: level in metres, same for both tanks here *)
LitresTank1 : REAL; (* 1.6 m mixing tank *)
LitresTank2 : REAL; (* 2.4 m buffer tank *)
END_VAR
LitresTank1 := TankLitres(LevelM := Level, DiameterM := 1.6);
LitresTank2 := TankLitres(LevelM := Level, DiameterM := 2.4);
END_PROGRAMChange Level and both results follow. One formula, two tanks, no copies.
Anatomy of a FUNCTION
Read the declaration line by line:
FUNCTION TankLitres : REALnames the function and states its return type. The type after the colon is the type of the one value the function hands back. It can be any elementary type:BOOL,INT,DINT,REAL,TIME,STRING.VAR_INPUT…END_VARlists the inputs. These are the only things the caller can pass in. Inside the function they behave like ordinary variables that already hold the caller's values.- The body computes the result and assigns it to the function's own name:
TankLitres := ...;. That assignment is how a value gets back to the caller. Forget it and the function returns the type's default —0.0,FALSE, an empty string — with no warning on most targets. END_FUNCTIONcloses it. Functions are declared outside thePROGRAM, as separate units — separate objects in a CODESYS or TwinCAT project tree.
A function may also have a VAR section for local working variables and may leave early with RETURN;. What it cannot have is anything that survives from one call to the next.
The call itself is an expression: TankLitres(...) is a REAL, usable anywhere a REAL fits — in a comparison, or as an argument to another function. IF TankLitres(LevelM := Level, DiameterM := 1.6) > 2200.0 THEN is legal ST.
Named and positional arguments
There are two ways to pass arguments. Named (formal) arguments pair each value with its input name, so the order does not matter. Positional (non-formal) arguments are matched to the inputs in the order they are declared:
FUNCTION Clamp : REAL
VAR_INPUT
In : REAL; (* value to limit *)
Lo : REAL; (* lower limit *)
Hi : REAL; (* upper limit *)
END_VAR
IF In < Lo THEN
Clamp := Lo;
RETURN; (* leave now; the lines below are skipped *)
END_IF;
IF In > Hi THEN
Clamp := Hi;
RETURN;
END_IF;
Clamp := In;
END_FUNCTION
PROGRAM Main
VAR
Speed : REAL := 75.0; (* input: speed request from the HMI, % *)
Named : REAL;
ByOrder : REAL;
Builtin : REAL;
END_VAR
Named := Clamp(Lo := 0.0, In := Speed, Hi := 100.0); (* any order *)
ByOrder := Clamp(Speed, 0.0, 100.0); (* positional: In, Lo, Hi *)
Builtin := LIMIT(0.0, Speed, 100.0); (* the standard one: MN, IN, MX *)
END_PROGRAMType 140 or −20 into Speed and all three results agree. Yes, LIMIT already does this — chapter 6 covers it — and in real code you should use the standard function. Clamp is here because it is short enough to see whole, and because it sets up the trap in the next box.
Notice that LIMIT takes its arguments as minimum, value, maximum, while our Clamp takes value, low, high. Both orders are reasonable. That is exactly the problem with positional arguments.
My rule: name every argument unless the function has a single input or the order is part of its meaning (like MIN(A, B)). Positional calls save a few characters and cost an afternoon on site. IEC 61131-3 also forbids mixing the two styles in one call, so pick one per call. Vendors differ in one useful detail: CODESYS and TwinCAT let you leave out a function input and use its declared initial value, while TIA Portal requires every parameter of an FC to be supplied. Supplying all of them by name works everywhere.
A function has no memory
This trips up everyone once. A function's VAR locals are created fresh for every call and thrown away when it returns. Nothing carries over to the next call, in this scan or the next.
Side by side, the difference is plain. The program's counter lives in the program's memory; the function's counter is reborn on every call:
FUNCTION CountScans : DINT
VAR_INPUT
Enable : BOOL;
END_VAR
VAR
N : DINT; (* local: created at 0 on every call *)
END_VAR
IF Enable THEN
N := N + 1;
END_IF;
CountScans := N;
END_FUNCTION
PROGRAM Main
VAR
Enable : BOOL; (* input *)
FromFunction : DINT;
InProgram : DINT; (* program memory: survives between scans *)
END_VAR
FromFunction := CountScans(Enable := Enable);
IF Enable THEN
InProgram := InProgram + 1;
END_IF;
END_PROGRAMSwitch Enable on. InProgram climbs every scan; FromFunction sits at 1.
Real controllers add a nasty variant. On a classic Siemens S7-300 or S7-400, an FC's TEMP variables are not initialised at all: they hold whatever the previous block left on the shared local stack, so a function that reads a local before writing it can appear to remember — sometimes. Newer platforms initialise locals, but the rule stands: write every local before you read it, and never expect it to survive the call. Two more consequences:
- A function must not contain a timer, counter or edge detector. IEC 61131-3 does not allow function block instances to be declared inside a function, and an editor that accepts one anyway gives you a timer that is reborn every call and never reaches its preset.
- A function should not read or write global variables, even where the compiler lets it. The moment it does, its result depends on something other than its inputs and you lose the property that made it worth writing.
A small library of pure helpers
A pure function depends only on its inputs and changes nothing but its return value. Every well-run site I have worked on had a small library of them, tested once and used everywhere: scaling, clamping, dead bands, plausibility checks, unit conversions.
Here are two of them, plus the standard LIMIT, working together on a pump that keeps a discharge header above a pressure setpoint. The transmitter delivers raw counts — 0 to 27,648 for 0 to 10 bar on a Siemens analog card. How to scale and validate those counts properly is the subject of chapter 19; here the point is how small functions compose:
FUNCTION F_Scale : REAL
VAR_INPUT
Raw : INT; (* raw counts from the analog card *)
RawLo : INT;
RawHi : INT;
EuLo : REAL; (* engineering units at RawLo *)
EuHi : REAL; (* engineering units at RawHi *)
END_VAR
F_Scale := (INT_TO_REAL(Raw) - INT_TO_REAL(RawLo)) * (EuHi - EuLo)
/ (INT_TO_REAL(RawHi) - INT_TO_REAL(RawLo)) + EuLo;
END_FUNCTION
FUNCTION F_InBand : BOOL
VAR_INPUT
Value : REAL;
Lo : REAL;
Hi : REAL;
END_VAR
F_InBand := Value >= Lo AND Value <= Hi;
END_FUNCTION
PROGRAM Main
VAR
Raw : INT := 8294; (* input: about 3 bar *)
SetpointReq : REAL := 4.5; (* input: operator setpoint, bar *)
PressureBar : REAL;
SignalOk : BOOL;
Setpoint : REAL;
PumpCmd : BOOL;
END_VAR
PressureBar := F_Scale(Raw := Raw, RawLo := 0, RawHi := 27648, EuLo := 0.0, EuHi := 10.0);
SignalOk := F_InBand(Value := PressureBar, Lo := -0.1, Hi := 10.2);
Setpoint := LIMIT(1.0, SetpointReq, 8.0); (* standard function: MN, IN, MX *)
PumpCmd := SignalOk AND PressureBar < Setpoint;
END_PROGRAMTry a setpoint of 12 bar: it is clamped to 8. Type a raw value of 32767 — what an S7-1500 card reports when it diagnoses a broken wire — and SignalOk drops, taking the pump command with it. The program body reads like a description of the machine, four lines, and each line can be checked on its own.
A few habits keep a function library healthy:
- Prefix the names (
F_here,FC_in many Siemens shops) so a reader knows a call has no memory. - Write a header comment: the units of each input and what happens at the edges. "Returns 0 when RawHi = RawLo" beats three lines of prose.
- Guard the inputs that break the maths.
F_Scaledivides byRawHi - RawLo; configure both as 0 and it divides by zero. A library function returns something defined instead. - Keep I/O out. A function that reads
%IW64directly can only ever serve one channel. - Store the library once — a CODESYS or TwinCAT library, a TIA Portal global library — not as copies pasted between projects.
Functions on real controllers
The IEC FUNCTION exists on every platform, but the names and the details differ enough to confuse people moving between them:
| Platform | What IEC calls a function | Notes |
|---|---|---|
| CODESYS, TwinCAT 3 | FUNCTION | Follows the standard closely. Omitted inputs take their initial value. |
| Siemens TIA Portal (SCL) | FC | The return type sits in the interface's Return section; an FC may also return Void and use outputs only. Every parameter must be supplied. |
| Rockwell Studio 5000 | none | Logix has no user-defined functions. The Add-On Instruction (AOI) has a backing tag that keeps its values between calls, so it behaves like a function block, not a function. |
| Omron Sysmac Studio | FUN | Separate from FB along IEC lines: FUN for stateless calculations, FB when you need instances. |
The Rockwell row matters: an AOI remembers. Coming from Studio 5000 to CODESYS or TIA, the equivalent you want is the function block, not the function.
IEC 61131-3 3rd edition builds methods, interfaces and inheritance on top of function blocks. The F · FB · OOP track covers them in depth; this course stays with the core every controller runs.
From answers to objects with memory
You can now write a function, call it with named arguments and explain why a counter inside it never counts. Pure helpers compose into a program body that reads like the machine: scale, check, clamp, decide.
What a function cannot do is remember. A motor has to remember that it was started; a valve has to time how long it has been open; three identical conveyors each need their own copy of that memory. The next chapter introduces the function block — inputs, outputs and private memory in one reusable unit — and builds a motor block you will use for three motors at once.