Chapter 8 / 32 · Part 2 · Decisions

Edges, latches and memory

Detect rising and falling edges, set and reset latches with clear priority, and decide what the program must remember.

Level and edge

Every condition you have written so far looks at a signal's level: is the button pressed, is the level above 80 %, is the mode 2. The answer is TRUE or FALSE on every scan, and the program reacts to it on every scan. For a lamp that follows a switch, that is exactly right.

Some things should happen once per event, though. A counter should add one per carton, not one per scan the carton spends in front of the photo-eye. A push-button that steps a sequence forward should step it once per press, however long the operator holds it. For those you need the edge: the single scan on which a signal changes. A rising edge is the scan where it goes from FALSE to TRUE. A falling edge is the scan where it goes from TRUE to FALSE.

The difference is easy to underestimate until you see it. An operator presses a button for about a third of a second, which is a normal, brisk press. At a 10 ms scan that is 30 scans with the button TRUE. Count the level and one press adds 30.

PROGRAM Main
VAR
    Button     : BOOL;   (* input: push-button, TRUE while pressed *)
    ButtonPrev : BOOL;   (* memory: Button on the previous scan *)
    LevelCount : DINT;   (* counts every scan the button is held *)
    EdgeCount  : DINT;   (* counts presses *)
END_VAR

IF Button THEN
    LevelCount := LevelCount + 1;
END_IF;

IF Button AND NOT ButtonPrev THEN     (* rising edge: TRUE now, FALSE last scan *)
    EdgeCount := EdgeCount + 1;
END_IF;
ButtonPrev := Button;

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

Switch Button on, wait a second, switch it off. Do it again. LevelCount climbs by ten per second while the button is held; EdgeCount goes up by exactly one per press. Same input, same scan, and the two counts mean completely different things.

A rising edge, written by hand

The second IF above is the whole trick, and it is worth taking apart because every edge detector in every PLC works this way.

The controller has no idea what an input was a moment ago. It only knows what it is now. To see a change, the program must remember the previous value itself, in a variable it writes at the end of the scan: ButtonPrev := Button;. On the next scan, ButtonPrev holds the old value while Button holds the new one. The rising edge is then simply "TRUE now and FALSE before": Button AND NOT ButtonPrev. A falling edge is the mirror image: NOT Button AND ButtonPrev.

Here is a press, scan by scan. The middle column is what ButtonPrev holds when the edge test runs, which is last scan's value of Button:

ScanButtonButtonPrev at the testButton AND NOT ButtonPrevButtonPrev after the update
1FALSEFALSEFALSEFALSE
2TRUEFALSETRUETRUE
3TRUETRUEFALSETRUE
4TRUETRUEFALSETRUE
5FALSETRUEFALSE (falling edge here)FALSE
6FALSEFALSEFALSEFALSE

The edge exists on scan 2 and nowhere else. The falling-edge expression would be TRUE on scan 5.

The order of those two lines is everything. The edge test must run before the previous-state variable is updated. Update it first and ButtonPrev already equals Button by the time you compare them, so the edge can never be seen.

The opposite mistake is just as easy to make, and on Siemens controllers it has a specific cause. The memory variable has to keep its value from one scan to the next. If it is reset or overwritten before the edge test, the test compares the input with FALSE every time, and the "edge" is TRUE on every scan the input is TRUE.

How short can a pulse be?

Two facts follow from the mechanism. An edge lasts exactly one scan: anything that needs to see it must run in that same scan, after the test. And an input pulse shorter than one scan can fall between two input reads and never be seen at all, edge or level.

The second one is easy to quantify. The controller samples each input once per scan, at the start, when it copies the input module's state into the process image. A pulse of width w on a task with scan time T, where w is shorter than T, is caught only if one of those samples happens to land inside it, and the chance of that is w divided by T. A 5 ms pulse from a fast sensor on a 20 ms task is seen about one time in four. A 15 ms pulse on the same task is seen three times in four. Nothing in the code tells you which pulses were missed.

The gaps between pulses obey the same rule. Picture a case conveyor running at 1.5 m/s with cartons 200 mm long and 50 mm apart, counted by a photo-eye:

QuantityValue
Time a carton blocks the eye200 mm ÷ 1.5 m/s = 133 ms
Time the gap clears the eye50 mm ÷ 1.5 m/s = 33 ms
Gap seen on a 10 ms task3 or 4 scans: always seen
Gap seen on a 20 ms task1 or 2 scans: always seen
Gap seen on a 40 ms task0 or 1 scan: missed about one gap in six

The 40 ms row is a real failure. When a gap is missed, two cartons look like one long carton, the eye gives one rising edge, and the count comes up short by one. Nobody wrote 40 ms on purpose, either: the task was 20 ms when the machine was commissioned, and two years of additions pushed it out. A count that starts drifting after a software change, without any change to the conveyor, is a sign to look at the scan time.

The input module filters the signal before the controller ever samples it, and that filter has to be shorter than the shortest pulse or gap you need. A Beckhoff EL1008 has a fixed 3 ms input filter, suited to mechanical contacts that bounce; the EL1018 in the same family filters at 10 µs, for fast signals. On many modules the filter is a parameter: the on-board inputs of an S7-1200 are set to about 6.4 ms by default and can be changed in the device configuration. Set a 20 ms filter on the input that sees the 33 ms gap above and you have taken most of the gap away before the program has a chance at it. On a fieldbus the I/O update time samples the signal again, so add the network cycle to the scan when you do this arithmetic.

When the numbers do not work, cleverer code will not fix them. Put the signal in a faster task (chapter 26), wire it to a hardware counter input that counts in the module, or use an input card that latches or timestamps short pulses.

R_TRIG and F_TRIG

Writing Prev variables by hand works, but every edge needs its own memory variable, and in a large program they multiply. IEC 61131-3 packages the pattern as two standard function blocks: R_TRIG for rising edges and F_TRIG for falling edges. Each instance carries its own memory inside, so you declare one instance per signal, call it once per scan with the signal on its CLK input, and read the one-scan pulse on its Q output.

Inside, they are the hand-written edge from the previous section. The standard gives their bodies in ST, and they are equivalent to this:

FUNCTION_BLOCK R_TRIG
VAR_INPUT  CLK : BOOL; END_VAR
VAR_OUTPUT Q   : BOOL; END_VAR
VAR        M   : BOOL; END_VAR        (* memory: CLK on the previous call *)
Q := CLK AND NOT M;
M := CLK;
END_FUNCTION_BLOCK

FUNCTION_BLOCK F_TRIG
VAR_INPUT  CLK : BOOL; END_VAR
VAR_OUTPUT Q   : BOOL; END_VAR
VAR        M   : BOOL := TRUE; END_VAR  (* starts TRUE: see "the first scan" below *)
Q := NOT CLK AND NOT M;
M := NOT CLK;
END_FUNCTION_BLOCK

The memory M is an internal variable of the instance, which is why it persists between scans and why each signal needs an instance of its own.

PROGRAM Main
VAR
    PartPresent : BOOL;      (* input: photo-eye, TRUE while a part blocks it *)
    PartIn      : R_TRIG;    (* rising edge: a part has just arrived *)
    PartOut     : F_TRIG;    (* falling edge: a part has just left *)
    Arrived     : DINT;
    Departed    : DINT;
END_VAR

PartIn(CLK := PartPresent);
PartOut(CLK := PartPresent);

IF PartIn.Q THEN
    Arrived := Arrived + 1;
END_IF;
IF PartOut.Q THEN
    Departed := Departed + 1;    (* the tail of the part has cleared the eye *)
END_IF;

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

Pause the program with the header controls, flip PartPresent, and step one scan at a time. PartIn.Q is TRUE for exactly one scan when the part arrives; PartOut.Q for exactly one scan when it leaves. The difference between Arrived and Departed is the number of parts in front of the eye right now: 0 or 1 here, more on a long accumulation zone with an eye at each end.

The falling edge is often the more useful one. A labeller should fire when the trailing edge of the box passes, a fill counter should increment when the bottle has fully left the scale, and a "part cleared" signal is what lets the next part in.

Three mistakes account for most edge-detector bugs.

The first is calling the instance conditionally. IF Auto THEN PartIn(CLK := PartPresent); END_IF; only updates the instance's memory while Auto is TRUE. Switch to Auto while the eye is blocked and you get a phantom edge from a part that arrived minutes ago. Call edge detectors unconditionally, every scan, and put the condition on what you do with Q.

The second is sharing one instance between two signals, or calling the same instance twice in a scan. The second call overwrites the memory and the first signal's edges disappear. One signal, one instance, one call.

The third is forgetting the first scan. The standard defines R_TRIG with its memory starting at FALSE, so if the input is already TRUE when the PLC starts, Q is TRUE on the very first scan. A part sitting in front of the eye at power-up is counted once. Usually that is right; occasionally it is not, and you need to know it happens. F_TRIG starts with its memory set the other way, so it does not fire on the first scan.

Edges by vendor

Vendors package edges differently, and the differences bite when you move code between them.

In TIA Portal, SCL has the IEC R_TRIG and F_TRIG blocks, each with its own instance data. Ladder and FBD have P and N contacts, P and N coils, and the P_TRIG and N_TRIG boxes, and every one of those needs an edge memory bit that you supply as an operand. That bit is the M in the listing above, handed to you to look after. It must be unique to that one edge, and it must persist between scans, so put it in a static variable of a function block, in a global data block or in a memory bit, and never in the Temp area. Reusing one memory bit for two edges is a classic of inherited S7 code: both edges then work only some of the time, depending on which was evaluated last.

CODESYS and TwinCAT 3 ship R_TRIG and F_TRIG in the standard library, exactly as the IEC defines them.

Studio 5000 has no R_TRIG. Ladder uses ONS, OSR and OSF, each with a storage bit you supply, which plays the same role as the Siemens edge memory bit. Structured text and function block diagrams use OSRI and OSFI, which work on a tag of the structure type FBD_ONESHOT: you write the signal to its .InputBit member, call the instruction with the tag, and read .OutputBit.

(* Studio 5000 structured text: vendor syntax, not runnable here *)
PartInOS.InputBit := PartPresent;
OSRI(PartInOS);                     (* PartInOS : FBD_ONESHOT *)
IF PartInOS.OutputBit THEN
    Arrived := Arrived + 1;
END_IF;

There is a behavioural difference hiding in there. Rockwell documents that on the first scan OSRI sets up its history so that it needs a FALSE-to-TRUE transition of InputBit before it fires. A part already sitting in front of the eye at power-up is therefore not counted by OSRI, where the IEC R_TRIG counts it. Neither is wrong, but a counting routine converted from one platform to the other will be one out after every power cycle with a part in the eye, and someone will spend a morning finding out why.

Under every name, an edge detector is a memory bit and an AND NOT.

Edges on values that are not BOOLs

An edge is a change, and the idea is not limited to BOOLs. The same "compare with last scan, then remember" pattern detects any change in a number: a new recipe selected, a new batch number from the MES, a mode switch, a step change.

PROGRAM Main
VAR
    RecipeNo      : INT := 1;   (* input: recipe selected on the HMI *)
    RecipePrev    : INT;        (* memory: RecipeNo on the previous scan *)
    RecipeChanged : BOOL;       (* TRUE for one scan after every change *)
    Reloads       : DINT;       (* how many times the parameters were loaded *)
END_VAR

RecipeChanged := RecipeNo <> RecipePrev;
RecipePrev    := RecipeNo;

IF RecipeChanged THEN
    Reloads := Reloads + 1;     (* load the new recipe's parameters here *)
END_IF;

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

Type 2, then 2 again, then 3. Reloads goes up once per real change, and typing the same number twice does nothing. Notice also that Reloads was already 1 before you touched anything: RecipePrev starts at 0, the recipe starts at 1, so the very first scan counts as a change. Here that is what you want, because the parameters should be loaded at power-up. If you do not want it, start the memory at the same value as the input, or use the trick from chapter 12, where LastState starts at -1 on purpose so the first scan is always an entry.

Latches: making the program remember

An edge is a moment. Plenty of machine behaviour needs the opposite: something that stays after the event that caused it has gone. The motor keeps running after the start button is released. The alarm stays on after the temperature has dropped, until someone acknowledges it. That is a latch, and it is what the missing-ELSE bug in chapter 5 was doing by accident. Here you do it on purpose.

A latch has a set condition, a reset condition and a memory. The plainest way to write one is two IFs:

PROGRAM Main
VAR
    StartPB   : BOOL;          (* input: green start button, normally open *)
    StopPB_NC : BOOL := TRUE;  (* input: red stop button, normally closed: TRUE = not pressed *)
    Motor     : BOOL;          (* output: motor contactor *)
END_VAR

IF StartPB THEN
    Motor := TRUE;             (* set *)
END_IF;
IF NOT StopPB_NC THEN
    Motor := FALSE;            (* reset - written last, so it wins *)
END_IF;

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

Flip StartPB on and off: the motor keeps running. Flip StopPB_NC off (that is pressing the stop button) and it stops. Now hold both at once. The motor stays off, because the reset IF comes second and has the last word. With two IFs, the one written last dominates. Swap them and start would beat stop, which on a conveyor is a hazard: an operator leaning on a jammed start button could not stop the belt.

Notice the stop button. It is wired normally closed, so the input is TRUE while nobody touches it, and the program stops the motor when the input goes FALSE. A broken wire or a loose terminal then stops the motor too, instead of making the stop button useless without anyone knowing. That is the fail-safe wiring rule from chapter 4, and it holds for every stop, every limit switch that ends a movement and every guard switch. It is good practice for operational stops; it does not make the stop button a safety function. An emergency stop or a guard interlock must act through a safety relay or safety PLC designed to ISO 13849-1, whatever the standard program's latch is doing.

You met the one-line form of the same latch in chapter 4 as a seal-in: Motor := (StartPB OR Motor) AND NOT StopPB;. The expression and the two IFs do the same thing. The two-IF form scales better when there are several set and several reset reasons; the expression is compact and impossible to misorder. Pick one per output and keep it.

When there are several reasons of each kind, collect them into two named BOOLs first, then latch on those. A reviewer can check the two lists against the specification without reading the latch at all:

StartRequest := StartPB OR HmiStart OR (AutoMode AND DownstreamReady);
StopRequest  := NOT StopPB_NC OR HmiStop OR Overload OR NOT GuardClosed;

IF StartRequest THEN
    Motor := TRUE;
END_IF;
IF StopRequest THEN
    Motor := FALSE;            (* last, so every stop reason beats every start reason *)
END_IF;

RS and SR: priority you can see

The two-IF latch hides its priority in the line order. IEC 61131-3 has two standard function blocks that put it in the name:

  • RS is reset-dominant. Inputs S and R1, output Q1. If both inputs are TRUE, Q1 is FALSE.
  • SR is set-dominant. Inputs S1 and R, output Q1. If both inputs are TRUE, Q1 is TRUE.

The memory aid: the input with the 1 wins. R1 dominates in RS, S1 dominates in SR.

PROGRAM Main
VAR
    SetIn     : BOOL;   (* input: set request *)
    ResetIn   : BOOL;   (* input: reset request *)
    ResetWins : RS;     (* reset-dominant latch *)
    SetWins   : SR;     (* set-dominant latch *)
END_VAR

ResetWins(S := SetIn, R1 := ResetIn);
SetWins(S1 := SetIn, R := ResetIn);

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

Turn SetIn on and off: both latches set and stay set. Turn ResetIn on and off: both reset. They only disagree when both inputs are on at the same time, and that is precisely the case you must decide for every latch in a machine. Written out as a table, with Q1 before and after one call:

S (set input)R (reset input)Q1 beforeRS givesSR gives
FALSEFALSEFALSEFALSE (holds)FALSE (holds)
FALSEFALSETRUETRUE (holds)TRUE (holds)
TRUEFALSEeitherTRUETRUE
FALSETRUEeitherFALSEFALSE
TRUETRUEeitherFALSETRUE

Only the last row differs, and it is the row nobody tests unless they are told to.

Siemens names them the other way round

If you work in TIA Portal ladder or FBD, read the pins, not the name on the box. Siemens's SR box ("set/reset flip-flop") has inputs S and R1, and reset wins when both are TRUE. Its RS box ("reset/set flip-flop") has R and S1, and set wins. That is the reverse of the IEC names above, a difference that goes back to STEP 7 and has outlived it. The memory aid still works, because the pin with the 1 is still the winner. What does not work is converting an IEC SR into a Siemens SR by name and expecting the same behaviour. When you inherit a project from another platform, check every latch's dominance against the specification, not against its name; and when you are not sure what a box does, press F1 on it in TIA Portal, or force both inputs in a simulation and watch the output.

Push-on, push-off

A work light over a bench has one illuminated push-button: press once for on, press again for off. That is a toggle, and it is the cleanest demonstration of why edges and memory belong together. The light needs memory (its own state), and the button must act on its edge, because "invert the light" on a level would invert it on every scan the button is held.

PROGRAM Main
VAR
    Button     : BOOL;     (* input: push-button *)
    Pressed    : R_TRIG;
    LightEdge  : BOOL;     (* correct: toggles on the rising edge *)
    LightLevel : BOOL;     (* the bug: toggles on the level *)
END_VAR

Pressed(CLK := Button);
IF Pressed.Q THEN
    LightEdge := NOT LightEdge;
END_IF;

IF Button THEN
    LightLevel := NOT LightLevel;    (* flips every scan the button is held *)
END_IF;

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

Press and release Button a few times. LightEdge behaves; LightLevel flickers ten times a second while you hold the button, and where it ends up depends on whether you held it for an odd or even number of scans, a coin toss for the operator. On a real bench at a 2 ms scan it is a 250 Hz flicker that looks like a dim light, then a random final state.

A real push-button also bounces: its contacts chatter for a few milliseconds when pressed, producing several edges where the operator made one. Most digital input cards filter that out with an input delay of around 3 ms, which is why you rarely see it. On a fast task with filtering turned off, a toggle can fire two or three times per press. Debouncing with a timer is one of the first jobs in chapter 9.

The toggle is also the place where I would think twice about using it at all. One button that does two opposite things gives the operator no way to know what the next press will do except by looking at the light. For a work light that is fine. For a pump or a conveyor, separate start and stop buttons are clearer, and a stop button that always stops is what EN 60204-1 expects of a machine's controls.

What to remember, and what not to

Every variable in a PLC program keeps its value from one scan to the next. That makes memory free to create and easy to create by accident. A good program is deliberate about it.

Remember the things that cannot be recomputed from the inputs: the state of a latch (is the motor running?), the previous value of a signal you need edges from, a count of events, an acknowledged-or-not alarm, the step of a sequence. These are the program's real state, and each one deserves a clear name and a known reset.

Do not remember what you can compute. If HighLevel is just Level > 80.0, assign it every scan; do not set it in one place and clear it in another. If an output depends only on present inputs, it should be written as an expression or a full IF/ELSE. Every unnecessary latch is a place where the program and the machine can disagree after a fault, a mode change or a download.

Then decide what memory should survive a power cycle. IEC 61131-3 lets you declare variables VAR RETAIN so they keep their value through a power loss. A shift production count or a total running-hours meter should be retained. A motor run latch must not be: when power returns, the machine must not restart on its own. EN 60204-1, the standard for electrical equipment of machines, requires that restarting after a power failure does not happen automatically where that could be hazardous, and a retained run latch is exactly that automatic restart. Leave motor and valve latches as ordinary variables, so they start FALSE and the operator has to press start again.

Run latches on a Logix controller

Chapter 2 noted that a Rockwell Logix controller keeps every tag through a power cycle, with no RETAIN keyword. For latches that changes the default. On CODESYS, TwinCAT or an S7-1500, a run latch in an ordinary variable comes back FALSE after a power cycle. On ControlLogix or CompactLogix, a run latch written in structured text with := comes back with whatever value it had when the power went, and if the controller returns to Run mode, so does the motor command.

Rockwell projects deal with this in two common ways. One is a first-scan routine that clears the run latches, using the first-scan status bit S:FS that chapter 11 mentions for counters. The other is Rockwell's non-retentive assignment, [:=], which resets its tag to zero each time the controller enters Run mode. Either way, write it on purpose and test it: power the cabinet down with the machine running, power it up, and check that nothing moves until someone presses start. That test belongs on every commissioning checklist, whatever the platform.

First out: which fault came first?

When a hydraulic unit trips, it rarely trips on one thing. The oil gets hot, the viscosity drops, the pressure falls, and within half a second three alarms are latched. The alarm list now shows all three, and the maintenance technician has to guess which one started it. A first-out indication answers that: it records the fault whose edge arrived first, and ignores the rest until the whole group has been reset.

It is a neat combination of the two halves of this chapter. The latches remember every fault. The edges say when each one happened. And one more piece of memory, the first-out code, is written only on an edge, and only while it is still empty.

Edges are what make it work. Written on levels, the code is overwritten by whichever alarm is lower in the program, because every latched alarm is TRUE on every scan. Two faults that arrive in the same scan cannot be told apart by a PLC that samples once per scan; the code then shows whichever the program tests first, and the scan time is the resolution of the whole scheme. Plants that need finer resolution, such as turbine trip systems, timestamp their inputs in the I/O module.

Memory meets time

Edges turn a level into a single-scan event, and latches turn an event into lasting state. Between them they give a program a sense of history. Each kind of memory comes with a question you must answer: where the edge memory lives, which input of a latch wins, and what comes back after a power cut.

The missing ingredient is time. A latch remembers that something happened; it does not know how long ago. Confirming that a level has been high for five seconds, ignoring a contact that bounces for 20 ms, or giving a motor three seconds to prove it is running all need a timer. The next chapter starts with the most common one: the on-delay timer, TON.

Practise this chapter