Chapter 18 / 32 · 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 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_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, 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_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. 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:

  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.

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:

ScanStart1Infeed.Run read above the callInfeed.Run read below the call
1FALSEFALSEFALSE
2TRUEFALSETRUE
3FALSETRUETRUE
4FALSETRUETRUE

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:

MemberSectionTypeWhat it holds between scans
Start, Stop, Feedback, ResetVAR_INPUTBOOLthe value passed by the last call
FbTimeoutVAR_INPUTTIMEinitial value T#2s, or the last value passed
Run, FaultVAR_OUTPUTBOOLthe block's decisions
FbTimerVARTONa 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

PlatformWhere an instance livesWhat you see
CODESYS, TwinCAT 3in the memory of whatever declared it: a program, a GVL or another FBthe instance as a variable; expand it in the watch window to see every member
Siemens TIA Portalan 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 sectionthe instance DB as a block in the project tree, e.g. "Conveyor_DB"
Rockwell Studio 5000a backing tag of the AOI's data type, one per instancea tag such as ConveyorMtr with every parameter and local tag as members; every AOI also has EnableIn and EnableOut
Omron Sysmac Studioan instance variable of the FB type in the POU's variable tablethe 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_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 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_PROGRAM
Live example — open this page with JavaScript to run and edit it. Inputs you can drive: LineRun, TripC3.

Watch 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_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: 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 by Infeed();. 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 VAR variable. CODESYS and TwinCAT reject x := Pump.FbTimer.ET; in code outside the block, because FbTimer is 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 VAR variable. 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_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, 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 RETAIN inside 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 Run latch 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.

Practise this chapter