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_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

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:

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

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

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_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:

  • Alarm activates when value reaches 80.0°C
  • Alarm clears only when value drops below 78.0°C
  • Prevents rapid on/off cycling ("chattering")
  • 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_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 Key Metrics

    Monitor these KPIs to evaluate alarm system health:

    MetricTargetDescription
    Alarm rate< 6/hr per operatorAverage 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/MedMost alarms should NOT be critical

    Best Practices Summary

  • Use the four-state model (Normal, Active-Unack, Active-Ack, Cleared-Unack) for every alarm
  • Implement deadbands on all analog alarms to prevent chattering
  • Limit alarm rates — if operators see more than 6 alarms per hour, redesign
  • First-out logic for critical trip systems to support root cause analysis
  • Alarm shelving with auto-unshelve timers for planned maintenance
  • Log all events — activation, clearing, and acknowledgment with timestamps
  • Review regularly — ISA-18.2 recommends periodic alarm rationalization
  • Test alarm suppression — shelved and disabled alarms must auto-restore
  • 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

  • Shelved — operator-initiated, time-limited, auto-returns, visible on a shelf list.
  • Suppressed by design — logic-driven, agreed during rationalisation. Nobody presses anything: a low-flow alarm is meaningless while its pump is commanded off.
  • Out of service — maintenance-initiated for a known-faulty instrument, indefinite, and it belongs under management of change, not behind a button.
  • 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.