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.

What goes wrong with alarms

Poor alarm management has played a part in many industrial incidents. When operators are overwhelmed by nuisance or low-priority alerts, they can miss the critical alarms among them; this is called "alarm flooding." The ISA-18.2 standard (IEC 62682) gives a framework for designing alarm systems that avoid it. This guide shows how to implement its principles in Structured Text.

The alarm data structure

Start with a data structure that holds every state an alarm can be in: its priority class, its state in the alarm state machine, the raw condition, the acknowledge and shelve flags, and the timestamps:

TYPE AlarmClass : (
    ALARM_NONE,
    ALARM_LOW,
    ALARM_MEDIUM,
    ALARM_HIGH,
    ALARM_CRITICAL
);
END_TYPE

TYPE 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

A basic alarm function block

This reusable function block implements the four-state alarm model used in ISA-18.2. An alarm goes from normal to active and unacknowledged; from there the operator can acknowledge it while it is still active, or the condition can clear first, leaving it cleared but unacknowledged until someone acknowledges it:

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 classes

ISA-18.2 recommends a structured priority scheme, based on the consequence and on how quickly the operator has to respond. A practical version looks like this:

PriorityResponse TimeConsequenceExample
CriticalImmediateSafety hazardE-Stop, gas leak
High< 5 minEquipment damageOver-temperature, over-pressure
Medium< 30 minProcess deviationSetpoint drift, low level
LowNext shiftMaintenance neededFilter dirty, vibration warning
The alarm manager runs 100 alarm instances. It passes a global acknowledge to all of them, counts the active and unacknowledged alarms, finds the highest active priority, and drives the horn while any alarm is unacknowledged and the horn has not been silenced:

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_VAR

ActiveCount := 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

After a trip on a critical process, the first thing you want to know is which alarm triggered first, because that is where the root cause analysis starts. A first-out group latches each input and records the order in which they came in:

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_VAR

IF 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 a known nuisance alarm during maintenance. The block un-shelves the alarm automatically when the time limit runs out:

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_VAR

R_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;

Deadband on analog alarms

A deadband stops an alarm chattering when the process value oscillates around the limit. The alarm sets at the limit but clears only once the value has dropped below the limit minus the deadband:

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_VAR

IF 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, the alarm activates when the value reaches 80.0°C and clears only when the value drops below 78.0°C. A value hovering just around the limit no longer cycles the alarm rapidly on and off ("chattering").

Alarm logging and history

Log every alarm event for later analysis and for compliance. This logger writes activation, clear and acknowledge events into a 500-entry circular buffer:

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_TYPE

FUNCTION_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 performance metrics

Measure these figures to judge whether the alarm system is healthy:

MetricTargetDescription
Alarm rate< 6/hr per operatorAverage alarms during normal operation
% time in flood< 1%Periods with > 10 alarms in 10 minutes
Stale alarmsFewer than 5 on any dayAlarms active > 24 hours
Chattering0Alarms activating 3 or more times in 1 minute
Priority distribution~80% Low/MedMost alarms should NOT be critical

The alarm lifecycle: the code is one stage of ten

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 belongs to implementation. That is one stage in ten, and it is the one stage that cannot rescue a bad alarm list.

The alarm list therefore needs one source of truth, and that source must not be the PLC. Rationalisation produces a master alarm database, and you generate the PLC constants and the HMI text from it. If you hand-type the alarm text into both, they will disagree within a year.

The alarm setpoint is not the trip setpoint, either. An alarm sitting on the interlock value only tells the operator that the shutdown has already happened. Back it off by at least (allowable response time × worst-case rate of change), with both figures taken from trend data.

What rationalisation decides

An operator, a process engineer and a controls engineer go through the list tag by tag. Each candidate alarm has to survive four questions: what causes it, what the consequence of doing nothing is, what corrective action is available from the console right now, and how long the operator has.

The third question removes the most. If the operator has no action to take, it is not an alarm; it is an event or a work order. Applied honestly, that test removes the bulk of a legacy list. Priority then comes from a documented severity-versus-response-time matrix. Do not tune priorities to hit a target distribution: the distribution is a result you measure afterwards.

Chattering: deadband and on-delay

Deadband cures chatter on a clean, slow signal. It does nothing when the peak-to-peak noise is larger than the band you chose, because the PV then crosses the limit and falls back below (limit − deadband) inside the same second.

Juniors often size the deadband from the setpoint when it should come from 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. The usual starting points put flow and level at the wide end, pressure in between and temperature narrowest, but they are only starting points, and you still have to read the trend.

Fleeting alarms, the spikes a deadband cannot see, need an on-delay instead. Make the delays asymmetric: short to set, long to clear. A long on-delay eats into the operator's response time, while 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. If the operator has 60 seconds and you spend 30 of them hiding a noisy transmitter, you have deleted half the response time. Fix the transmitter instead.

The block below puts all of this together. It applies the deadband to the raw comparison, qualifies the result with asymmetric delays, refuses to raise a process alarm on a faulty transmitter, and reports suppression as its own output:

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

Shelved, suppressed and out of service are three different states

An alarm can be silent for three different reasons, and ISA-18.2 treats each one separately.

A shelved alarm was shelved by the operator. Shelving is time-limited, the alarm returns automatically, and it is visible on a shelf list.

An alarm suppressed by design is silenced by logic that was agreed during rationalisation. Nobody presses anything: a low-flow alarm is meaningless while its pump is commanded off.

An alarm out of service was taken out by maintenance for a known-faulty instrument. That state is indefinite, so it belongs under management of change, not behind a button.

A common mistake is one Inhibit bit doing all three jobs. Six months later, nobody can tell whether an alarm is silent by design or because a technician inhibited it for calibration and then went on leave. Give each state its own flag, and add one HMI screen that lists every alarm currently unable to reach the operator.

Watch for this at commissioning: if you retain the shelve flag, 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 gives the alarm another full eight hours, and the shelve 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.

Suppressing alarm floods

A compressor trip produces dozens of alarms, and nearly all of them are consequences of the first. Two techniques deal with that.

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 alarm suppress a defined list of child alarms for a defined window. Each of those must be defined in advance. A generic rule such as if the rate exceeds N per minute, stop presenting alarms 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. Also watch for the stampede when the suppression lifts. When the parent clears, hold the window open past it (the TOF above) so that each child re-qualifies against its own on-delay.

Alarm design checklist

  • Use the four-state model (Normal, Active-Unack, Active-Ack, Cleared-Unack) for every alarm.
  • Put a deadband on every analog alarm to prevent chattering, sized from the instrument span.
  • Limit alarm rates. If operators see more than 6 alarms per hour, redesign.
  • Add first-out logic to critical trip systems to support root cause analysis.
  • Shelve alarms for planned maintenance only with auto-unshelve timers.
  • Log every event (activation, clearing and acknowledgment) with timestamps.
  • Review the alarm list regularly; ISA-18.2 recommends periodic alarm rationalization.
  • Test alarm suppression: shelved and disabled alarms must restore automatically.
  • Build and test these alarm patterns in the online ST editor. The function blocks tutorial covers how to design reusable FBs like the ones above.