Chapter 8 / 24 · 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 — 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, completely different meaning.

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.

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.

Two more 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. A 5 ms pulse from a fast sensor on a 20 ms task is a coin toss. For that you need a faster task, a hardware counter or a latching input card, not cleverer code.

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.

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:

  • 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.
  • 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.
  • 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.

Vendors package edges differently. TIA Portal offers R_TRIG and F_TRIG in SCL, plus the P_TRIG/N_TRIG contacts and coils in ladder. CODESYS and TwinCAT ship the IEC blocks in the standard library. Studio 5000 uses ONS, OSR and OSF in ladder and OSRI/OSFI in structured text and function block diagrams. Under every name, it is a memory bit and an AND NOT.

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 silently useless. 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.

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.

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 — reset-dominant. Inputs S and R1, output Q1. If both inputs are TRUE, Q1 is FALSE.
  • SR — 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.

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.

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.

Memory meets time

You now have the two building blocks that give a PLC program a sense of history. Edges turn a level into a single-scan event — by hand with a previous-state variable, or with R_TRIG and F_TRIG. Latches turn an event into lasting state — with two IFs, a seal-in expression, or RS and SR when you want the priority written into the block's name. And you know which memory to keep, which to compute, and which must never survive a power cycle.

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