Advanced
PLC State Machine Design Patterns in Structured Text
Design robust, maintainable PLC programs using state machine patterns in Structured Text — the professional approach to sequential control.
Why sequence logic ends up as a state machine
Every complex PLC program ends up behaving as a state machine, whether you design it as one or not. If you don't build that structure on purpose, the program turns into a tangle of interlocking BOOLs that is nearly impossible to debug.
What a deliberate state machine gives you
The machine is always in exactly one known state, so its behaviour is predictable, and you can check a single variable online to see what it is doing. Each state spells out what is allowed to happen in it, so transitions are explicit rather than implied by a combination of flags. Adding a new state doesn't break the existing logic, and the list of states doubles as the specification of the sequence.
A basic state machine with CASE
The simplest and most common PLC state machine is an INT variable driving a CASE statement:
PROGRAM SimpleStateMachine
VAR
State : INT := 0;
StartButton : BOOL := FALSE;
Sensor1 : BOOL := FALSE;
Sensor2 : BOOL := FALSE;
Cylinder1 : BOOL := FALSE;
Cylinder2 : BOOL := FALSE;
DoneFlag : BOOL := FALSE;
END_VAR
CASE State OF
0: // IDLE
Cylinder1 := FALSE;
Cylinder2 := FALSE;
DoneFlag := FALSE;
IF StartButton THEN
State := 10;
END_IF;
10: // EXTEND CYLINDER 1
Cylinder1 := TRUE;
IF Sensor1 THEN
State := 20;
END_IF;
20: // EXTEND CYLINDER 2
Cylinder2 := TRUE;
IF Sensor2 THEN
State := 30;
END_IF;
30: // RETRACT ALL
Cylinder1 := FALSE;
Cylinder2 := FALSE;
IF NOT Sensor1 AND NOT Sensor2 THEN
State := 40;
END_IF;
40: // DONE
DoneFlag := TRUE;
IF NOT StartButton THEN
State := 0;
END_IF;
END_CASE;
Why the states are numbered 0, 10, 20
Leaving gaps (10, 20, 30) instead of numbering 1, 2, 3 lets you insert new states later without renumbering everything. It is standard industrial practice.
Readable states with an ENUM type
An enumeration makes the states readable and lets the compiler reject a value that is not a valid state:
TYPE MachineState :
(
IDLE := 0,
LOADING := 10,
PROCESSING := 20,
UNLOADING := 30,
ERROR := 99
);
END_TYPE
PROGRAM EnumStateMachine
VAR
State : MachineState := IDLE;
PrevState : MachineState := IDLE;
StartCmd : BOOL := FALSE;
LoadDone : BOOL := FALSE;
ProcessDone : BOOL := FALSE;
UnloadDone : BOOL := FALSE;
ErrorDetected : BOOL := FALSE;
END_VAR
// Track state changes
PrevState := State;
// Global error check — can interrupt any state
IF ErrorDetected THEN
State := ERROR;
END_IF;
CASE State OF
IDLE:
IF StartCmd THEN
State := LOADING;
END_IF;
LOADING:
// Activate loading mechanism
IF LoadDone THEN
State := PROCESSING;
END_IF;
PROCESSING:
// Run process
IF ProcessDone THEN
State := UNLOADING;
END_IF;
UNLOADING:
// Unload finished product
IF UnloadDone THEN
State := IDLE;
END_IF;
ERROR:
// Safe state — all outputs off
IF NOT ErrorDetected AND StartCmd THEN
State := IDLE;
END_IF;
END_CASE;
Pattern: state machine with timeouts
Real machines need timeouts to catch a jammed cylinder, a missing part or a stalled process:
PROGRAM TimedStateMachine
VAR
State : INT := 0;
StateTimer : TON;
TimeoutAlarm : BOOL := FALSE;
StartButton : BOOL := FALSE;
PartPresent : BOOL := FALSE;
ClampClosed : BOOL := FALSE;
Clamp : BOOL := FALSE;
END_VAR
// Reset timer on state change (use one-shot detection)
StateTimer(IN := TRUE, PT := T#10s);
CASE State OF
0: // IDLE
StateTimer(IN := FALSE); // Reset timer
Clamp := FALSE;
TimeoutAlarm := FALSE;
IF StartButton AND PartPresent THEN
State := 10;
StateTimer(IN := FALSE);
END_IF;
10: // CLAMPING
Clamp := TRUE;
StateTimer(IN := TRUE, PT := T#5s);
IF ClampClosed THEN
State := 20;
StateTimer(IN := FALSE);
ELSIF StateTimer.Q THEN
TimeoutAlarm := TRUE;
State := 99;
END_IF;
20: // PROCESSING
StateTimer(IN := TRUE, PT := T#30s);
// ... process logic
IF StateTimer.Q THEN
TimeoutAlarm := TRUE;
State := 99;
END_IF;
99: // FAULT
Clamp := FALSE;
// Wait for operator reset
IF StartButton AND NOT TimeoutAlarm THEN
State := 0;
END_IF;
END_CASE;
Pattern: parallel state machines
Complex machines often have several subsystems running at the same time. Give each one its own state variable and let them talk through handshake signals:
PROGRAM ParallelStates
VAR
ConveyorState : INT := 0;
RobotState : INT := 0;
InspectionState : INT := 0;
// Handshake signals between subsystems
PartAtRobot : BOOL := FALSE;
RobotPickDone : BOOL := FALSE;
PartAtInspection : BOOL := FALSE;
InspectResult : BOOL := FALSE;
END_VAR
// Each CASE block runs independently every scan
CASE ConveyorState OF
0: // ... conveyor logic
10: // ...
END_CASE;
CASE RobotState OF
0: // WAIT FOR PART
IF PartAtRobot THEN
RobotState := 10;
END_IF;
10: // PICKING
// ... robot pick logic
RobotPickDone := TRUE;
RobotState := 20;
20: // PLACING
// ...
END_CASE;
CASE InspectionState OF
0: // ...
10: // ...
END_CASE;
Pattern: step and transition (SFC-like)
You can implement a Grafcet/SFC-style pattern in plain Structured Text:
PROGRAM SFCStyle
VAR
Step : INT := 0;
StepTimer : TON;
// Transitions (conditions to move forward)
T1 : BOOL := FALSE; // Start condition
T2 : BOOL := FALSE; // Sensor reached
T3 : BOOL := FALSE; // Process complete
// Actions
Motor : BOOL := FALSE;
Heater : BOOL := FALSE;
Valve : BOOL := FALSE;
END_VAR
CASE Step OF
0: // Initial step — all off
Motor := FALSE;
Heater := FALSE;
Valve := FALSE;
IF T1 THEN Step := 1; END_IF;
1: // Step 1: Start motor
Motor := TRUE;
Heater := FALSE;
StepTimer(IN := TRUE, PT := T#2s);
IF StepTimer.Q AND T2 THEN
StepTimer(IN := FALSE);
Step := 2;
END_IF;
2: // Step 2: Heat
Motor := TRUE; // Keep running
Heater := TRUE;
IF T3 THEN Step := 3; END_IF;
3: // Step 3: Dispense
Heater := FALSE;
Valve := TRUE;
StepTimer(IN := TRUE, PT := T#5s);
IF StepTimer.Q THEN
StepTimer(IN := FALSE);
Step := 0; // Return to idle
END_IF;
END_CASE;
Checklist for a PLC state machine
Practise state machines online
You can build your own state machine in the free PLC simulator. Start with the basic CASE pattern, then add timeouts and error handling. The lessons include step-by-step state machine exercises.