Intermediate
PLC Timer Programming: TON, TOF, and TP Timer Examples in Structured Text
Timers are essential in every PLC program. Learn how TON, TOF, and TP timers work with practical Structured Text examples.
Timers in a PLC program
Timers are among the most used function blocks in industrial automation. Delaying a motor start, debouncing a sensor, running a fan on after a heater stops: nearly every PLC program has at least one.
IEC 61131-3 defines three timer types:
| Timer | Name | Purpose |
|---|---|---|
| TON | On-Delay Timer | Output turns ON after a set delay |
| TOF | Off-Delay Timer | Output stays ON for a set delay after input turns OFF |
| TP | Pulse Timer | Generates a fixed-duration pulse |
TON: on-delay timer
TON is the timer you will write most often. Its output (Q) goes TRUE only after the input (IN) has been TRUE for the preset time (PT).
VAR
startDelay : TON;
startButton : BOOL := FALSE;
motorReady : BOOL := FALSE;
END_VAR
startDelay(IN := startButton, PT := T#3s);
motorReady := startDelay.Q;
// motorReady becomes TRUE 3 seconds after startButton is pressed
Example: motor start delay
Motors often need a start delay, to give the lubrication system time to build pressure or to let the interlocks confirm:
PROGRAM MotorStartSequence
VAR
startCmd : BOOL;
lubePressureOK : BOOL;
guardsClosed : BOOL;
startDelay : TON;
motorRun : BOOL;
END_VAR
// Only start timing when all safety conditions are met
startDelay(
IN := startCmd AND lubePressureOK AND guardsClosed,
PT := T#5s
);
motorRun := startDelay.Q;
END_PROGRAM
Here guardsClosed is a start permissive in standard logic. The guard's safety function itself belongs in a safety relay or safety PLC, not in this timer.
TOF: off-delay timer
TOF holds the output ON for a set time after the input turns OFF. Typical uses are running a cooling fan on after a heater turns off, keeping lights on after a motion sensor drops out, and keeping a pump running for a while after a level switch drops so the line drains.
VAR
fanDelay : TOF;
heaterOn : BOOL := FALSE;
coolingFan : BOOL := FALSE;
END_VAR
fanDelay(IN := heaterOn, PT := T#30s);
coolingFan := fanDelay.Q;
// Fan stays on for 30 seconds after heater turns off
TP: pulse timer
TP produces a single pulse of fixed length, however long the input stays ON:
VAR
alarm : TP;
faultDetected : BOOL := FALSE;
alarmHorn : BOOL := FALSE;
END_VAR
alarm(IN := faultDetected, PT := T#10s);
alarmHorn := alarm.Q;
// Horn sounds for exactly 10 seconds when a fault is detected
Cascading timers
Applications often chain timers to run steps one after another:
VAR
step1Timer : TON;
step2Timer : TON;
step3Timer : TON;
startSequence : BOOL;
step1Done, step2Done, step3Done : BOOL;
END_VAR
step1Timer(IN := startSequence, PT := T#2s);
step1Done := step1Timer.Q;
step2Timer(IN := step1Done, PT := T#3s);
step2Done := step2Timer.Q;
step3Timer(IN := step2Done, PT := T#1s);
step3Done := step3Timer.Q;
Try the examples
Open the PLC simulator and run the timer examples above. The simulator supports all three IEC 61131-3 timer types and shows timer states and elapsed times live.
What the scan cycle does to a timer
A TON with PT := T#10ms in a task that runs every 20 ms does not give you 10 ms. The block is evaluated only when it is called, so Q can change only on a task boundary. Your real resolution is the task period plus scheduler jitter under load. Anything tighter belongs in a fast task, a high-speed input channel or the drive.
Runtimes also disagree about how they measure time. Some timestamp against a free-running system clock; others add the task period on each call. The two behave the same while you call the block every scan and diverge as soon as you don't: one catches up at once, the other loses the time. Find out which kind you have before you port code between platforms.
TIME is almost always a 32-bit millisecond count. TIA Portal treats it as a signed DINT, topping out at T#24d20h31m23s647ms. CODESYS-derived runtimes treat it as unsigned, which gives roughly 49 days. Anything longer than that is a job for a counter, not a timer.
Retentive behaviour is a vendor extension
None of TON, TOF or TP keeps its time across an interruption, but each discards it differently, and you need to know which way. TON zeroes ET the instant IN drops. TOF works the other way round: the falling edge of IN is what starts its timing, and ET is cleared when IN goes TRUE again. TP ignores IN entirely once the pulse has started. Drop IN halfway through and Q stays TRUE while ET keeps counting until it reaches PT.
What none of them offers is retention. IEC 61131-3 has no retentive timer in its standard block set. Rockwell's RTO (cleared by a RES instruction) and Siemens' TONR (cleared by its R input) are vendor extensions, and neither is portable. For accumulated runtime that has to survive stops and power cycles, build it from a TIME variable in retain memory.
The conditional call is the defect you will ship
The most common timer bug in commissioned code is a timer instance inside an IF that sometimes does not execute. It does the most damage with TOF and TP, because both still need to be called after IN goes FALSE. TOF does not even start timing until that falling edge, and TP has to keep running to finish its pulse and clear ET. Write IF heaterOn THEN fanDelay(IN := TRUE, PT := T#30s); END_IF and the block never sees the falling edge. Q freezes at its last value, the cross-reference looks clean, and the fan runs until someone cycles power.
The rule that holds up on real machines: call every timer instance exactly once per scan, unconditionally, and put the condition in IN. Give the timers their own block near the top of the POU, and let the logic underneath only read .Q and .ET.
Debounce both edges or don't bother
A single TON filters the make and passes the break straight through, and contacts bounce on release too. Use a symmetric filter with separate on and off times.
VAR
xRaw : BOOL; // raw DI from a hard-wired limit switch
xClean : BOOL;
tOnDly : TON;
tOffDly : TOF;
END_VAR
tOnDly (IN := xRaw, PT := T#20ms); // must be solid for 20 ms to make
tOffDly(IN := xRaw, PT := T#50ms); // must be gone for 50 ms to break
// set on the delayed make, hold until the off-delay expires
xClean := tOnDly.Q OR (xClean AND tOffDly.Q);
The filter can never be finer than the task period, and bounce shorter than one scan may never reach your code at all. Most DI modules have a configurable hardware input delay, and that is where fast chatter should be filtered. Never debounce a safety input in standard logic.
Heartbeats that catch a dead peer
The classic mistake is watching a heartbeat bit with a level check. A partner that crashes leaves the bit frozen at whatever value it had, and a level check cannot tell frozen from live, so a bit stuck TRUE reads as healthy forever. Watch a value that has to change.
VAR
wHbIn : WORD; // free-running counter from the remote node
wHbLast : WORD;
tNoChange : TON;
tStartGrace : TON;
xCommsOK : BOOL;
END_VAR
tStartGrace(IN := TRUE, PT := T#5s); // free-runs from the first scan
tNoChange(IN := (wHbIn = wHbLast) AND tStartGrace.Q, PT := T#1s);
wHbLast := wHbIn;
xCommsOK := NOT tNoChange.Q;
Comparing for equality instead of using > means counter rollover causes no false alarm. The grace timer stops the watchdog faulting the machine before the first telegram arrives; without it, every cold start looks like a comms failure. Set PT to several times the nominal update period, with three to five as a common starting point. Keep the watchdog in a task that cannot be starved, too. Parked beside your recipe maths, it gets stretched by heavy load well past the timeout you configured.
Sequencers: time as a guard, not as a transition
Dead-reckoning a sequence (energise the clamp, wait 2 s, assume it closed) is how a machine keeps running with a failed proximity switch or a starved air supply, right up until it wrecks a part. Transition on feedback and let the timer fault the step. Use time as the transition only where there is nothing to measure, such as a dwell, a cure or a glue bead, and make those times HMI parameters, because commissioning will change them twenty times.
The other trap is one timer instance per step. Each one is more state to clear, and re-entering a step with a stale instance is where "it fired instantly the second time" comes from. One shared step timer, reset when the step number changes, is smaller and documents itself.
FUNCTION_BLOCK FB_ClampStation
VAR_INPUT
xEnable : BOOL;
xStart : BOOL;
xClosedFB : BOOL; // clamp-closed proximity
xOpenFB : BOOL; // clamp-open proximity
tMoveTimeout : TIME := T#2s; // HMI parameter
tDwell : TIME := T#500ms; // HMI parameter
END_VAR
VAR_OUTPUT
xClampSol : BOOL;
xCycleDone : BOOL;
xTimeout : BOOL;
iFaultStep : INT;
END_VAR
VAR
iStep : INT := 0;
iStepLast : INT := 0;
xStepChanged : BOOL;
tStepPT : TIME := T#0s;
tmrStep : TON;
END_VAR
// --- timer block: one unconditional call, ahead of all decision logic ---
xStepChanged := (iStep <> iStepLast);
iStepLast := iStep;
tmrStep(IN := xEnable AND (iStep > 0) AND (iStep < 900) AND NOT xStepChanged,
PT := tStepPT);
// --- state machine: reads tmrStep.Q, never calls it ---
CASE iStep OF
0: // idle
xClampSol := FALSE;
IF xEnable AND xStart THEN
xCycleDone := FALSE;
iStep := 10;
END_IF;
10: // closing - feedback decides, the timer only guards
tStepPT := tMoveTimeout;
xClampSol := TRUE;
IF xClosedFB THEN
iStep := 20;
ELSIF tmrStep.Q THEN
iFaultStep := iStep;
iStep := 900;
END_IF;
20: // dwell - nothing to measure, so time IS the transition
tStepPT := tDwell;
IF tmrStep.Q THEN
iStep := 30;
END_IF;
30: // opening
tStepPT := tMoveTimeout;
xClampSol := FALSE;
IF xOpenFB THEN
xCycleDone := TRUE;
iStep := 0;
ELSIF tmrStep.Q THEN
iFaultStep := iStep;
iStep := 900;
END_IF;
900: // latched timeout fault - cleared by dropping xEnable
xClampSol := FALSE;
xTimeout := TRUE;
IF NOT xEnable THEN
xTimeout := FALSE;
iStep := 0;
END_IF;
END_CASE;
END_FUNCTION_BLOCK
xStepChanged deliberately holds IN FALSE for one scan on every transition. That guarantees the TON a clean FALSE-to-TRUE edge, a zeroed ET and a tStepPT that has already been written. The cost is one task period per step, which is cheaper than a chain of dedicated timers, where every stage adds its own scan of latency and one upstream drop collapses the whole chain.