PLC Power-Up Initialization & Startup Sequences: Safe Boot Logic in Structured Text

What happens in the first 5 seconds after power-up determines whether your machine starts safely or damages itself. Build bulletproof PLC initialization sequences with proven Structured Text patterns.

The first 5 seconds after power-up

For a short while after a PLC powers up, outputs can glitch, sensors have not settled and communication links are not yet established. In that window:

  • A valve that defaults to OPEN could flood a tank
  • A motor contactor that defaults to ON could start a conveyor with someone inside the machine
  • A heater that retains its last state could overshoot while the temperature sensor is still initializing
  • The first rule of PLC startup is to assume nothing is in a known state. Your initialization sequence has to verify everything before it enables any output.

    The sequence below is machine logic. It does not replace the safety circuit: protecting someone inside the machine is the job of the safety relay or safety PLC. The startup sequencer keeps the process logic from running on outputs, sensors and links in an unknown state.

    A startup state machine that holds off normal operation

    A well-built PLC program starts with a startup sequencer, and nothing in normal operation runs until it finishes:

    TYPE StartupState : (
        BOOT_INIT,           // First scan — initialize variables
        BOOT_IO_CHECK,       // Verify I/O modules are online
        BOOT_COMM_CHECK,     // Verify network connections
        BOOT_SENSOR_VALID,   // Wait for sensor readings to stabilize
        BOOT_SAFE_POSITION,  // Move actuators to safe/home positions
        BOOT_SELF_TEST,      // Run diagnostic checks
        BOOT_READY,          // All checks passed — ready for operation
        BOOT_FAILED          // Startup failed — manual intervention needed
    );
    END_TYPE
    
    PROGRAM StartupManager
    VAR
        State           : StartupState := BOOT_INIT;
        BootComplete    : BOOL := FALSE;
        BootFailed      : BOOL := FALSE;
        BootErrorCode   : DINT := 0;
        BootErrorMsg    : STRING[80] := '';
        BootTimeElapsed : TIME;
        MaxBootTime     : TIME := T#30S;
    
        tmrBootTimeout  : TON;
        tmrStepDelay    : TON;
        FirstScan       : BOOL := TRUE;
    END_VAR
    
    // Global boot timeout — if startup takes too long, something is wrong
    tmrBootTimeout(IN := (State <> BOOT_READY) AND (State <> BOOT_FAILED), PT := MaxBootTime);
    
    IF tmrBootTimeout.Q THEN
        State := BOOT_FAILED;
        BootErrorCode := 9999;
        BootErrorMsg := 'Boot timeout — startup exceeded maximum time';
    END_IF;
    

    Phase 1: first-scan initialization

    The first scan is where you set initial values and clear any retained variables that should not survive a power cycle:

    // Inside the BOOT_INIT state:
    CASE State OF
        BOOT_INIT:
            // === CRITICAL FIRST-SCAN ACTIONS ===
            
            // 1. Force all outputs to safe state
            DO_AllMotors := FALSE;
            DO_AllValves := FALSE;
            DO_AllHeaters := FALSE;
            AO_AllSpeeds := 0;
            
            // 2. Clear one-shot commands that might have been retained
            CMD_Start := FALSE;
            CMD_AutoRun := FALSE;
            CMD_BatchStart := FALSE;
            
            // 3. Reset all function block instances
            // (Call with Enable:=FALSE to force internal reset)
            Motor1(Enable := FALSE);
            Motor2(Enable := FALSE);
            TempLoop1(Enable := FALSE);
            
            // 4. Initialize counters and accumulators
            ProductionCount := 0;
            ShiftRuntime := T#0S;
            
            // 5. Load configuration from retained memory
            IF RetainedConfig.IsValid THEN
                ActiveRecipe := RetainedConfig.LastRecipe;
                UnitConversion := RetainedConfig.Units;
            ELSE
                // First-ever boot or corrupted retain — use defaults
                ActiveRecipe := 1;
                UnitConversion := 0;   // Metric
            END_IF;
            
            State := BOOT_IO_CHECK;
    

    Warm start vs. cold start

    On a cold start the retained memory is cleared as well, so every variable goes back to its initial value. On a warm start the RETAIN variables keep their values and only the non-retained variables are reset. The function below tells the two apart with a marker value kept in retained memory:

    TYPE RestartType : (
        RESTART_COLD,        // Full power cycle — initialize everything
        RESTART_WARM,        // Stop/Start — retain some context
        RESTART_FAULT        // Recovered from CPU fault
    );
    END_TYPE
    
    FUNCTION FC_DetectRestartType : RestartType
    VAR
        RetainMarker     : DINT := 0;         // Non-retained
        RetainedMarker   : DINT;              // RETAIN variable
    END_VAR
    VAR RETAIN
        RetainedMarker   : DINT := 0;
        BootCounter      : DINT := 0;
    END_VAR
    
    BootCounter := BootCounter + 1;
    
    IF RetainedMarker = 16#DEADBEEF THEN
        // Retained memory intact — warm start
        FC_DetectRestartType := RESTART_WARM;
    ELSE
        // Retained memory was cleared — cold start
        RetainedMarker := 16#DEADBEEF;
        FC_DetectRestartType := RESTART_COLD;
    END_IF;
    

    Phase 2: I/O module validation

    Before you trust any input, check that the I/O modules are online and responding:

    // Inside BOOT_IO_CHECK state:
        BOOT_IO_CHECK:
            // Check I/O module status (platform-specific diagnostic registers)
            // Siemens: Module status in system DB
            // Allen-Bradley: Module fault bits in controller properties
            // CODESYS: IoConfig device state
            
            ioCheckResult := FC_ValidateIOModules();
            
            CASE ioCheckResult OF
                0: // All modules OK
                    State := BOOT_COMM_CHECK;
                    
                1: // Non-critical module missing (e.g., spare I/O)
                    BootErrorMsg := 'Warning: Non-critical I/O module offline';
                    // Continue with degraded capability
                    State := BOOT_COMM_CHECK;
                    
                2: // Critical module missing
                    BootErrorCode := 2001;
                    BootErrorMsg := 'Critical I/O module not responding';
                    State := BOOT_FAILED;
            END_CASE;
    

    FUNCTION FC_ValidateIOModules : INT
    VAR
        ModulesExpected : INT := 8;
        ModulesOnline   : INT := 0;
        CriticalMask    : WORD := 16#003F;   // Modules 0–5 are critical
        OnlineMask      : WORD := 16#0000;
        i               : INT;
    END_VAR
    
    // Platform-specific: read module status bits
    // This example uses a status word where each bit = one module
    FOR i := 0 TO ModulesExpected - 1 DO
        IF (* module i is online *) TRUE THEN
            OnlineMask := OnlineMask OR SHL(WORD#16#0001, i);
            ModulesOnline := ModulesOnline + 1;
        END_IF;
    END_FOR;
    
    // Check if all critical modules are present
    IF (OnlineMask AND CriticalMask) = CriticalMask THEN
        IF ModulesOnline = ModulesExpected THEN
            FC_ValidateIOModules := 0;   // All modules OK
        ELSE
            FC_ValidateIOModules := 1;   // Non-critical module missing
        END_IF;
    ELSE
        FC_ValidateIOModules := 2;       // Critical module missing
    END_IF;
    

    Phase 3: communication validation

    Drives, remote I/O and SCADA links take time to boot. Wait for them, but put a limit on the wait:

        BOOT_COMM_CHECK:
            // Check each communication link
            commCheckDone := TRUE;
            
            // VFD on PROFINET
            IF NOT VFD_Drive1.IsOnline THEN
                commCheckDone := FALSE;
                tmrStepDelay(IN := TRUE, PT := T#10S);
                IF tmrStepDelay.Q THEN
                    BootErrorCode := 3001;
                    BootErrorMsg := 'VFD Drive 1 not responding on PROFINET';
                    State := BOOT_FAILED;
                END_IF;
            END_IF;
            
            // Remote I/O on EtherNet/IP
            IF NOT RemoteIO_Panel2.IsOnline THEN
                commCheckDone := FALSE;
                tmrStepDelay(IN := TRUE, PT := T#10S);
                IF tmrStepDelay.Q THEN
                    BootErrorCode := 3002;
                    BootErrorMsg := 'Remote I/O Panel 2 not responding';
                    State := BOOT_FAILED;
                END_IF;
            END_IF;
            
            // SCADA — non-critical, continue without it
            IF NOT SCADA_Connection.IsOnline THEN
                // Log warning but don't fail boot
                BootErrorMsg := 'SCADA offline — running in local mode';
            END_IF;
            
            IF commCheckDone THEN
                tmrStepDelay(IN := FALSE);
                State := BOOT_SENSOR_VALID;
            END_IF;
    

    Phase 4: sensor stabilization

    Analog sensors need time to settle after power-up. Thermocouples, pressure transmitters and flow meters can read garbage for 1–5 seconds, so each critical reading has to pass a plausibility check and then stay inside a window for a set time:

    FUNCTION_BLOCK FB_SensorStabilizer
    VAR_INPUT
        RawValue       : REAL;
        StableWindow   : REAL := 2.0;    // Must stay within this range
        StableTime     : TIME := T#3S;   // For this duration
        PlausibleMin   : REAL := -50.0;
        PlausibleMax   : REAL := 500.0;
    END_VAR
    VAR_OUTPUT
        IsStable       : BOOL := FALSE;
        StableValue    : REAL;
        IsPlausible    : BOOL := FALSE;
    END_VAR
    VAR
        tmrStable      : TON;
        refValue       : REAL;
        initialized    : BOOL := FALSE;
    END_VAR
    
    // Check if reading is physically plausible
    IsPlausible := (RawValue >= PlausibleMin) AND (RawValue <= PlausibleMax);
    
    IF NOT IsPlausible THEN
        IsStable := FALSE;
        tmrStable(IN := FALSE);
        RETURN;
    END_IF;
    
    // Initialize reference on first plausible reading
    IF NOT initialized THEN
        refValue := RawValue;
        initialized := TRUE;
    END_IF;
    
    // Check if value is stable (within window of reference)
    IF ABS(RawValue - refValue) <= StableWindow THEN
        tmrStable(IN := TRUE, PT := StableTime);
    ELSE
        // Value drifted — reset with new reference
        refValue := RawValue;
        tmrStable(IN := FALSE);
    END_IF;
    
    IsStable := tmrStable.Q;
    StableValue := RawValue;
    

        BOOT_SENSOR_VALID:
            // Run stabilizers for critical sensors
            stabTemp1(RawValue := AI_Temperature1, StableWindow := 2.0, StableTime := T#3S);
            stabPress1(RawValue := AI_Pressure1, StableWindow := 0.5, StableTime := T#2S);
            stabLevel1(RawValue := AI_Level1, StableWindow := 3.0, StableTime := T#2S);
            
            IF stabTemp1.IsStable AND stabPress1.IsStable AND stabLevel1.IsStable THEN
                State := BOOT_SAFE_POSITION;
            END_IF;
            
            // Individual sensor failure messages
            IF NOT stabTemp1.IsPlausible THEN
                BootErrorMsg := 'Temperature sensor 1 — reading not plausible';
            END_IF;
    

    Phase 5: safe positioning

    Move the actuators to their safe starting positions before you enable automatic operation. Here that means closing the valves and homing the servo axis:

        BOOT_SAFE_POSITION:
            // Close all valves
            Valve_Inlet(CMD_Close := TRUE);
            Valve_Outlet(CMD_Close := TRUE);
            Valve_Drain(CMD_Close := TRUE);
            
            // Home all axes (if servo-controlled)
            IF NOT Axis1_Homed THEN
                fbHome_Axis1(Axis := Axis1, Execute := TRUE, Velocity := 10.0);
                IF fbHome_Axis1.Done THEN
                    Axis1_Homed := TRUE;
                    fbHome_Axis1(Execute := FALSE);
                ELSIF fbHome_Axis1.Error THEN
                    BootErrorCode := 5001;
                    BootErrorMsg := 'Axis 1 homing failed';
                    State := BOOT_FAILED;
                END_IF;
            END_IF;
            
            // Verify all actuators reached safe state
            allSafe := Valve_Inlet.IsClosed
                       AND Valve_Outlet.IsClosed
                       AND Valve_Drain.IsClosed
                       AND Axis1_Homed;
            
            IF allSafe THEN
                State := BOOT_SELF_TEST;
            END_IF;
    

    Phase 6: self-diagnostics

    Automated checks then confirm the analog range, output feedback and retained data before the machine is declared ready:

    FUNCTION_BLOCK FB_SelfTest
    VAR_INPUT
        Execute      : BOOL;
    END_VAR
    VAR_OUTPUT
        Done         : BOOL;
        Passed       : BOOL;
        TestID       : INT;
        FailMessage  : STRING[80];
    END_VAR
    VAR
        Step         : INT := 0;
        TestCount    : INT := 0;
        PassCount    : INT := 0;
    END_VAR
    
    IF NOT Execute THEN Step := 0; Done := FALSE; RETURN; END_IF;
    
    CASE Step OF
        0: // TEST 1: Verify analog input range
            TestCount := TestCount + 1;
            TestID := 1;
            IF AI_Temperature1 > -40.0 AND AI_Temperature1 < 300.0 THEN
                PassCount := PassCount + 1;
                Step := 1;
            ELSE
                FailMessage := 'Self-test: Temperature 1 out of range at boot';
                Step := 99;
            END_IF;
    
        1: // TEST 2: Verify output feedback matches command
            TestCount := TestCount + 1;
            TestID := 2;
            // Briefly pulse an output and check feedback
            // (Only for outputs with feedback wiring)
            DO_TestOutput := TRUE;
            Step := 2;
    
        2: // Check feedback after one scan
            IF DI_TestFeedback THEN
                DO_TestOutput := FALSE;
                PassCount := PassCount + 1;
                Step := 3;
            ELSE
                DO_TestOutput := FALSE;
                FailMessage := 'Self-test: Output feedback mismatch';
                Step := 99;
            END_IF;
    
        3: // TEST 3: Verify retained data integrity
            TestCount := TestCount + 1;
            TestID := 3;
            IF RetainedConfig.Checksum = FC_CalcChecksum(RetainedConfig) THEN
                PassCount := PassCount + 1;
                Step := 4;
            ELSE
                FailMessage := 'Self-test: Retained data checksum mismatch';
                // Not fatal — use defaults
                PassCount := PassCount + 1;
                Step := 4;
            END_IF;
    
        4: // ALL TESTS COMPLETE
            Done := TRUE;
            Passed := (PassCount = TestCount);
    
        99: // TEST FAILED
            Done := TRUE;
            Passed := FALSE;
    END_CASE;
    

    Phase 7: ready for operation

    The last two states either release the machine or hold every output off and wait for the operator:

        BOOT_READY:
            BootComplete := TRUE;
            BootFailed := FALSE;
            // Now enable normal operation
            // The main program checks BootComplete before running any sequence
            
        BOOT_FAILED:
            BootComplete := FALSE;
            BootFailed := TRUE;
            // All outputs forced to safe state
            DO_AllMotors := FALSE;
            DO_AllValves := FALSE;
            DO_AllHeaters := FALSE;
            // Display error on HMI
            // Wait for operator to fix issue and request re-boot
    END_CASE;
    

    Holding the main program until boot completes

    The main process program returns early on every scan until BootComplete is TRUE:

    // In your main process program:
    PROGRAM ProcessControl
    VAR
        startup : StartupManager;
    END_VAR
    
    IF NOT startup.BootComplete THEN
        // DON'T RUN ANYTHING — startup hasn't completed
        // Optionally show boot progress on HMI
        RETURN;
    END_IF;
    
    // === Normal operation starts here ===
    // Only reached after ALL boot checks pass
    

    Power failure recovery

    If power is lost in the middle of a process, a recovery sequence decides what to do from the step that was interrupted. Some steps can resume on their own; an interrupted reaction waits for an operator decision:

    TYPE ProcessCheckpoint : (
        CP_IDLE,
        CP_FILLING,
        CP_HEATING,
        CP_REACTING,
        CP_COOLING,
        CP_DRAINING,
        CP_COMPLETE
    );
    END_TYPE
    
    VAR RETAIN
        LastCheckpoint    : ProcessCheckpoint := CP_IDLE;
        LastBatchID       : DINT := 0;
        LastStepParams    : RecipeStep;
        CheckpointValid   : BOOL := FALSE;
    END_VAR
    
    // After boot completes, check for interrupted process:
    IF startup.BootComplete AND FC_DetectRestartType() = RESTART_COLD THEN
        IF CheckpointValid AND LastCheckpoint <> CP_IDLE AND LastCheckpoint <> CP_COMPLETE THEN
            // Process was interrupted!
            CASE LastCheckpoint OF
                CP_FILLING:
                    // Safe to resume — verify weight and continue
                    RecoveryAction := 'Resuming fill from checkpoint';
                    
                CP_HEATING:
                    // Check actual temperature — may need to re-ramp
                    IF ActualTemp > (LastStepParams.TargetTemp - 10.0) THEN
                        RecoveryAction := 'Near setpoint — resuming hold timer';
                    ELSE
                        RecoveryAction := 'Temperature dropped — restarting ramp';
                    END_IF;
                    
                CP_REACTING:
                    // CRITICAL: reaction may be in unknown state
                    RecoveryAction := 'OPERATOR DECISION REQUIRED — reaction interrupted';
                    // Don't auto-resume — require operator confirmation
                    
                CP_DRAINING:
                    // Safe to resume draining
                    RecoveryAction := 'Resuming drain sequence';
            END_CASE;
        END_IF;
    END_IF;
    
    // Save checkpoint at each major step transition:
    LastCheckpoint := CP_HEATING;
    LastStepParams := CurrentStep;
    CheckpointValid := TRUE;
    // These RETAIN variables survive power loss
    

    Phase timeouts and design rules at a glance

    PhasePurposeTimeout
    INITForce outputs safe, clear commands, load configImmediate
    IO_CHECKVerify all I/O modules are online5s
    COMM_CHECKVerify network devices are responding10s
    SENSOR_VALIDWait for analog readings to stabilize5–10s
    SAFE_POSITIONMove actuators to home/safe positions15s
    SELF_TESTAutomated diagnostic checks5s
    READYEnable normal operationNone
    PrincipleImplementation
    First scan safetyForce all outputs OFF before checking anything
    Warm vs. cold detectionRETAIN marker variable (0xDEADBEEF pattern)
    Sensor stabilizationPlausibility check + stability window timer
    Graceful degradationNon-critical failures log warnings but don't block boot
    Power failure recoveryRETAIN checkpoints + operator-confirmed resume for critical steps
    Boot timeoutGlobal watchdog prevents infinite startup loops
    The startup sequence runs once per power-up and takes a few seconds. Without it, every power cycle starts the process logic on outputs, sensors and network links in an unknown state.