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 from the last chapter.
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_PROGRAMFlip Start1 on and off. Infeed.Run stays on, because the FB remembered, and Outfeed.Run does not move, because the outfeed has its own memory. An 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.
This is a stop-button seal-in for teaching. On a real conveyor the stop that protects people is a hard-wired emergency stop through a safety relay or safety PLC, designed to ISO 13849-1 or IEC 62061, and it removes power whatever the PLC program says. The FB's Stop is the operational stop.
Declaring, calling and reading a block
The declaration looks like a function's, with three differences:
FUNCTION_BLOCK FB_StartStophas no return type. A block hands results back throughVAR_OUTPUT, and it can have as many outputs as it needs.VAR_OUTPUTvariables 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 anIF, in an expression, even several routines away.VARvariables 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. That is legal, occasionally handy, and more often a bug; the predict further down shows how it bites. Pass every input every time.
Two habits separate tidy FB code from the other kind:
- Call every instance exactly once per scan, unconditionally. More on why below.
- Read outputs after the call, so the program sees this scan's result. Reading
Infeed.Runabove the call gives you last scan's value. Sometimes that is what you want; usually it is an accident.
Here is the second habit in numbers. A line elsewhere in the program copies the conveyor's run command to a horn interlock, once above the call and once below it. Start1 is pressed on scan 2:
| Scan | Start1 | Infeed.Run read above the call | Infeed.Run read below the call |
|---|---|---|---|
| 1 | FALSE | FALSE | FALSE |
| 2 | TRUE | FALSE | TRUE |
| 3 | FALSE | TRUE | TRUE |
| 4 | FALSE | TRUE | TRUE |
The copy above the call is always one scan late. At a 10 ms scan nobody will notice on a horn. On a fast reject gate, or in a chain where block B reads block A's output above A's call and block C reads B above B's call, the lags add up, and you get a sequence that works on the bench and misses parts at full line speed. Order the calls so that data flows downwards through the program: call A, then the code that reads A, then B. Chapter 9 shows the same effect on a timer's Q.
What an instance is in memory
When you declare Infeed : FB_StartStop;, the compiler sets aside one block of memory with room for every variable the FB type declares: its inputs, its outputs and its VAR section, in a fixed layout. Unlike a function's scratch memory, this block is allocated once and stays at the same place for as long as the program is loaded. That is why it remembers.
For the FB_Motor type used later in this chapter, one instance holds:
| Member | Section | Type | What it holds between scans |
|---|---|---|---|
Start, Stop, Feedback, Reset | VAR_INPUT | BOOL | the value passed by the last call |
FbTimeout | VAR_INPUT | TIME | initial value T#2s, or the last value passed |
Run, Fault | VAR_OUTPUT | BOOL | the block's decisions |
FbTimer | VAR | TON | a whole timer instance: IN, PT, Q, ET and its internal start time |
Three motors means three copies of that table, side by side. The TON inside is itself a function block instance, nested inside the motor's memory. Blocks inside blocks are normal: a pump block can contain a motor block, which contains a timer, and each level of nesting is just more memory inside the outer instance.
The call itself does three things. It copies the arguments into the instance's input slots, runs the body with the instance as its working memory, and returns. The outputs are not copied anywhere; they stay in the instance, and Infeed.Run reads them in place. That is why an output can be read before the call, after the call or from another program entirely: it is just a variable that the block happens to own.
Where the vendors keep it
| Platform | Where an instance lives | What you see |
|---|---|---|
| CODESYS, TwinCAT 3 | in the memory of whatever declared it: a program, a GVL or another FB | the instance as a variable; expand it in the watch window to see every member |
| Siemens TIA Portal | an instance data block for a call from an OB, or a multi-instance inside the caller's own instance DB when declared in another FB's Static section | the instance DB as a block in the project tree, e.g. "Conveyor_DB" |
| Rockwell Studio 5000 | a backing tag of the AOI's data type, one per instance | a tag such as ConveyorMtr with every parameter and local tag as members; every AOI also has EnableIn and EnableOut |
| Omron Sysmac Studio | an instance variable of the FB type in the POU's variable table | the instance in the watch tab |
TIA Portal makes the memory visible as a block, and that has consequences. Call an FB from OB1 and TIA asks you for an instance DB: "FB_Motor" called as "Conveyor_DB"(). Call the same FB from inside another FB, with the instance declared in Static, and it becomes a multi-instance: no separate DB, just a section of the caller's DB. Most Siemens projects I would call well structured use one single-instance DB per machine unit and multi-instances for everything inside it, so the project tree does not fill up with 200 motor DBs.
When the block's interface changes
Because an instance has a fixed layout, adding or removing a variable in the FB type changes the layout of every instance. What the controller does about that on a running machine is one of the most important vendor differences in this chapter:
- CODESYS and TwinCAT handle it as an online change. The runtime allocates the new layout, copies the values of members that still exist across, and initialises the new ones. Running latches survive. Anything that held the address of the old instance (a pointer or reference in vendor code) is now stale, which is why the online change dialog warns you.
- TIA Portal has to reinitialise an instance DB whose interface has changed, which resets every value in it to its start value, unless the S7-1500 block uses optimised access with a memory reserve configured, in which case it can download without reinitialisation as long as the new variables fit in the reserve.
- Studio 5000 does not allow an AOI's definition to be edited online at all (chapter 21 covers what that does to how people design AOIs). You edit offline and download, which stops the controller.
Here is how the TIA case goes wrong on site. An engineer adds a statRunHours variable to the conveyor FB during production, downloads, and accepts the prompt to reinitialise. Every conveyor instance resets: Run goes FALSE on all eleven conveyors at once, the line stops, and the product on the belts has to be cleared by hand. Nothing was wrong with the new code. The fix is procedural: on S7-1500, configure the memory reserve before the plant goes live, and make interface changes during a planned stop. Read the download dialog before clicking through it.
One instance per machine
The instance is the memory. Two machines therefore need two instances, even when the code is identical. Trying 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:
The other half of "inputs belong to the call" is that an input you leave out of a call is not reset. It keeps the last value any call gave it:
A motor block worth reusing
The second predict 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_PROGRAMTurn PumpFails on, then pulse StartAll. The conveyor and mixer run. The pump runs for 1 s while Pump.FbTimer.ET climbs, then it 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, and the pump gets 1 s.
Two calls omit FbTimeout here, and that is the one case where leaving an input out is fine: a parameter with a declared initial value that nobody ever changes at runtime. The conveyor uses the 2 s default on every scan. If the value could change, pass it every time like any other input.
The feedback timeout is not a safety function
FB_Motor detects a contactor that did not pull in, which protects the process and tells maintenance where to look. It does not detect a contactor whose contacts have welded shut, and it cannot stop a motor whose contactor will not drop out. Monitoring a contactor for a safety function (a guard door, a two-hand control) is done by a safety relay or safety PLC that reads the contactor's mirror contact, as required by the risk assessment under ISO 13849-1. The standard-PLC block sits beside that, not instead of it.
Arrays of instances
Once a block exists, a line of identical machines is an array of instances and a loop. Here are four conveyors in a cascade, numbered from the infeed (1) to the discharge (4). Each one may only start when the one downstream of it is already running, and each one starts 1 s after its permit appears, so the line fills from the discharge end and no conveyor dumps product onto a stationary belt. If a conveyor trips, every conveyor upstream of it stops, and the ones downstream keep running to clear themselves.
FUNCTION_BLOCK FB_Conveyor
VAR_INPUT
Request : BOOL; (* line run request from the operator *)
Permit : BOOL; (* the conveyor downstream is running *)
Healthy : BOOL; (* overload, pull-cord and drive OK *)
END_VAR
VAR_OUTPUT
Run : BOOL;
END_VAR
VAR
StartDly : TON; (* stagger the starts *)
END_VAR
StartDly(IN := Request AND Permit AND Healthy AND NOT Run, PT := T#1s);
IF NOT (Request AND Permit AND Healthy) THEN
Run := FALSE;
ELSIF StartDly.Q THEN
Run := TRUE;
END_IF;
END_FUNCTION_BLOCK
PROGRAM Main
VAR
LineRun : BOOL := TRUE; (* input: line run selector *)
TripC3 : BOOL; (* input: simulate a trip on conveyor 3 *)
(* Conv[1] is the infeed, Conv[4] the discharge *)
Conv : ARRAY[1..4] OF FB_Conveyor;
Permit : BOOL;
i : INT;
END_VAR
(* Discharge end first, so each permit is this scan's value *)
Permit := TRUE; (* takeaway after conveyor 4 is always ready *)
FOR i := 4 TO 1 BY -1 DO
Conv[i](Request := LineRun, Permit := Permit, Healthy := NOT (i = 3 AND TripC3));
Permit := Conv[i].Run; (* the next one upstream needs this one running *)
END_FOR;
END_PROGRAMWatch the line come up from the discharge end, one conveyor about every second. Then switch TripC3 on: conveyors 3, 2 and 1 stop on the same scan, and conveyor 4 keeps running. Switch it off again and the upstream conveyors restart in order, because the run selector is still on. On a real line you might not want that automatic restart after a trip; it would be a design decision, recorded in the functional specification, and chapter 22 shows how to latch a trip until someone resets it.
The loop runs from 4 down to 1 on purpose. Each conveyor's permit is the Run output of the one downstream, read straight after that conveyor's call, so every permit is this scan's value, and a trip ripples up the whole line in one scan. Run the loop the other way and each conveyor sees last scan's permit: on a four-conveyor line the stop takes three scans to reach the infeed. That is the read-after-call habit from earlier, applied to a loop.
Two cautions about instance arrays. A loop index that goes out of range does not write "the next conveyor"; it is an array bounds error, which some platforms catch at runtime and others do not (chapter 13). And an array hides the identity of each machine. When the maintenance technician asks "which one is CV-103?", Conv[3] is not an answer they can find on the drawings. Keep a comment or a constant that maps each index to its plant tag, or use named instances when there are only a handful.
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, 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_PROGRAMLet it run for a second, then switch Enable off. Dly.ET freezes: it stops updating, but it is not reset. 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.
Studio 5000 has a twist on this. An AOI called from a ladder rung whose condition is false is still executed, in a sense: the controller runs the AOI's optional EnableInFalse routine, and with no such routine it sets EnableOut FALSE and skips the logic. Called from ST there is no rung condition, so the AOI runs whenever the statement runs. The safest habit on every platform is the same: call it unconditionally and give it a mode or enable input that it handles in its own logic.
Reaching into an instance from outside
Dot notation reads an instance's outputs. Can it do more? The answer depends on the platform, and portable code avoids relying on any of it.
- Writing an input outside the call. CODESYS and TwinCAT allow
Infeed.Start := TRUE;followed later byInfeed();. It works, but now the block's inputs are set in one place and the block runs in another, and a reader of the call line no longer sees what the block was told. The engine on this site does not accept it. Pass inputs in the call. - Reading a
VARvariable. CODESYS and TwinCAT rejectx := Pump.FbTimer.ET;in code outside the block, becauseFbTimeris not an input or output. You can still see it online in the watch window, which is what the examples on these pages do. TIA Portal lets you read a static variable of an instance DB from anywhere, as"Pump_DB".FbTimer.ET, which is convenient and makes the block's internals part of its public interface by accident. - Writing a
VARvariable. Never. If the caller needs to change something, it is an input.
If code outside a block needs a value, make it an output. If it needs to change a value, make it an input. The interface then describes everything the block exchanges with the rest of the program, and you can rewrite its insides without breaking the callers.
Passing a variable in by reference: VAR_IN_OUT
VAR_INPUT copies a value in; VAR_OUTPUT holds a value for the caller to read. 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, such as a line total or 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_PROGRAMPulse 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, because a block that writes its caller's variables is harder to reason about. If an output would do, use an output.
The in-out also brings back a problem the copy semantics of inputs had solved: the variable can change under the block's feet. If the two lanes ran in two different tasks, both could read Total as 41, both add one, and both write 42. Chapter 26 calls this the read-modify-write race. Within one task, as here, the calls run one after the other and the total is exact.
Restarts, initial values and retained instances
An instance's variables start at their declared initial values (FbTimeout : TIME := T#2s) or at the type default when the program is first downloaded and on a cold start. What happens on a warm restart, after a power cut, depends on whether the instance is retained, and the platforms disagree enough to cause real trouble:
- CODESYS and TwinCAT: if you mark a single local variable
RETAINinside a function block, CODESYS places the entire instance in retain memory, all of its variables, though only the marked one is protected. CODESYS's own help advises against marking FB locals as RETAIN for that reason. The usual pattern is to keep the instance non-retained and hold the few values that must survive (a run-hour total, a calibration offset) in a retained variable outside it, passed in by in-out. - TIA Portal: each static variable of an FB has a Retain setting in the block interface, and the instance DB applies it. Check it for every latch in the block, because the default is not retained.
- Studio 5000: tags retain their values through a power cycle by default, AOI backing tags included. A
Runlatch inside an AOI is still TRUE when the controller comes back.
That last point turns into a field story with depressing regularity. A conveyor AOI holds its run latch in a local BOOL. The plant loses power with the line running. When power returns and the controller goes back to Run, every conveyor whose latch was TRUE starts again, with nobody at the panel and a fitter halfway through clearing a jam. The fix belongs in the block: a power-up or first-scan input that clears every latch, driven from the controller's first-scan flag (S:FS in Logix, the FirstScan bit of the system memory byte or a variable set in the startup OB100 in TIA Portal, a variable set once at start-up in CODESYS), and a functional specification that says, in writing, which equipment may restart on its own after power-up. Usually the answer is none of it.
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, from 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.
Designing a block's interface
The body of a block can be rewritten in an afternoon. Its interface is copied into every call in every project that uses it, so mistakes there last for years. A few conventions keep it right first time.
Prefix the type name (FB_Motor) and give instances the plant name (CV103, fbInfeedConveyor), so a search for the tag on the drawing finds the instance. Chapter 21 covers naming in full. For blocks, the rule that counts most is that inputs read as what the block is told (Start, Feedback) and outputs as what it reports (Run, Fault), never as what the caller will do with them.
Decide for each BOOL input whether the block reacts to its level or to its rising edge, and say so in the comment. FB_Motor's Start is a level: holding it does nothing extra, because the latch is already set. A "start a move" or "begin a dose" command is usually an edge, and the block should detect the edge itself with an R_TRIG, so the caller can pass a plain button. PLCopen's convention, used by every motion block in chapter 31, is a rising-edge Execute input with Done, Busy and Error outputs; it is worth copying for any block that performs an action that takes time.
A timeout or a scaling range is a parameter: set once, rarely changed. Give it a sensible initial value in the declaration, so a caller that does not care gets a working block. Operating commands are inputs and are passed every scan.
Report status as well as decisions. A block that only outputs Run makes the HMI programmer reach into its internals to show why a motor is not running. Add the outputs a technician needs: Fault, a fault code, Ready (all permissives met). They cost nothing and they save an hour every time something stops.
Keep I/O and globals out. Everything physical comes in through inputs and goes out through outputs. That one rule is what makes a block reusable and testable.
When you review someone else's block, check these in order: is every instance called once per scan, unconditionally; does every call pass every input that can change; are outputs read after the call; does anything outside the block read or write its VAR section; and what does every latch inside it do after a power cut. Most of the FB bugs I have chased on site fail one of those five.
A peak that is sampled once per scan can still miss a spike shorter than the scan. A press stroke peak lasting 2 ms will be caught on a 1 ms task and missed half the time on a 10 ms one. If the quality station depends on it, the load-cell amplifier's own peak-hold function, or a fast task dedicated to the measurement, is the right tool; the PLC block then reads the held value.
From blocks to real signals
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 level you can trust (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.