Safety
PLC Safety Programming: SIL-Rated Logic & Safety Function Blocks in Structured Text
Design SIL-rated safety logic in Structured Text — covering dual-channel monitoring, safe shutdown sequences, diagnostic coverage, and IEC 62061 / IEC 61508 compliance patterns.
What functional safety asks of the PLC program
Functional safety means a Safety Instrumented System (SIS) performs its safety function correctly when a demand comes. IEC 61508 (general), IEC 62061 (machinery) and IEC 61511 (process industry) set the requirements for the whole safety lifecycle, from hazard analysis through design, coding and testing to maintenance.
This article covers the PLC programming patterns used in safety-rated applications. A real safety system needs certified hardware (a safety PLC) and formal verification. Every automation engineer should still know the patterns themselves.
Safety integrity levels (SIL)
A SIL states how much risk reduction a safety function must deliver. The higher the SIL, the lower the allowed probability of a dangerous failure:
| SIL | PFD (Low Demand) | PFH (High/Continuous) | Risk Reduction | Example Application |
|---|---|---|---|---|
| 1 | 10⁻¹ to 10⁻² | 10⁻⁵ to 10⁻⁶ /h | 10–100× | Light curtain guard stop |
| 2 | 10⁻² to 10⁻³ | 10⁻⁶ to 10⁻⁷ /h | 100–1,000× | Emergency stop, burner management |
| 3 | 10⁻³ to 10⁻⁴ | 10⁻⁷ to 10⁻⁸ /h | 1,000–10,000× | High-pressure relief, toxic gas shutdown |
| 4 | 10⁻⁴ to 10⁻⁵ | 10⁻⁸ to 10⁻⁹ /h | 10,000–100,000× | Nuclear protection (rarely needed in manufacturing) |
Fail-safe design
Design safety logic so that the most likely failure mode leads to the safe state. In practice:
Dual-channel input processing
SIL 2 and above typically require redundant sensing: two independent sensors watching the same process condition. The PLC must compare the two channels and detect when they disagree:
FUNCTION_BLOCK FB_DualChannelInput
VAR_INPUT
Channel_A : BOOL; // Sensor 1 (NC contact)
Channel_B : BOOL; // Sensor 2 (NC contact)
DiscrepancyTime : TIME := T#500ms; // Max allowed disagreement
END_VAR
VAR_OUTPUT
SafeInput : BOOL; // TRUE = safe condition detected
Discrepancy : BOOL; // Channels disagree too long
DiagFault : BOOL; // Diagnostic fault detected
END_VAR
VAR
DiscrepTimer : TON;
BothClosed : BOOL;
BothOpen : BOOL;
Disagreeing : BOOL;
END_VAR
BothClosed := Channel_A AND Channel_B;
BothOpen := NOT Channel_A AND NOT Channel_B;
Disagreeing := NOT BothClosed AND NOT BothOpen;
// Discrepancy timer: channels must agree within time limit
DiscrepTimer(IN := Disagreeing, PT := DiscrepancyTime);
IF DiscrepTimer.Q THEN
// Channels have disagreed too long — diagnostic fault
Discrepancy := TRUE;
DiagFault := TRUE;
SafeInput := FALSE; // Go to safe state on fault
ELSE
Discrepancy := FALSE;
// Safe condition: BOTH channels indicate safe
// (For NC contacts: both TRUE = guards closed / condition OK)
SafeInput := BothClosed AND NOT DiagFault;
END_IF;
// DiagFault latches until explicit reset (not shown here)
What the discrepancy timer catches
Without discrepancy detection, a single sensor failure can go unnoticed. The system then runs on one channel of protection, which violates the SIL rating. The timer allows for normal switching tolerance, since two contacts never close at exactly the same instant, and still catches a genuine fault.
Safe shutdown sequencing
A safety shutdown is more than "turn everything off." Equipment has to be de-energized in the correct order, or the shutdown itself causes water hammer, pressure surges or thermal shock:
FUNCTION_BLOCK FB_SafeShutdownSequence
VAR_INPUT
TripCommand : BOOL; // Safety system trip signal
ResetCommand : BOOL; // Operator reset (after investigation)
AllValvesClosed : BOOL; // Feedback: all isolation done
DrivesStopped : BOOL; // Feedback: all motors stopped
END_VAR
VAR_OUTPUT
ShutdownActive : BOOL;
Step : INT;
FuelValveCmd : BOOL; // FALSE = close (safe)
FeedPumpCmd : BOOL; // FALSE = stop (safe)
CoolingPumpCmd : BOOL; // Keep running during shutdown!
PurgeBlowerCmd : BOOL; // Run during purge cycle
ReadyToRestart : BOOL;
END_VAR
VAR
StepTimer : TON;
R_Trip : R_TRIG;
R_Reset : R_TRIG;
END_VAR
R_Trip(CLK := TripCommand);
R_Reset(CLK := ResetCommand);
IF R_Trip.Q THEN
Step := 1;
ShutdownActive := TRUE;
ReadyToRestart := FALSE;
END_IF;
CASE Step OF
0: // NORMAL OPERATION
ShutdownActive := FALSE;
// Outputs controlled by normal process logic
1: // STEP 1: Close fuel valve IMMEDIATELY
FuelValveCmd := FALSE; // De-energize = close
FeedPumpCmd := TRUE; // Keep feed pump running briefly
CoolingPumpCmd := TRUE; // Cooling must continue!
PurgeBlowerCmd := FALSE;
StepTimer(IN := TRUE, PT := T#5s);
IF StepTimer.Q THEN
StepTimer(IN := FALSE);
Step := 2;
END_IF;
2: // STEP 2: Stop feed pump after 5s delay
FeedPumpCmd := FALSE;
CoolingPumpCmd := TRUE; // Still cooling
StepTimer(IN := TRUE, PT := T#30s);
IF StepTimer.Q THEN
StepTimer(IN := FALSE);
Step := 3;
END_IF;
3: // STEP 3: Start purge cycle (30s post-trip)
PurgeBlowerCmd := TRUE;
CoolingPumpCmd := TRUE;
StepTimer(IN := TRUE, PT := T#120s); // 2-minute purge
IF StepTimer.Q THEN
StepTimer(IN := FALSE);
Step := 4;
END_IF;
4: // STEP 4: Purge complete, stop blower
PurgeBlowerCmd := FALSE;
CoolingPumpCmd := TRUE; // Cooling runs 10 min total
StepTimer(IN := TRUE, PT := T#480s); // Remaining cool-down
IF StepTimer.Q THEN
StepTimer(IN := FALSE);
Step := 5;
END_IF;
5: // STEP 5: Shutdown complete — awaiting reset
FuelValveCmd := FALSE;
FeedPumpCmd := FALSE;
CoolingPumpCmd := FALSE;
PurgeBlowerCmd := FALSE;
ShutdownActive := TRUE;
IF R_Reset.Q AND NOT TripCommand THEN
// Only allow reset when trip condition cleared
ReadyToRestart := TRUE;
Step := 0;
ShutdownActive := FALSE;
END_IF;
END_CASE;
Sequencing rules
Emergency stop (E-stop) logic
ISO 13850 covers emergency stop design, including the stop category an E-stop uses. This is a Category 1 implementation (controlled stop, then power removal):
FUNCTION_BLOCK FB_EmergencyStop
VAR_INPUT
EStop_Ch1 : BOOL; // NC contact, channel 1
EStop_Ch2 : BOOL; // NC contact, channel 2
ResetButton : BOOL; // Momentary NO contact
AllDrivesSafe : BOOL; // Speed = 0 feedback
END_VAR
VAR_OUTPUT
SafetyOK : BOOL; // Master safety relay output
Cat1StopActive : BOOL; // Controlled stop in progress
EStopLatched : BOOL; // Latched trip state
END_VAR
VAR
DualInput : FB_DualChannelInput;
StopTimer : TON;
R_Reset : R_TRIG;
END_VAR
// Process dual-channel E-Stop inputs
DualInput(
Channel_A := EStop_Ch1,
Channel_B := EStop_Ch2,
DiscrepancyTime := T#200ms
);
R_Reset(CLK := ResetButton);
// E-Stop pressed (NC contacts open = both FALSE)
IF NOT DualInput.SafeInput OR DualInput.DiagFault THEN
EStopLatched := TRUE;
END_IF;
IF EStopLatched THEN
// Category 1: Allow controlled deceleration
Cat1StopActive := TRUE;
// After drives have stopped (or 10s max), remove power
StopTimer(IN := TRUE, PT := T#10s);
IF AllDrivesSafe OR StopTimer.Q THEN
SafetyOK := FALSE; // Open safety relay
END_IF;
ELSE
SafetyOK := TRUE;
Cat1StopActive := FALSE;
StopTimer(IN := FALSE);
END_IF;
// Manual reset: requires E-Stop released AND button press
IF R_Reset.Q AND DualInput.SafeInput
AND NOT DualInput.DiagFault THEN
EStopLatched := FALSE;
END_IF;
Diagnostic coverage and proof testing
Diagnostic coverage (DC) is the fraction of dangerous failures that online diagnostics detect. A higher DC helps you reach the target SIL:
| Diagnostic Technique | Typical DC | ST Implementation |
|---|---|---|
| Input comparison (dual-channel) | 90-99% | FB_DualChannelInput above |
| Output readback (EDM) | 90-99% | Compare command vs. feedback |
| Watchdog timer | 60-90% | Cyclic pulse monitored externally |
| Valve partial stroke test | 60-90% | Periodic partial close + position check |
| Wire break detection | 60-90% | Check for out-of-range analog |
Output readback (external device monitoring)
After you command an output, check that the device actually responded:
FUNCTION_BLOCK FB_OutputReadback
VAR_INPUT
Command : BOOL; // What we commanded
Feedback : BOOL; // What actually happened
MonitoringTime : TIME := T#2s; // Max response time
END_VAR
VAR_OUTPUT
EDM_Fault : BOOL; // External Device Monitoring fault
END_VAR
VAR
MismatchTimer : TON;
Mismatch : BOOL;
END_VAR
// Check if command and feedback disagree
Mismatch := Command XOR Feedback;
MismatchTimer(IN := Mismatch, PT := MonitoringTime);
IF MismatchTimer.Q THEN
// Device did not respond within time — latch fault
EDM_Fault := TRUE;
// Requires manual reset (not shown)
END_IF;
Analog input diagnostics
You can detect a failed sensor by checking for out-of-range values. A 4-20mA sensor reads below 4mA on a wire break:
FUNCTION_BLOCK FB_AnalogDiagnostic
VAR_INPUT
RawValue : INT; // 0-32767 from ADC
WireBreakLow : INT := 1000; // Below this = wire break
OverRangeHigh : INT := 31000; // Above this = over-range
END_VAR
VAR_OUTPUT
ValidSignal : BOOL;
WireBreak : BOOL;
OverRange : BOOL;
EngValue : REAL; // Scaled 4-20mA → 0-100%
END_VAR
WireBreak := RawValue < WireBreakLow;
OverRange := RawValue > OverRangeHigh;
ValidSignal := NOT WireBreak AND NOT OverRange;
IF ValidSignal THEN
// Scale 4-20mA range (approx 6554-26214 in 0-32767 ADC)
EngValue := (INT_TO_REAL(RawValue) - 6554.0) / 19660.0 * 100.0;
// Clamp to 0-100%
IF EngValue < 0.0 THEN EngValue := 0.0; END_IF;
IF EngValue > 100.0 THEN EngValue := 100.0; END_IF;
ELSE
EngValue := 0.0; // Or last-known-good, depending on strategy
END_IF;
Safety PLC watchdog pattern
A watchdog timer checks that the PLC program is still executing. The PLC toggles a bit and an external safety relay monitors the pulse. If the program hangs, the pulse stops and the relay opens:
FUNCTION_BLOCK FB_SafetyWatchdog
VAR_OUTPUT
WatchdogPulse : BOOL; // Connect to safety relay input
END_VAR
VAR
PulseTimer : TON;
PulseState : BOOL;
END_VAR
PulseTimer(IN := NOT PulseState, PT := T#250ms);
IF PulseTimer.Q THEN
PulseState := NOT PulseState;
PulseTimer(IN := FALSE);
END_IF;
WatchdogPulse := PulseState;
// If PLC program hangs, pulse stops → safety relay opens
// Typical external relay: expects pulse within 500ms window
Software development requirements by SIL
IEC 61508 Part 3 sets software development requirements according to SIL:
This is a simplified reading. IEC 61508-3 itself grades each technique per SIL as recommended (R) or highly recommended (HR), and you must justify leaving out an HR technique; "Required" below marks those.
| Requirement | SIL 1 | SIL 2 | SIL 3 |
|---|---|---|---|
| Structured programming | Recommended | Required | Required |
| Defensive programming | Recommended | Required | Required |
| Formal code review | Recommended | Required | Required |
| Unit testing | Recommended | Required | Required |
| Integration testing | Required | Required | Required |
| Cyclomatic complexity limit | - | ≤ 10 | ≤ 10 |
| Single entry/exit per block | Recommended | Required | Required |
| No dynamic variables | Required | Required | Required |
| No recursion | Required | Required | Required |
| No pointers (SIL 3) | - | Recommended | Required |
| 100% code coverage testing | - | Recommended | Required |
| Independent verification | - | Recommended | Required |
Coding rules for safety ST
Keep each block to one purpose, name every tag for what it measures, and bound every loop. The pressure monitor below follows those rules and trips on a channel fault as well as on high pressure:
// GOOD: Single purpose, clear naming, bounded loops
FUNCTION_BLOCK FB_SafePressureMonitor
VAR_INPUT
PV_Pressure_Ch1 : REAL; // Channel 1 reading (bar)
PV_Pressure_Ch2 : REAL; // Channel 2 reading (bar)
SP_HighHigh : REAL; // Trip setpoint (bar)
SP_Deviation : REAL; // Max channel deviation
END_VAR
VAR_OUTPUT
TripOutput : BOOL; // TRUE = trip (safe action)
ChannelFault : BOOL;
VotedPressure : REAL; // Selected value for display
END_VAR
// Channel deviation check
ChannelFault := ABS(PV_Pressure_Ch1 - PV_Pressure_Ch2) > SP_Deviation;
// 1oo2 voting: trip if EITHER channel exceeds limit
// (conservative — avoids missed trip on single sensor failure)
TripOutput := (PV_Pressure_Ch1 >= SP_HighHigh)
OR (PV_Pressure_Ch2 >= SP_HighHigh)
OR ChannelFault; // Fault = trip (fail-safe)
// Select higher value for display (conservative)
IF PV_Pressure_Ch1 >= PV_Pressure_Ch2 THEN
VotedPressure := PV_Pressure_Ch1;
ELSE
VotedPressure := PV_Pressure_Ch2;
END_IF;
Voting architectures: 1oo1, 1oo2, 2oo3
| Architecture | Description | Safe Failures | Dangerous Failures | Use Case |
|---|---|---|---|---|
| 1oo1 | Single channel | May cause spurious trip | Missed trip possible | SIL 1 basic |
| 1oo2 | Trip if either channel trips | Higher spurious trip rate | Very low (both must fail) | SIL 2–3 |
| 2oo3 | Trip if 2 of 3 trip | Low spurious (majority vote) | Low dangerous (2 must fail) | SIL 3, high availability |
| 2oo2 | Trip only if both trip | Lowest spurious rate | Higher dangerous (1 failure masks) | Avoid for safety |
2oo3 (two-out-of-three) gives the best balance of safety and availability. Critical process plants use it where a spurious trip is also hazardous, for example a refinery emergency shutdown.
Before you use any of this on a machine
You can try these safety logic patterns in the online ST editor. For the alarm handling that complements a safety system, see the alarm management tutorial.