Chapter 9 / 32 · Part 3 · Time and counting

On-delay timers (TON)

Use TON for delays, debouncing and confirmation times, and read its IN, PT, Q and ET pins scan by scan.

The on-delay timer is a function block

A conveyor at the end of a bottling line must not start the instant someone turns the selector. The horn sounds first, for two seconds, so nobody has a hand on the belt when the 1.5 kW gear motor engages. Those two seconds are the most common job in PLC programming: wait until a condition has been true for long enough, then act. In Structured Text the tool for it is the on-delay timer, TON.

If you come from ladder, you are used to dropping a timer instruction on a rung. In ST a timer is a function block, a small piece of code that carries its own memory. You declare an instance of it in the VAR section, exactly like a variable, and then call that instance in the program body. The instance remembers when its input went TRUE and how long ago that was. The type TON is the blueprint; StartWarning below is one physical timer built from it. Chapter 18 shows how to write your own function blocks. For now, treat TON as a ready-made one with four pins:

PinDirectionTypeMeaning
INinputBOOLThe condition being timed.
PTinputTIMEPreset time: how long IN must stay TRUE.
QoutputBOOLTRUE once IN has been TRUE for at least PT.
EToutputTIMEElapsed time since IN went TRUE, stopping at PT.

Here is the start-up warning as a complete program:

PROGRAM Main
VAR
    Start        : BOOL;   (* input: run selector on the panel *)
    StartWarning : TON;    (* one timer instance, used for one purpose *)
    Horn         : BOOL;   (* output: start-up horn *)
    Conveyor     : BOOL;   (* output: conveyor contactor *)
END_VAR

StartWarning(IN := Start, PT := T#2s);

Horn     := Start AND NOT StartWarning.Q;
Conveyor := StartWarning.Q;

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

Turn Start on. The horn sounds, StartWarning.ET climbs in 100 ms steps, and at T#2s the horn stops and the conveyor starts. Turn Start off and everything drops on the next scan.

The call StartWarning(IN := Start, PT := T#2s); passes values to the inputs by name with :=. Afterwards you read the outputs with a dot, StartWarning.Q. Inputs you leave out keep the value from the last call, because they are stored in the instance like everything else. Get into the habit of passing IN and PT on every call anyway, so the line tells the whole story to whoever reads it next.

A start-up warning like this one is an operational courtesy. It is not a safety function: if someone can reach a moving belt, the guarding, the emergency stop circuit and the safety relay or safety PLC behind them are what protect them, designed to ISO 13849-1 or IEC 62061. The horn tells people the belt is about to move, and the program can sound it; the program cannot be what stops the belt from hurting someone.

IN, PT, Q and ET, scan by scan

A TON follows three rules, and everything else in this chapter comes from them:

  1. On the scan where IN goes TRUE, the timer notes the time and ET starts at T#0s.
  2. While IN stays TRUE, ET grows with real elapsed time. When ET reaches PT, Q goes TRUE and ET stops at PT.
  3. On any call where IN is FALSE, Q is FALSE and ET is T#0s, immediately, on that same call.

Rule 3 is the one people forget. A TON has no memory of partial time. If IN drops for a single scan, the next rising edge starts again from zero, and that is exactly why the block is useful: Q means "IN has been continuously TRUE for at least PT". Here is a 500 ms timer on this page's 100 ms scan:

ScanINETQNote
1FALSET#0sFALSEidle
2TRUET#0sFALSErising edge: start noted
3TRUET#100msFALSE
6TRUET#400msFALSE
7TRUET#500msTRUEET reached PT
8TRUET#500msTRUEET stays at PT
9FALSET#0sFALSEreset on the same scan

The timer reacts to the level of IN, but it starts timing on the edge. That combination has a consequence that catches people who come from pushbutton logic. Feed a TON with a one-scan pulse, such as the Q of an R_TRIG from chapter 8, and it never finishes: it starts on the pulse and resets on the very next scan, when the pulse is gone.

What an instance holds

An instance is a block of memory with a fixed layout. Some of it you can see: the four pins. The rest is internal and differs by implementation, but every TON needs at least two hidden things: whether it is currently timing, and a reference point to time from.

There are two common ways to keep that reference point, and which one your platform uses decides how a timer misbehaves when you call it wrongly:

(* A sketch of a timestamp-style TON body. Not any vendor's source code.
   Now() stands for the controller's free-running millisecond clock. *)
IF NOT IN THEN
    Q := FALSE;
    ET := T#0s;
    Timing := FALSE;
ELSIF NOT Timing THEN
    Timing := TRUE;              (* rising edge of IN *)
    StartTime := Now();
    ET := T#0s;
    Q := PT <= T#0s;
ELSE
    ET := Now() - StartTime;     (* time since the edge, however many calls ago *)
    IF ET >= PT THEN
        ET := PT;
        Q := TRUE;
    END_IF;
END_IF;

The timestamp style stores the clock value at the rising edge and computes ET as "now minus then" on each call. CODESYS-style libraries work like this, and so does the engine on these pages. The alternative is an accumulator: each call adds the time since the previous call to a running total. Both give the same answer when the block is called every scan. They part company when it is not, which is the subject of the next section.

The pins themselves are stored in the instance too, which is why the outputs keep their last value between calls, and why StartWarning.Q can be read anywhere in the program, even before the call on the next scan. If you read StartWarning.Q above the call, you get the value from the previous scan; below it, the value from this one. A one-scan difference rarely matters for a horn. In chapter 16 a stopwatch reads ET before the call on purpose, because the call is about to reset it.

What the vendors put in the instance

The same four pins appear on every platform under different names and inside different structures. If you work on more than one, this table saves a lot of searching:

PlatformHow you declare itInputsOutputsTime type
IEC 61131-3, CODESYS, TwinCAT 3MyTimer : TON;IN, PTQ, ETTIME (also LTON / LTIME for 64-bit)
Siemens S7-1200 / S7-1500 (TIA Portal)multi-instance TON_TIME in Static, or a single-instance DBIN, PTQ, ETTime; the S7-1500 also has an LTime version
Rockwell Logix ladder TONtag of type TIMERrung condition, .PRE.EN, .TT, .DN, .ACCDINT milliseconds
Rockwell Logix ST / FBD TONRtag of type FBD_TIMER.TimerEnable, .PRE, .Reset.EN, .TT, .DN, .ACCDINT milliseconds

The Rockwell bits map neatly onto the IEC pins once you know them. EN is the rung condition, which is IN. DN is done, which is Q. TT ("timer timing") is TRUE while EN is on and DN is not yet, so TT is simply IN AND NOT Q, a status the IEC block leaves you to compute. ACC is ET in milliseconds and PRE is PT in milliseconds. Because PRE and ACC are DINTs, the longest Logix preset is 2,147,483,647 ms, a little under 24 days 21 hours.

Two Siemens details catch people who port code. On S7-1200 and S7-1500, Siemens documents that the timer's Q and ET are refreshed when the instruction is called and also when you access them. Read "FillTimer".Q in two places in one cycle and the two reads can disagree, because time has passed between them. Copy Q into a variable once, right after the call, and use the copy. And the older S5 timers (S_ODT and friends, with presets in S5TIME format) still exist on S7-300/400 and are offered for compatibility; they are a different family with their own behaviour and a coarser time base, described in the section on resolution below.

One name, two meanings. TONR means different things on the two biggest platforms. In TIA Portal, TONR is the time accumulator: it keeps ET when its input drops and only clears on its R input. In Studio 5000, TONR is "timer on delay with reset", an ordinary non-retentive on-delay for ST and FBD; the retentive one is RTOR. Code ported from one to the other by name alone will behave differently on the first interruption.

Call the timer on every scan

The timer only does anything when its instance is called. Between calls it is frozen memory. So the most important rule for timers in ST is this: call every timer instance exactly once per scan, unconditionally, and put the condition in IN.

Engineers who learned on ladder break this rule all the time, because in ladder the rung condition is the timer input. Translated literally into ST, the rung becomes an IF:

PROGRAM Main
VAR
    Start : BOOL;
    Delay : TON;
    Motor : BOOL;
END_VAR

IF Start THEN
    Delay(IN := Start, PT := T#1s);   (* BUG: only called while Start is TRUE *)
END_IF;

Motor := Delay.Q;

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

The motor keeps running with the selector off. When Start goes FALSE the IF skips the call, so the instance never sees IN = FALSE and never applies rule 3. Delay.Q stays TRUE forever, from the last scan the block was called. Worse, the stale value survives silently: nothing in the online view looks broken except the motor.

There is a second symptom. Turn Start off before the second is up, wait a few seconds, then turn it on again. On this engine, and on CODESYS-style implementations, the TON computes ET as "now minus the time I noted", so a timer that was skipped for a while jumps straight to Q the moment it is called again. An accumulator-style timer freezes instead and carries on from where it stopped. Neither is what you meant.

The fix is to always call it and let IN carry the condition:

Delay(IN := Start, PT := T#1s);   (* every scan *)
Motor := Delay.Q;

The same trap waits inside CASE branches (chapter 12), inside FOR loops and inside any code you skip with a RETURN. It also waits one level up, where it is harder to see. A function block called only in automatic mode, a TIA Portal FB called from a conditional block call, a Studio 5000 routine reached through a JSR on a rung that is sometimes false: every timer inside is frozen whenever its container is not called. Keep timer calls at the top level of the program, near the logic that reads them, and call the blocks that contain timers unconditionally too.

Vendors package the call differently. In Siemens SCL on an S7-1500 you declare the TON as a multi-instance in the block's Static section and call it as #FillDelay(IN := ..., PT := T#2s); TIA Portal can also put it in its own instance DB. Studio 5000 has no TON in Structured Text at all: you use TONR, set MyTimer.TimerEnable and MyTimer.PRE (a DINT in milliseconds), call TONR(MyTimer); and read MyTimer.DN and MyTimer.ACC. Chapter 21 shows one piece of logic written in all three dialects. The call-every-scan rule applies to all of them.

Debouncing a sensor

Real inputs are not clean. A mechanical limit switch bounces for a few milliseconds when its contacts close. A photo-eye looking at shrink-wrapped packs on a vibrating conveyor flickers as the film catches the light. A float switch in a tank chatters as the surface waves. To your program, every flicker is a fresh edge.

The digital input module handles the shortest noise. Most modules have an input delay, typically around 3 ms by default and often configurable from 0.05 ms up to 20 ms. That filters contact bounce, but it is far too short for a sloshing tank. For anything slower, debounce in the program with a TON, so the signal only counts once it has been stable for a set time.

PROGRAM Main
VAR
    PhotoEye    : BOOL;   (* raw input: TRUE = beam blocked *)
    EyeStable   : TON;
    PackPresent : BOOL;   (* the signal the rest of the program uses *)
END_VAR

EyeStable(IN := PhotoEye, PT := T#200ms);
PackPresent := EyeStable.Q;

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

Flick PhotoEye on and off quickly and PackPresent never moves. Hold it for longer than 200 ms and it follows. This debounces only the rising side: PackPresent drops the moment the eye clears, because rule 3 resets the timer. For a pack detector that is often what you want, since the trailing edge matters less. For a level switch you usually want both sides filtered, so the output holds its last stable state until the input has been stable the other way. Build that in the challenge below.

Choose the debounce time from the physics, not from habit. A pack moving at 0.5 m/s past a 150 mm gap leaves the beam clear for 300 ms; a 200 ms debounce on the falling side would still see the gap, a 400 ms one would merge two packs into one. Measure, or calculate, and write the reason next to the preset.

A debounce also delays the signal by exactly its preset, on top of everything else. On a pack detector that only feeds a counter, 200 ms of delay costs nothing. On a photo-eye that tells a pusher when to fire, it moves the push point by 100 mm at 0.5 m/s. Every debounce is a trade between false triggers and lateness, and the lateness belongs in the calculation for anything that acts on position.

Confirmation delays

Debouncing cleans up a signal that is electrically noisy. A confirmation delay is a different job: the signal is perfectly clean, but the process condition it reports is only worth acting on if it lasts. The numbers are bigger, typically seconds rather than milliseconds, and they come from the process.

Take a centrifugal pump drawing from a 2 m tank, with a low-level switch above the suction. When the pump starts, the surface dips and swirls at the switch for a second or two. If you trip the pump the instant the switch reports low, it will never get running. If you never trip it, it runs dry and cooks its mechanical seal. The usual answer is a confirmation delay: low level must persist for 3 s before the pump trips. That is long enough to ride out the start-up swirl and short enough to save the seal.

The same pattern runs half of every alarm list. A high-temperature alarm on an extruder barrel must persist for 10 s. A motor feedback must arrive within 2 s of the contactor command, or the motor is declared failed. A door switch must stay made for 1 s before the guard counts as closed. In every case the TON input is the bad condition, and its Q is the confirmed fault.

Name the instance after the condition it confirms (LowLevelConfirm, not T7), so a cross-reference listing reads like the alarm list. And keep the trip separate from the timer, latched, so a confirmed fault stays visible after the condition clears. Chapter 8 covered latches; chapter 22 builds full alarm handling on top of them.

Here is the motor feedback check written out. The contactor's auxiliary contact is wired back to an input, and the program confirms that what it commanded actually happened:

PROGRAM Main
VAR
    RunCmd      : BOOL;   (* input: run request from the sequence *)
    RunningFb   : BOOL;   (* input: contactor auxiliary contact, TRUE = closed *)
    Reset       : BOOL;   (* input: fault reset push button *)
    FbTimeout   : TON;
    FailToStart : BOOL;   (* output: latched motor fault *)
    Contactor   : BOOL;   (* output: contactor coil *)
END_VAR

(* Commanded, but no feedback: the condition being confirmed *)
FbTimeout(IN := RunCmd AND NOT FailToStart AND NOT RunningFb, PT := T#2s);

IF FbTimeout.Q THEN
    FailToStart := TRUE;
END_IF;
IF Reset AND NOT RunCmd THEN
    FailToStart := FALSE;
END_IF;

Contactor := RunCmd AND NOT FailToStart;

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

Turn RunCmd on and leave RunningFb off: after 2 s the fault latches and the contactor drops. Reset only works once the run command has gone away, so the motor cannot restart by surprise the moment someone presses reset. Now try the good case: RunCmd on, then RunningFb on within the 2 s. Leave it running and turn RunningFb off. The same timer catches the feedback disappearing while the motor runs, because its input is "commanded and no feedback" whatever the reason. A real motor block often wants a shorter delay for a lost feedback than for a slow start, and you can get that from one timer with two thresholds (see below) or with a second instance.

A field story: the drive that got slower

A conveyor drive on a palletiser line raised a "fail to start" alarm on roughly one start in three, starting the week after a maintenance shutdown. Nothing in the PLC had changed. The motor ran on a variable-speed drive, and the program's feedback check used the drive's at speed status bit with a 3 s confirmation delay. During the shutdown a fitter had lengthened the drive's acceleration ramp (parameter P041 Accel Time 1 on that PowerFlex 525; on a SINAMICS G120 it would be p1120) from 2 s to 4 s, to stop cartons tipping on start. The motor now reached speed after about 4 s, and whether it beat the 3 s timer depended on the load.

The symptom pointed the wrong way: an alarm that says "fail to start" sends people to the contactor and the motor. What found it was reading the timer's ET at the moment of the trip next to the drive's ramp time. The fix had two parts. The feedback check moved to the drive's running status, which answers the question "did the drive accept the command?" within a scan or two. The at-speed check became a separate, longer timeout derived from the ramp setting with a margin, so the next ramp change would not break it again. When a confirmation delay depends on a parameter held in another device, write that dependency into the comment next to the preset.

One timer, several thresholds

Q answers one question at one preset. ET is a number, and you can compare it against as many thresholds as you like. When several reactions are all measured from the same starting moment, one timer with comparisons on ET is easier to keep right than three timers with the same IN, which drift apart if someone edits one input and not the others.

The usual shape is a warning followed by a trip. A hydraulic power unit starts its pump and expects the pressure switch to make within a few seconds. At 5 s without pressure the operator gets a warning lamp, which often means a relief valve is set low or a hose has let go. At 8 s the program gives up and stops the pump, because a pump running against an open circuit heats the oil and wears the pump for nothing. The PT is set to the last threshold, so Q is the trip and ET serves the earlier ones.

Two details make this pattern behave. The comparison should be >=, because ET moves in steps of one scan and may never equal a threshold exactly. And the earlier thresholds must be below PT, since ET stops at PT: a check for ET >= T#10s on a timer with PT := T#8s can never be TRUE.

Timer resolution and the scan

A timer can only change Q when it is called, and it is called once per scan. So Q comes on at the first scan after PT has elapsed, never in between. Give a TON a preset that is not a whole number of scans and you can see it:

PROGRAM Main
VAR
    Settle : TON;
    Ready  : BOOL;
    ScanNo : INT;
END_VAR

ScanNo := ScanNo + 1;
Settle(IN := TRUE, PT := T#250ms);   (* 2.5 scans on a 100 ms task *)
Ready := Settle.Q;

END_PROGRAM
Live example — open this page with JavaScript to run and edit it.

Step it scan by scan. ET reads T#0s, T#100ms, T#200ms, then T#250ms with Q TRUE, but that fourth scan happened 300 ms after the timer started. ET is clamped to PT, so the online view shows 250 ms while the process actually waited 300. The error is anywhere between zero and one scan, every time.

On a 100 ms task that is a big error; on a 10 ms TwinCAT task it is 10 ms. On a Siemens OB1 that free-runs between 4 ms and 15 ms depending on what else the program is doing, it varies from scan to scan. Here is a 50 ms TON on a free-running cycle, with the kind of cycle times a busy S7-1500 reports in its online diagnostics:

CallCycle before itTime since the edgeETQ
10 msT#0msFALSE
28 ms8 msT#8msFALSE
312 ms20 msT#20msFALSE
49 ms29 msT#29msFALSE
515 ms44 msT#44msFALSE
67 ms51 msT#50msTRUE

This time the delay came out at 51 ms. With a different run of cycle times (12, 15, 14, 13 ms) it would be 54 ms. A free-running cycle makes the error random as well as one-sided. Three rules follow from this.

Timer resolution is your task cycle. The TIME value can be written to the millisecond, but the result is only as fine as the scan that reads it.

Don't time anything shorter than about five scans. A 30 ms delay on a 10 ms task can be 30 or 40 ms, a 33 % error, and on a free-running cycle it can be anything in between.

For real precision, use the hardware. Cam outputs, high-speed counter modules, timestamped inputs and fast cyclic tasks exist because the cyclic scan cannot do sub-millisecond timing. On an S7-1500 that means a cyclic interrupt OB or a technology module; on TwinCAT, a second task with a shorter cycle time; on a ControlLogix, a periodic task.

The whole reaction, end to end

The timer's own error is only one link in the chain. When a field signal changes, the controller does not see it until the input module has filtered it and the next scan has read the input image. When Q changes, the field does not see it until the output image is written and the output module switches. For a 500 ms confirmation on a 10 ms task, the worst case adds up like this:

LinkBest caseWorst case
Input module filter (default delay)3 ms3 ms
Waiting for the next input image read0 ms10 ms
The TON itself500 ms510 ms
Waiting for the output image to be written0 ms10 ms
Output module switchingunder 1 msunder 1 ms
Totalabout 503 msabout 534 ms

Thirty milliseconds of spread on a half-second confirmation does not matter. The same arithmetic on a 50 ms reject pulse does. The figures for the module are typical, so take your own from the module's data sheet and from the configured input delay in its hardware settings. And add the network update time if the I/O sits on PROFINET or EtherNet/IP rather than in the rack.

Older platforms had coarser timers still. The S5-style timers that Siemens still supports on S7-300/400 use an S5T# format with a time base of 10 ms, 100 ms, 1 s or 10 s, chosen automatically from the value, and a largest preset of S5T#2H46M30S. IEC timers (TON with T# presets) run on a millisecond clock on every current platform, which is one more reason to use them.

Restarting a timer and one instance per purpose

Sometimes you want a timer to start again from zero while its condition is still TRUE. A jam monitor on a carton conveyor is the classic case: while the belt runs, a carton should pass the photo-eye at least every 3 s. Each carton restarts the timer; if it ever finishes, the line has jammed or starved.

To restart a TON you must give it IN = FALSE on at least one call. The cleanest way is to call the instance twice in the same scan: once with IN := FALSE when the restart condition is true, then the normal call.

PROGRAM Main
VAR
    Running    : BOOL;   (* input: conveyor running feedback *)
    CartonEye  : BOOL;   (* input: photo-eye, TRUE = carton in the beam *)
    CartonEdge : R_TRIG;
    GapTimer   : TON;
    Jam        : BOOL;   (* output: no carton for 3 s *)
END_VAR

CartonEdge(CLK := CartonEye);
IF CartonEdge.Q THEN
    GapTimer(IN := FALSE);            (* a carton arrived: restart *)
END_IF;
GapTimer(IN := Running, PT := T#3s);  (* the normal call, every scan *)

Jam := GapTimer.Q;

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

Turn Running on and toggle CartonEye every second or so: ET keeps falling back to zero and Jam never comes on. Stop toggling and the jam appears 3 s after the last carton. The restart call is conditional, but the normal call is not, so the rule from earlier still holds. The alternative, GapTimer(IN := Running AND NOT CartonEdge.Q, ...), also works and costs one scan of timing; pick one style and keep it across the project.

The opposite mistake is sharing one instance between two jobs. Somebody needs a 5 s open-timeout on two valves and writes:

ValveTimer(IN := ValveA_Cmd AND NOT ValveA_Open, PT := T#5s);
ValveA_Fault := ValveTimer.Q;
ValveTimer(IN := ValveB_Cmd AND NOT ValveB_Open, PT := T#5s);   (* same instance! *)
ValveB_Fault := ValveTimer.Q;

Each call overwrites the other. When valve A sticks and valve B is closed, the second call resets the timer every scan, so ValveA_Fault never appears. An instance is a physical timer, so give each job its own: declare ValveA_OpenTimeout and ValveB_OpenTimeout. When there are twelve identical valves, wrap one valve and its timer in a function block (chapter 18) so every instance carries its own timer automatically.

When the preset changes

Presets rarely stay fixed for the life of a machine. An operator lengthens a mixing time from the HMI, a recipe loads a new dwell, a technician trims a confirmation delay during commissioning. What happens to a timer that is already running when its PT changes?

The standard does not pin this down, and implementations differ. The engine on these pages, like CODESYS-style libraries, compares ET with whatever PT is on each call. Lower the preset below the time already elapsed and Q goes TRUE on the next call; raise it and the running timer simply has further to go.

When a change must not affect the cycle in progress, latch the preset at the start of the cycle and feed the timer from the copy:

IF CycleStart THEN
    ActiveMixTime := HmiMixTime;   (* taken once, when the batch starts *)
END_IF;
MixTimer(IN := Mixing, PT := ActiveMixTime);

Clamp anything that comes from an HMI, too. A preset of T#0s makes a TON finish on the same call that IN rises, which turns a confirmation delay into no delay at all, and an operator who types 600 meaning seconds into a field that expects minutes can stall a line for ten hours. Limit the value to a sensible range where it enters the program, next to the conversion from the HMI's integer.

TIME literals

PT is a TIME value, and Structured Text writes durations as literals with a T# (or the longer TIME#) prefix. The units can be combined, upper or lower case, with optional underscores for readability:

LiteralDuration
T#500mshalf a second
T#2stwo seconds
T#1.5sone and a half seconds (decimal on the last unit)
T#1m30sninety seconds
T#1h_30man hour and a half
TIME#2htwo hours, long prefix
T#1d2htwenty-six hours

TIME is a 32-bit count of milliseconds, but the platforms disagree about the sign. On an S7-1200 or S7-1500 it is signed, so the largest value is T#24d20h31m23s647ms. On CODESYS and TwinCAT it is unsigned, up to T#49d17h2m47s295ms. Either is plenty for delays and far too little for a running-hours meter, which should be a DINT of seconds or an LTIME, the 64-bit nanosecond type from the third edition of IEC 61131-3 that CODESYS, TwinCAT and the S7-1500 all support. Chapter 16 covers TIME arithmetic and what subtraction does when the result would be negative.

Presets rarely stay hard-coded. An operator wants to adjust the mixing delay from the HMI, and HMI fields are usually plain integers. Convert at the boundary with PT := DINT_TO_TIME(MixDelayMs); and keep TIME everywhere inside the program. You can also do arithmetic and comparisons on TIME values, so Remaining := MixTimer.PT - MixTimer.ET; gives the operator a countdown, and IF MixTimer.ET > T#20s THEN lets one timer drive several thresholds. Rockwell's TONR has no TIME type at all: PRE and ACC are DINT milliseconds, so MyTimer.PRE := 3000; is three seconds.

Reviewing timer code

When I review someone else's program, timers are one of the first things I search for, because their bugs pass every bench test and show up on the machine. A cross-reference listing does most of the work: TIA Portal, Studio 5000 and CODESYS all have one, and on each it shows every place an instance is called and every place its outputs are read. A healthy timer has exactly one call and a handful of reads. Two calls means a restart (fine, if it comes right before the normal call) or a shared instance (a bug). A call inside an IF, a CASE branch or a conditionally called block is a bug waiting for the right sequence of events.

Then read each preset and ask where the number came from. "3 s" with a comment that says "start-up swirl at the low-level switch, measured at commissioning" is a design decision. "3 s" with no comment is a guess that nobody will dare to change, and a preset of T#10ms on a 10 ms task is no delay at all.

Next: holding on and pulsing

A TON answers one question: has this been true for long enough? Two other questions come up just as often. One is "keep this running for a while after the signal goes away", like a cooling fan after a heater stops. The other is "give me exactly this long, however long the button is held", like a fixed shot of lubricant. Those are the off-delay TOF and the pulse timer TP, and with two TONs you can also build the flasher that every beacon on the plant floor needs. Chapter 10 covers all three.

Practise this chapter