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 Use State Machines in PLC Programming?

Every complex PLC program eventually becomes a state machine — whether you design it that way or not. Without a deliberate state machine structure, programs devolve into tangled webs of interlocking booleans that are nearly impossible to debug.

Benefits of State Machine Design

  • Predictable behavior: The system is always in exactly one known state
  • Easy debugging: Check one variable to know what the machine is doing
  • Safe transitions: Explicitly define what can happen in each state
  • Maintainable: Adding new states doesn't break existing logic
  • Documented: The state list IS the specification
  • Basic State Machine with CASE Statement

    The simplest and most common PLC state machine uses an INT variable with 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 Number States 0, 10, 20...?

    Using gaps (10, 20, 30) instead of sequential numbers (1, 2, 3) lets you insert new states later without renumbering everything. This is standard industrial practice.

    Using ENUM Types for Readable States

    For better readability and type safety, use an enumeration:

    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 detect jammed cylinders, missing parts, or stalled processes:

    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 multiple subsystems running simultaneously. Use separate state variables:

    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/Transition (SFC-like)

    You can implement a Grafcet/SFC-style pattern in pure 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;

    Best Practices for PLC State Machines

  • One state variable per machine — Don't split a single sequence across multiple flags
  • Always have an IDLE state — State 0 should be safe, all outputs off
  • Always have an ERROR state — Provide a safe fallback
  • Add timeouts to every waiting state — Machines jam; detect it
  • Log state changes — Track PrevState for diagnostics
  • Use meaningful names — ENUMs or well-commented CASE values
  • Test every transition — Each IF condition should be verified
  • Never skip states — Transitions should only go to adjacent states (except error)
  • Practice State Machines Online

    Build your own state machine in our free PLC simulator. Start with the basic CASE pattern, then add timeouts and error handling. Our lessons include step-by-step state machine exercises.