Advanced
PLC Alarm Management in Structured Text: Design Patterns & Best Practices
Build professional alarm management systems in Structured Text with priority handling, acknowledgment logic, first-out detection, and ISA-18.2 aligned design patterns.
Why Alarm Management Matters
Poor alarm management is a leading cause of industrial incidents. Studies show that operators can miss critical alarms when overwhelmed by nuisance or low-priority alerts — a phenomenon called "alarm flooding." The ISA-18.2 standard (IEC 62682) provides a framework for designing effective alarm systems. This guide shows how to implement these principles in Structured Text.
Alarm Data Structure
A well-designed alarm system starts with a clean data structure that captures all relevant states:
TYPE AlarmClass : (
ALARM_NONE,
ALARM_LOW,
ALARM_MEDIUM,
ALARM_HIGH,
ALARM_CRITICAL
);
END_TYPETYPE AlarmState : (
STATE_NORMAL, // Condition clear, acknowledged
STATE_ACTIVE_UNACK, // Condition present, not acknowledged
STATE_ACTIVE_ACK, // Condition present, acknowledged
STATE_CLEARED_UNACK // Condition cleared, not yet acknowledged
);
END_TYPE
TYPE AlarmRecord :
STRUCT
ID : INT; // Unique alarm number
Description : STRING(80); // Human-readable text
Class : AlarmClass; // Priority level
State : AlarmState; // Current alarm state
Active : BOOL; // Raw condition status
Acked : BOOL; // Operator acknowledged
Shelved : BOOL; // Temporarily suppressed
Timestamp : DT; // When alarm activated
AckTime : DT; // When acknowledged
ActiveCount : DINT; // Activation counter
END_STRUCT
END_TYPE
Basic Alarm Function Block
This reusable function block handles the four-state alarm model used in ISA-18.2:
FUNCTION_BLOCK FB_Alarm
VAR_INPUT
Condition : BOOL; // Raw alarm condition
Acknowledge : BOOL; // Operator ACK button
Reset : BOOL; // Reset latched alarms
Enable : BOOL := TRUE; // Enable/disable
END_VAR
VAR_OUTPUT
Active : BOOL; // Alarm is active
Unacked : BOOL; // Needs acknowledgment
State : AlarmState;
FlashOutput : BOOL; // For HMI flashing indicator
END_VAR
VAR
PrevCondition : BOOL;
FlashTimer : TON;
FlashBlink : BOOL;
END_VAR// Flash timer for unacknowledged indication
FlashTimer(IN := NOT FlashBlink, PT := T#500ms);
IF FlashTimer.Q THEN
FlashBlink := NOT FlashBlink;
END_IF;
IF NOT Enable THEN
State := STATE_NORMAL;
Active := FALSE;
Unacked := FALSE;
FlashOutput := FALSE;
RETURN;
END_IF;
// State machine
CASE State OF
STATE_NORMAL:
IF Condition THEN
State := STATE_ACTIVE_UNACK;
Active := TRUE;
Unacked := TRUE;
END_IF;
STATE_ACTIVE_UNACK:
IF Acknowledge THEN
State := STATE_ACTIVE_ACK;
Unacked := FALSE;
END_IF;
IF NOT Condition THEN
State := STATE_CLEARED_UNACK;
Active := FALSE;
END_IF;
STATE_ACTIVE_ACK:
IF NOT Condition THEN
State := STATE_NORMAL;
Active := FALSE;
Unacked := FALSE;
END_IF;
STATE_CLEARED_UNACK:
IF Acknowledge OR Reset THEN
State := STATE_NORMAL;
Unacked := FALSE;
END_IF;
IF Condition THEN
State := STATE_ACTIVE_UNACK;
Active := TRUE;
Unacked := TRUE;
END_IF;
END_CASE;
FlashOutput := Unacked AND FlashBlink;
Alarm Priority Classification
ISA-18.2 recommends a structured priority scheme. Here's a practical implementation:
| Priority | Response Time | Consequence | Example |
|---|---|---|---|
| Critical | Immediate | Safety hazard | E-Stop, gas leak |
| High | < 5 min | Equipment damage | Over-temperature, over-pressure |
| Medium | < 30 min | Process deviation | Setpoint drift, low level |
| Low | Next shift | Maintenance needed | Filter dirty, vibration warning |
FUNCTION_BLOCK FB_AlarmManager
VAR
Alarms : ARRAY[1..100] OF FB_Alarm;
AlarmConfig : ARRAY[1..100] OF AlarmRecord;
ActiveCount : INT;
UnackedCount : INT;
HighestPriority : AlarmClass;
END_VAR
VAR_INPUT
GlobalAck : BOOL; // Acknowledge all visible
SilenceHorn : BOOL; // Silence audible alert
END_VAR
VAR_OUTPUT
HornActive : BOOL; // Audible alarm output
CriticalActive : BOOL; // Any critical alarms?
SummaryWord : WORD; // Packed status for HMI
END_VAR
VAR
i : INT;
END_VARActiveCount := 0;
UnackedCount := 0;
HighestPriority := ALARM_NONE;
CriticalActive := FALSE;
FOR i := 1 TO 100 DO
// Process each alarm
IF GlobalAck THEN
Alarms[i].Acknowledge := TRUE;
END_IF;
// Count active and unacknowledged
IF Alarms[i].Active THEN
ActiveCount := ActiveCount + 1;
IF AlarmConfig[i].Class > HighestPriority THEN
HighestPriority := AlarmConfig[i].Class;
END_IF;
IF AlarmConfig[i].Class = ALARM_CRITICAL THEN
CriticalActive := TRUE;
END_IF;
END_IF;
IF Alarms[i].Unacked THEN
UnackedCount := UnackedCount + 1;
END_IF;
END_FOR;
// Horn logic: active on new unacked alarms, silenceable
HornActive := (UnackedCount > 0) AND NOT SilenceHorn;
First-Out Alarm Logic
In critical processes, knowing which alarm triggered first is essential for root cause analysis. A first-out group captures the sequence:
FUNCTION_BLOCK FB_FirstOutGroup
VAR_INPUT
Conditions : ARRAY[1..8] OF BOOL; // Up to 8 inputs
Reset : BOOL; // Reset first-out latch
END_VAR
VAR_OUTPUT
FirstAlarm : INT; // Index of first alarm (0 = none)
TripSequence : ARRAY[1..8] OF INT; // Order of activation
Tripped : BOOL; // Group has tripped
END_VAR
VAR
Latched : ARRAY[1..8] OF BOOL;
SequenceIdx : INT;
i : INT;
END_VARIF Reset THEN
FirstAlarm := 0;
SequenceIdx := 0;
Tripped := FALSE;
FOR i := 1 TO 8 DO
Latched[i] := FALSE;
TripSequence[i] := 0;
END_FOR;
RETURN;
END_IF;
FOR i := 1 TO 8 DO
IF Conditions[i] AND NOT Latched[i] THEN
Latched[i] := TRUE;
SequenceIdx := SequenceIdx + 1;
TripSequence[SequenceIdx] := i;
IF FirstAlarm = 0 THEN
FirstAlarm := i;
Tripped := TRUE;
END_IF;
END_IF;
END_FOR;
Alarm Shelving
Shelving temporarily suppresses known nuisance alarms during maintenance, with automatic un-shelve after a time limit:
FUNCTION_BLOCK FB_AlarmShelve
VAR_INPUT
ShelveCmd : BOOL; // Operator shelve request
UnshelveCmd : BOOL; // Manual unshelve
MaxDuration : TIME := T#8h; // Auto-unshelve limit
END_VAR
VAR_OUTPUT
Shelved : BOOL;
TimeRemaining : TIME;
END_VAR
VAR
ShelveTimer : TON;
R_Shelve : R_TRIG;
END_VARR_Shelve(CLK := ShelveCmd);
IF R_Shelve.Q THEN
Shelved := TRUE;
END_IF;
IF UnshelveCmd THEN
Shelved := FALSE;
END_IF;
// Auto-unshelve timer
ShelveTimer(IN := Shelved, PT := MaxDuration);
IF ShelveTimer.Q THEN
Shelved := FALSE;
END_IF;
IF Shelved THEN
TimeRemaining := MaxDuration - ShelveTimer.ET;
ELSE
TimeRemaining := T#0s;
END_IF;
Alarm Deadband for Analog Signals
Prevent alarm chatter when a process value oscillates near the setpoint:
FUNCTION_BLOCK FB_AnalogAlarm
VAR_INPUT
ProcessValue : REAL; // Current measurement
HighLimit : REAL; // Alarm threshold
Deadband : REAL; // Hysteresis band
Enable : BOOL := TRUE;
END_VAR
VAR_OUTPUT
HighAlarm : BOOL; // Alarm active output
END_VARIF NOT Enable THEN
HighAlarm := FALSE;
RETURN;
END_IF;
// Set alarm when above limit
IF ProcessValue >= HighLimit THEN
HighAlarm := TRUE;
END_IF;
// Clear alarm only when below limit minus deadband
IF ProcessValue < (HighLimit - Deadband) THEN
HighAlarm := FALSE;
END_IF;
Deadband Example
With HighLimit = 80.0 and Deadband = 2.0:
Alarm Logging and History
Track alarm events for analysis and compliance:
TYPE AlarmEvent :
STRUCT
AlarmID : INT;
EventType : INT; // 1=Activate, 2=Clear, 3=Ack
Timestamp : DT;
Value : REAL; // Process value at event time
END_STRUCT
END_TYPEFUNCTION_BLOCK FB_AlarmLogger
VAR
EventBuffer : ARRAY[1..500] OF AlarmEvent;
WriteIndex : INT := 1;
EventCount : DINT;
END_VAR
VAR_INPUT
LogEvent : BOOL;
NewEvent : AlarmEvent;
END_VAR
VAR
R_Log : R_TRIG;
END_VAR
R_Log(CLK := LogEvent);
IF R_Log.Q THEN
EventBuffer[WriteIndex] := NewEvent;
WriteIndex := WriteIndex + 1;
IF WriteIndex > 500 THEN
WriteIndex := 1; // Circular buffer
END_IF;
EventCount := EventCount + 1;
END_IF;
ISA-18.2 Key Metrics
Monitor these KPIs to evaluate alarm system health:
| Metric | Target | Description |
|---|---|---|
| Alarm rate | < 6/hr per operator | Average alarms during normal operation |
| % time in flood | < 1% | Periods with > 10 alarms in 10 minutes |
| Stale alarms | < 5% | Alarms active > 24 hours |
| Chattering | < 5% | Alarms activating > 5 times in 1 minute |
| Priority distribution | ~80% Low/Med | Most alarms should NOT be critical |
Best Practices Summary
Build and test alarm logic patterns in our online ST editor. See our function blocks tutorial for reusable FB design patterns.
The Lifecycle Is the Product; the Code Is One Stage
ISA-18.2 / IEC 62682 defines ten lifecycle stages: philosophy, identification, rationalisation, detailed design, implementation, operation, maintenance, monitoring and assessment, management of change, and audit. Every block above sits in implementation — one stage in ten, and the only one that cannot rescue a bad alarm list.
So the alarm list needs one source of truth, and it must not be the PLC. Rationalisation produces a master alarm database, and you generate the PLC constants and the HMI text from it. Hand-type alarm text into both and they disagree within a year.
And the alarm setpoint is not the trip setpoint. An alarm sitting on the interlock value only tells the operator the shutdown already happened. Back it off by at least (allowable response time × worst-case rate of change), taken from trend data.
What Rationalisation Actually Decides
An operator, a process engineer and a controls engineer go tag by tag. Each candidate must survive four questions: the cause, the consequence of doing nothing, the corrective action available from the console right now, and how long the operator has.
Question three is the guillotine. No operator action means it is not an alarm — it is an event or a work order, and applying that test honestly removes the bulk of a legacy list. Priority then falls out of a documented severity-versus-response-time matrix; you never tune it to hit a target distribution, because the distribution is a result you measure, not an input you set.
Chattering: Deadband Is Only Half the Fix
Deadband cures chatter on a clean, slow signal. It does nothing when peak-to-peak noise exceeds the band you chose — the PV crosses the limit and falls back below (limit − deadband) inside the same second.
The mistake juniors reliably make is sizing deadband from the setpoint rather than the instrument span. Two percent of an 80 °C setpoint is 1.6 °C; two percent of a 0–600 °C transmitter is 12 °C, and noise scales with span. EEMUA 191 publishes default deadbands by service, and the usual starting points put flow and level at the wide end, pressure in between, and temperature narrowest — a starting point, not a substitute for reading the trend.
On-delay, not deadband, is what kills fleeting alarms — the spikes a deadband cannot see. Make the delays asymmetric: short to set, long to clear. A long on-delay eats response time; a long off-delay costs nothing and stops the alarm flickering off during recovery. On-delay must stay small against the time-to-consequence recorded during rationalisation — give the operator 60 seconds, spend 30 of them hiding a noisy transmitter, and you have deleted half the response time. Fix the transmitter instead.
FUNCTION_BLOCK FB_ProcessAlarm
// Analogue alarm with noise-aware deadband, asymmetric qualification,
// state-based enabling and cause/effect suppression.
// Raw is always historised. Alarm is only what reaches the operator.
VAR_INPUT
PV : REAL; // filtered process value, engineering units
Limit : REAL;
Deadband : REAL; // sized from signal noise, NOT from Limit
OnDelay : TIME := T#3s; // must be << time-to-consequence
OffDelay : TIME := T#20s; // longer: stops flicker during recovery
PVBad : BOOL; // AI channel / transmitter diagnostic
EquipRunning : BOOL; // state gate: alarm only valid when running
SettleTime : TIME := T#45s; // start-up grace period
ParentActive : BOOL; // cause/effect parent alarm asserted
ParentHold : TIME := T#60s; // window held open after the parent clears
END_VAR
VAR_OUTPUT
Alarm : BOOL; // presented to the operator
Raw : BOOL; // real condition - always logged
Suppressed : BOOL; // real, but deliberately not presented
BadPV : BOOL; // instrument alarm, raised instead of the process alarm
END_VAR
VAR
Over : BOOL;
OnTmr : TON;
OffTmr : TON;
SettleTmr : TON;
HoldTmr : TOF;
END_VAR// 1. Deadband on the raw comparison, before any timing
IF PV >= Limit THEN
Over := TRUE;
ELSIF PV < (Limit - Deadband) THEN
Over := FALSE;
END_IF;
// 2. Asymmetric qualification: quick to set, slow to release
OnTmr(IN := Over, PT := OnDelay);
OffTmr(IN := NOT Over, PT := OffDelay);
IF OnTmr.Q THEN
Raw := TRUE;
ELSIF OffTmr.Q THEN
Raw := FALSE;
END_IF;
// 3. A faulty transmitter must never raise a process alarm.
// An over-range fault current (NAMUR NE 43: >= 21 mA, above the 20.5 mA
// saturation ceiling) scales past full span, so a bare limit comparison
// trips just as it would on a real excursion. The channel diagnostic is
// what tells the two apart - so gate on it.
BadPV := PVBad;
IF PVBad THEN
Raw := FALSE;
END_IF;
// 4. Start-up grace period and cause/effect window
SettleTmr(IN := EquipRunning, PT := SettleTime);
HoldTmr(IN := ParentActive, PT := ParentHold); // TOF: stays TRUE past the clear
// 5. Suppression is an explicit output, so the HMI can show WHY it is silent
Suppressed := Raw AND ( NOT EquipRunning
OR NOT SettleTmr.Q
OR HoldTmr.Q );
Alarm := Raw AND NOT Suppressed;
END_FUNCTION_BLOCK
Shelving, Suppression and Out-of-Service Are Not the Same Thing
The anti-pattern is one Inhibit bit doing all three jobs. Six months on, nobody can tell whether an alarm is silent by design or because a technician inhibited it for calibration and went on leave. Give each its own flag, plus one HMI screen listing every alarm currently unable to reach the operator.
The commissioning gotcha: retain the shelve flag and you must retain the shelve timer. Declare Shelved as VAR_RETAIN while the TON holding the eight-hour limit is not, and a power cycle brings the plant back with the flag still set and the elapsed time gone. The countdown restarts from zero, so every restart hands the alarm another full eight hours and the shelve quietly outlives the window it was granted. Store an absolute expiry DT against the real-time clock instead, and block shelving on the top priority class entirely.
Flood Suppression Without Blinding the Operator
A compressor trip produces dozens of alarms, nearly all of them consequences of the first. State-based enabling binds each alarm to an equipment state plus a settle period — most start-up floods are alarms that are perfectly valid at steady state being evaluated during a transient. Cause-and-effect suppression lets a defined parent suppress a defined list of children for a defined window. "Defined" carries that sentence: a generic if the rate exceeds N per minute, stop presenting alarms rule is a coin toss, because you cannot say in advance which alarm gets dropped.
Suppressed alarms still go to the historian — suppression removes an alarm from the screen, never from the record. And watch the un-mask stampede: when the parent clears, hold the window open past it (the TOF above) so each child re-qualifies against its own on-delay.