Chapter 9 / 24 · 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.

A timer is a block you call, not a line you write

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. That two seconds is 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, but get into the habit of passing IN and PT on every call so the line tells the whole story.

IN, PT, Q and ET, scan by scan

A TON has three rules, and everything else follows 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. That is precisely what makes it 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

Call the timer on every scan

The timer only does anything when its instance is called. Between calls it is frozen memory. So the single most important rule for timers in ST is: 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 typical 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. Other platforms may freeze instead. 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. Keep timer calls at the top level of the program, near the logic that reads them.

Vendors package the same idea 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. 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: 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.

Confirmation delays

Debouncing cleans up a signal that is electrically noisy. A confirmation delay is different: 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. Long enough to ride out the start-up swirl, short enough to save the seal.

The same pattern runs half of every alarm list. A high-temperature alarm on an extruder barrel that must persist for 10 s. A motor feedback that must arrive within 2 s of the contactor command, or the motor is declared failed. A door switch that 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.

Two habits make these delays maintainable. Name the instance after the condition, not the timer: LowLevelConfirm, not T7. 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.

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. Three practical rules follow:

  • 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.
  • 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.
  • For real precision, use the hardware. Cam outputs, high-speed counter modules, timestamped inputs and fast tasks exist because the cyclic scan cannot do sub-millisecond timing.

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

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; one timer, one job. 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.

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

On most platforms TIME is a signed 32-bit count of milliseconds, so the largest value is T#24d20h31m23s647ms. That 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.

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

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. Keep this running for a while after the signal goes away — a cooling fan after a heater stops. Give me exactly this long, no matter how long the button is held — a fixed squirt 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