Skip to main content

ControlForge Timers, Counters & Function Blocks Guide

James M. Belcher Founder, JMB Technical Services LLC April 2026 | ControlForge v1.0.535


1. Overview

ControlForge implements 14 IEC 61131-3 standard function blocks. Unlike plain functions, function blocks retain state between scan cycles — a timer remembers how long it has been running, a counter remembers its count, a PID controller remembers its integral term.

CategoryFunction BlocksDescription
TimersTON, TOF, TP, RTOTime-based delays, pulses, and accumulation
CountersCTU, CTD, CTUDEvent counting (up, down, bidirectional)
BistablesSR, RSSet/reset latches with priority control
Edge TriggersR_TRIG, F_TRIGRising and falling edge detection
PID ControllersPID, PIDEProportional-Integral-Derivative feedback control

How Function Blocks Work in ST

Function blocks are declared as variables, then called with named parameters:

PROGRAM POU_Example
VAR
myTimer : TON; (* Declare instance *)
startButton : BOOL;
output : BOOL;
END_VAR

myTimer(IN := startButton, PT := T#5s); (* Call with inputs *)
output := myTimer.Q; (* Read outputs *)
END_PROGRAM

Each instance maintains its own state. You can have multiple instances of the same type:

VAR
pumpDelay : TON;
fanDelay : TON;
alarmDelay : TON;
END_VAR

2. Timers

2.1 TON — Timer On-Delay

Output Q goes TRUE after IN has been TRUE continuously for PT duration. If IN goes FALSE before PT expires, the timer resets.

VAR
startDelay : TON;
END_VAR

startDelay(IN := startButton, PT := T#3s);

IF startDelay.Q THEN
(* Button held for 3 seconds — start motor *)
motor := TRUE;
END_IF;

Inputs:

ParameterTypeDescription
INBOOLEnable input — timer runs while TRUE
PTTIMEPreset time (e.g., T#3s, T#500ms, T#1h30m)
RBOOLReset — clears elapsed time and output

Outputs:

ParameterTypeDescription
QBOOLOutput — TRUE when elapsed >= PT
ETTIMEElapsed time (capped at PT)
DNBOOLDone — TRUE when timing complete
TTBOOLTimer Timing — TRUE while actively counting
ENBOOLEnabled — mirrors IN

Timing Diagram:

IN: ──┐ ┌──────────────────────┐ ┌───
└─────┘ └─────┘
PT: ======== (3 seconds)

Q: ─────────────────┐ ┌─────────
└─────────────┘
<--- 3s --->
IN went FALSE Q stays FALSE
before PT, until IN has been
timer reset TRUE for full PT

2.2 TOF — Timer Off-Delay

Output Q goes TRUE immediately when IN goes TRUE. When IN goes FALSE, Q stays TRUE for PT duration before going FALSE.

VAR
coolDown : TOF;
END_VAR

coolDown(IN := runCommand, PT := T#10s);

(* Fan keeps running 10 seconds after run command stops *)
fan := coolDown.Q;

Inputs:

ParameterTypeDescription
INBOOLEnable input
PTTIMEOff-delay duration
RBOOLReset

Outputs: Same as TON (Q, ET, DN, TT, EN).

Timing Diagram:

IN: ──┐ ┌────────────┐ ┌───
└─────┘ └─────┘

Q: ──┐ ┌────────────┐ ┌───
└─────┘ └───────────┘
<-- 10s -->
Q holds TRUE
after IN falls

2.3 TP — Timer Pulse

Generates a fixed-width pulse on the rising edge of IN. Not retriggerable — if IN pulses again during an active pulse, it is ignored.

VAR
oneShot : TP;
END_VAR

oneShot(IN := trigger, PT := T#200ms);

(* 200ms pulse on every rising edge of trigger *)
solenoid := oneShot.Q;

Inputs:

ParameterTypeDescription
INBOOLTrigger input (rising edge starts pulse)
PTTIMEPulse width

Outputs: Same as TON (Q, ET, DN, TT, EN).

Timing Diagram:

IN: ──┐ ┌──┐ ┌──┐
└─┘ └─────┘ └───
^ignored (pulse active)

Q: ──┐ ┌──────────┐
└────────────────┘ └───
<---- 200ms ----> <-200ms->

2.4 RTO — Retentive Timer On-Delay

Accumulates time while IN is TRUE. Unlike TON, it does not reset when IN goes FALSE — accumulated time is retained. Only an explicit R (reset) input clears the timer.

VAR
runHours : RTO;
totalRuntime : TIME;
END_VAR

runHours(IN := motorRunning, PT := T#8h, R := resetBtn);

totalRuntime := runHours.ET;

IF runHours.Q THEN
(* Motor has accumulated 8 hours of runtime — schedule maintenance *)
maintenanceDue := TRUE;
END_IF;

Inputs:

ParameterTypeDescription
INBOOLEnable — accumulates time while TRUE
PTTIMEPreset time (total accumulation target)
RBOOLReset — clears accumulated time and output

Outputs: Same as TON (Q, ET, DN, TT, EN).

Timing Diagram:

IN: ──┐ ┌──┐ ┌──────────────
└─────┘ └─────┘

ET: 0 2s 2s 4s 4s 6s 8s...
^retained ^retained
when IN when IN
goes FALSE goes FALSE

Q: ──────────────────────────┐
└── (Q stays TRUE until R)
ET >= PT

3. Counters

3.1 CTU — Count Up

Increments CV on each rising edge of CU. Q becomes TRUE when CV reaches PV.

VAR
partCount : CTU;
END_VAR

partCount(CU := proxSensor, PV := 100, R := resetBtn);

IF partCount.Q THEN
(* 100 parts counted — signal batch complete *)
batchDone := TRUE;
END_IF;

currentCount := partCount.CV;

Inputs:

ParameterTypeDescription
CUBOOLCount Up — rising edge increments CV
PVINTPreset value (target count)
RBOOLReset — sets CV to 0
LDBOOLLoad — loads PV into CV

Outputs:

ParameterTypeDescription
QBOOLOutput — TRUE when CV >= PV
CVINTCurrent count value

3.2 CTD — Count Down

Decrements CV on each rising edge of CD. Q becomes TRUE when CV reaches 0.

VAR
remaining : CTD;
END_VAR

remaining(CD := dispenseSensor, PV := 50, LD := loadBtn);

IF remaining.Q THEN
(* All items dispensed *)
hopperEmpty := TRUE;
END_IF;

itemsLeft := remaining.CV;

Inputs:

ParameterTypeDescription
CDBOOLCount Down — rising edge decrements CV
PVINTPreset value (loaded by LD)
RBOOLReset — sets CV to 0
LDBOOLLoad — loads PV into CV

Outputs:

ParameterTypeDescription
QBOOLOutput — TRUE when CV <= 0
CVINTCurrent count value

3.3 CTUD — Count Up/Down

Bidirectional counter with separate up and down inputs.

VAR
position : CTUD;
END_VAR

position(CU := forwardPulse, CD := reversePulse, PV := 1000, R := homeBtn);

atUpperLimit := position.QU; (* CV >= 1000 *)
atLowerLimit := position.QD; (* CV <= 0 *)
currentPos := position.CV;

Inputs:

ParameterTypeDescription
CUBOOLCount Up — rising edge increments CV
CDBOOLCount Down — rising edge decrements CV
PVINTPreset value (upper threshold)
RBOOLReset — sets CV to 0
LDBOOLLoad — loads PV into CV

Outputs:

ParameterTypeDescription
QUBOOLUpper limit — TRUE when CV >= PV
QDBOOLLower limit — TRUE when CV <= 0
CVINTCurrent count value

4. Bistables (Latches)

4.1 SR — Set-Reset (Set Dominant)

When both S1 and R are TRUE, Set wins — Q1 stays TRUE.

VAR
latch : SR;
END_VAR

latch(S1 := startBtn, R := stopBtn);
motorEnabled := latch.Q1;
InputTypeDescription
S1BOOLSet (dominant)
RBOOLReset
OutputTypeDescription
Q1BOOLLatched output (retained)

Logic: Q1 := S1 OR (NOT R AND Q1)


4.2 RS — Reset-Set (Reset Dominant)

When both S and R1 are TRUE, Reset wins — Q1 goes FALSE. Safer for emergency stop circuits.

VAR
safeLatch : RS;
END_VAR

safeLatch(S := runPermit, R1 := eStop);
motorAllowed := safeLatch.Q1;
InputTypeDescription
SBOOLSet
R1BOOLReset (dominant)
OutputTypeDescription
Q1BOOLLatched output (retained)

Logic: Q1 := NOT R1 AND (S OR Q1)

Safety: Use RS (reset-dominant) for safety-critical latches. An E-stop should always be able to override a run command, even if both signals are active simultaneously.


5. Edge Triggers

5.1 R_TRIG — Rising Edge Detector

Output Q is TRUE for exactly one scan when CLK transitions from FALSE to TRUE.

VAR
riseDetect : R_TRIG;
END_VAR

riseDetect(CLK := inputSignal);

IF riseDetect.Q THEN
(* Rising edge detected — execute once *)
batchCount := batchCount + 1;
END_IF;
InputTypeDescription
CLKBOOLSignal to monitor
OutputTypeDescription
QBOOLTRUE for one scan on rising edge

5.2 F_TRIG — Falling Edge Detector

Output Q is TRUE for exactly one scan when CLK transitions from TRUE to FALSE.

VAR
fallDetect : F_TRIG;
END_VAR

fallDetect(CLK := inputSignal);

IF fallDetect.Q THEN
(* Falling edge detected — signal just went off *)
offCount := offCount + 1;
END_IF;
InputTypeDescription
CLKBOOLSignal to monitor
OutputTypeDescription
QBOOLTRUE for one scan on falling edge

6. PID Controllers

6.1 PID — Standard PID Controller

Proportional-Integral-Derivative feedback controller with anti-windup.

VAR
tempPID : PID;
heaterOutput : REAL;
END_VAR

tempPID(
EN := TRUE,
PV := actualTemp, (* Measured temperature *)
SP := setpointTemp, (* Desired temperature *)
KP := 10.0, (* Proportional gain *)
KI := 0.5, (* Integral gain *)
KD := 2.0, (* Derivative gain *)
CYCLE := 0.1, (* Scan time in seconds *)
MN := 0.0, (* Min output *)
MX := 100.0 (* Max output *)
);

heaterOutput := tempPID.CV;

Inputs:

ParameterTypeDefaultDescription
ENBOOLTRUEEnable
PVREALrequiredProcess variable (measurement)
SPREALrequiredSetpoint (target)
KPREAL1.0Proportional gain
KIREAL0.0Integral gain
KDREAL0.0Derivative gain
CYCLEREAL0.1Scan time in seconds
MNREAL0.0Minimum output
MXREAL100.0Maximum output
MRBOOLFALSEManual reset (clears integral)

Outputs:

ParameterTypeDescription
CVREALControl variable (calculated output)
EREALError (SP - PV)

Algorithm:

Error = SP - PV
P = KP * Error
I = KI * accumulated_integral
D = KD * (Error - prevError) / CYCLE
CV = CLAMP(P + I + D, MN, MX)

Anti-windup prevents the integral term from growing when the output is saturated at MN or MX.


6.2 PIDE — Enhanced PID (Rockwell-Style)

Extended PID with feed-forward, output bias, manual mode, setpoint limits, and alarm thresholds.

VAR
reactorPID : PIDE;
END_VAR

reactorPID(
EN := TRUE,
PV := reactorTemp,
SP := 180.0,
KP := 5.0,
KI := 0.2,
KD := 1.0,
CYCLE := 0.1,
FF := steamFlow * 0.5, (* Feed-forward from steam *)
BIAS := 10.0, (* Output offset *)
MAXO := 100.0, (* Max output *)
MINO := 0.0, (* Min output *)
MAXS := 200.0, (* Max setpoint *)
MINS := 50.0 (* Min setpoint *)
);

valveOutput := reactorPID.CV;
spClamped := reactorPID.SPH OR reactorPID.SPL;

Additional Inputs (beyond PID):

ParameterTypeDefaultDescription
FFREAL0.0Feed-forward term (added directly to output)
BIASREAL0.0Output bias/offset
MAXOREAL100.0Maximum output
MINOREAL0.0Minimum output
MAXSREAL0.0Maximum setpoint limit
MINSREAL0.0Minimum setpoint limit
MAXIREAL100.0Maximum integral accumulation
MINIREAL-100.0Minimum integral accumulation
DPTSBOOLFALSEDependent gains mode
MOBOOLFALSEManual output mode (CV = MOCV)
MOCVREAL0.0Manual CV value
INIMANBOOLFALSEInitialize integral from MOCV

Additional Outputs:

ParameterTypeDescription
SPHBOOLSetpoint clamped to MAXS
SPLBOOLSetpoint clamped to MINS
PVHHBOOLPV high-high alarm
PVHBOOLPV high alarm
PVLBOOLPV low alarm
PVLLBOOLPV low-low alarm

Key Differences from PID:

  • Derivative is calculated on PV (not error) to avoid derivative kick on setpoint changes
  • Feed-forward term for measurable disturbance rejection
  • Setpoint clamping with limit flags
  • Manual mode for bumpless transfer between auto and manual
  • Bounded integral accumulation (separate from output limits)

7. TIME Literals

All timer presets use IEC 61131-3 TIME literals:

LiteralDuration
T#500ms500 milliseconds
T#1s1 second
T#5s5 seconds
T#1m30s1 minute 30 seconds
T#1h1 hour
T#1h30m1 hour 30 minutes
T#2d2 days
T#100ms100 milliseconds
T#10us10 microseconds

8. Complete Example: Pump Station

A realistic pump control program using timers, counters, edge triggers, and PID.

PROGRAM POU_PumpStation
VAR
(* Inputs *)
startBtn : BOOL;
stopBtn : BOOL;
eStop : BOOL;
levelSensor : REAL; (* 0-100% *)
flowSensor : REAL; (* GPM *)

(* Function block instances *)
runLatch : RS; (* Reset-dominant for safety *)
startDelay : TON; (* Anti-short-cycle delay *)
runTimer : RTO; (* Accumulate total run hours *)
cycleCount : CTU; (* Count start/stop cycles *)
startEdge : R_TRIG; (* Detect start events *)
levelPID : PID; (* Level control *)
dryRunTimer : TON; (* Dry run protection *)

(* Outputs *)
pumpRun : BOOL;
vfdSpeed : REAL;
maintenanceDue : BOOL;
dryRunFault : BOOL;
END_VAR

(* Safety latch — E-stop always wins *)
runLatch(S := startBtn AND NOT dryRunFault, R1 := stopBtn OR eStop);

(* Anti-short-cycle: must wait 30s between starts *)
startDelay(IN := NOT runLatch.Q1, PT := T#30s);
pumpRun := runLatch.Q1 AND startDelay.Q;

(* Count start events *)
startEdge(CLK := pumpRun);
cycleCount(CU := startEdge.Q, PV := 10000, R := FALSE);

(* Accumulate runtime for maintenance scheduling *)
runTimer(IN := pumpRun, PT := T#2000h, R := FALSE);
maintenanceDue := runTimer.Q;

(* Level PID — controls VFD speed *)
levelPID(
EN := pumpRun,
PV := levelSensor,
SP := 75.0, (* Maintain 75% level *)
KP := 5.0,
KI := 0.3,
KD := 0.5,
CYCLE := 0.05, (* 50ms scan *)
MN := 20.0, (* Min speed 20% *)
MX := 100.0 (* Max speed 100% *)
);
vfdSpeed := levelPID.CV;

(* Dry run protection: fault if running with no flow for 10s *)
dryRunTimer(IN := pumpRun AND (flowSensor < 1.0), PT := T#10s);
IF dryRunTimer.Q THEN
dryRunFault := TRUE; (* Latches until operator clears *)
END_IF;

END_PROGRAM

Appendix A: Quick Reference

Timers

FBPurposeKey I/O
TONOn-delayIN + PT → Q after delay
TOFOff-delayQ holds TRUE for PT after IN falls
TPPulseFixed-width pulse on rising edge
RTORetentive on-delayAccumulates time, retains on IN=FALSE, needs R to clear

Counters

FBPurposeKey I/O
CTUCount upCU rising edge → CV++, Q when CV >= PV
CTDCount downCD rising edge → CV--, Q when CV <= 0
CTUDUp/downCU/CD edges, QU (upper), QD (lower)

Bistables

FBPurposePriority
SRSet-Reset latchSet dominant
RSReset-Set latchReset dominant (use for safety)

Edge Triggers

FBPurposeOutput
R_TRIGRising edgeQ = TRUE for one scan on FALSE→TRUE
F_TRIGFalling edgeQ = TRUE for one scan on TRUE→FALSE

PID

FBPurposeKey Features
PIDStandard PIDKP/KI/KD, anti-windup, output clamping
PIDEEnhanced PIDFeed-forward, bias, manual mode, SP limits, alarms

ControlForge v1.0.535 | IEC 61131-3 Function Blocks | Timers, Counters, PID

© 2026 JMB Technical Services LLC. All rights reserved. Back to All Guides