Warnings, alarms and trips
Ask three engineers what an alarm is and you will get four answers. Before writing a line of code, settle the words, because they decide what the program must do.
- A warning tells the operator something is drifting: a filter differential pressure creeping up, a lubrication tank at 20 %. The machine keeps running. Nobody has to press anything.
- An alarm demands an operator response within a defined time. It stays on the alarm list until someone acknowledges it, even if the condition has already gone away. ISA-18.2 and EEMUA 191, the two standards most alarm philosophies are written from, are blunt about this: if nobody needs to act, it is not an alarm, it is an event, and it belongs in a log.
- A trip (or fault) stops equipment. The program drives outputs to a safe state on its own, without waiting for anyone, and refuses to restart until a person has confirmed it is safe.
The same physical condition often produces all three. A gearbox oil temperature at 75 °C is a warning, at 85 °C an alarm, and at 95 °C a trip. Three thresholds, three behaviours, one sensor.
The most common failure in alarm handling is not a bug in a single line. It is a plant with 400 configured alarms, 60 of them active at any moment, and operators who acknowledge the horn without reading the screen. EEMUA 191 suggests that an operator can handle about one alarm every ten minutes in steady operation. Every alarm you add should be one somebody would be grateful for at 3 a.m.
Detect: a condition and a delay
Raw conditions chatter. A float switch bounces as the surface ripples, a pressure reading dips for 200 ms when a valve opens elsewhere, a flow switch reads "no flow" for the first two seconds after a pump starts because the water has not got there yet. If you alarm on the raw condition, you get nuisance alarms, and nuisance alarms train operators to ignore real ones.
So detection has two parts. First, qualify the condition so it only counts when it means something: no-flow only matters while the pump is supposed to be running. Second, delay it with a TON so it must persist before it counts. Chapter 19 applied the same idea to analog alarms with deadbands; here the focus is on what happens after detection.
PROGRAM Main
VAR
PumpRunning : BOOL; (* input: contactor feedback *)
FlowSwitch : BOOL; (* input: paddle flow switch, TRUE = flow *)
NoFlowCond : BOOL;
FlowDelay : TON;
LowFlow : BOOL; (* detected alarm condition *)
END_VAR
(* Qualify: "no flow" only means something while the pump runs *)
NoFlowCond := PumpRunning AND NOT FlowSwitch;
(* Delay: 3 s covers pipe fill time after a start *)
FlowDelay(IN := NoFlowCond, PT := T#3s);
LowFlow := FlowDelay.Q;
END_PROGRAMSwitch PumpRunning on and leave FlowSwitch off. FlowDelay.ET climbs and LowFlow comes on after 3 s. Switch FlowSwitch on at 2 s instead and the timer resets: a normal start, no alarm. Choose the delay from the process, not from habit. Pipe fill time, valve stroke time and transmitter response are all measurable on commissioning; 3 s here is a typical figure for a few metres of DN50 pipe.
LowFlow is not yet an alarm. It follows the condition: when flow returns, it vanishes. If it disappeared in 4 seconds while the operator was on the other side of the line, nobody would ever know it happened. That is what latching is for.
Latch until acknowledged
An alarm has two independent pieces of state, and mixing them up is the root of most broken alarm code:
- Is the condition still present? That is what detection tells you, scan by scan.
- Has the operator seen it? That is memory, and only the operator's acknowledge clears it.
Together they give four states, and the alarm stays on the list in three of them:
| Condition | Acknowledged | Alarm list | Lamp | Horn |
|---|---|---|---|---|
| Present | No | Shown | Flashing | On |
| Present | Yes | Shown | Steady | Off |
| Gone | No | Shown | Flashing | On |
| Gone | Yes | Removed | Off | Off |
The third row is the one beginners miss. A condition that came and went must stay visible until somebody acknowledges it, otherwise a 4-second overload at 2 a.m. leaves no trace. And the second row means acknowledging does not make the alarm go away; it only says "I know". The alarm leaves the list only when the condition is gone and it has been acknowledged.
PROGRAM Main
VAR
HighTemp : BOOL; (* input: detected condition (already delayed) *)
Ack : BOOL; (* input: acknowledge push button *)
Shown : BOOL; (* on the alarm list *)
Unacked : BOOL; (* waiting for the operator *)
Horn : BOOL; (* output *)
Lamp : BOOL; (* output: flashing = unacknowledged *)
FlashT : TON;
Flash : BOOL;
END_VAR
(* A new occurrence: show it and demand an acknowledge *)
IF HighTemp AND NOT Shown THEN
Shown := TRUE;
Unacked := TRUE;
END_IF;
IF Ack THEN
Unacked := FALSE;
END_IF;
(* Leaves the list only when gone AND acknowledged *)
IF NOT HighTemp AND NOT Unacked THEN
Shown := FALSE;
END_IF;
(* 1 Hz flasher *)
FlashT(IN := NOT FlashT.Q, PT := T#500ms);
IF FlashT.Q THEN
Flash := NOT Flash;
END_IF;
Horn := Unacked;
Lamp := Shown AND (Flash OR NOT Unacked);
END_PROGRAMTry both orders. Switch HighTemp on, then Ack (and release it): the horn stops, the lamp goes steady, the alarm stays shown until you switch HighTemp off. Now switch HighTemp on and off again without acknowledging: the lamp keeps flashing on a condition that is already gone, until Ack clears it.
Acknowledge is not reset
Two buttons sit on most operator panels, often next to each other, and they mean different things.
Acknowledge is about people. It says "I have seen this alarm". It silences the horn and stops the flashing. It never changes what the machine does.
Reset is about the machine. It says "the cause has been dealt with; you may leave the tripped state". It clears latched trips so the equipment can be started again — but only if the condition that caused the trip is gone. A reset pressed while the motor is still overheating does nothing, and the code must make sure of that rather than trusting the operator to check.
Some plants combine them into one button that acknowledges on the first press and resets on the second. That is fine as long as the program still keeps the two states separate internally. What is not fine is a reset that clears a trip whose cause is still present, because the next start command will run straight into the same fault — or worse, start a pump against a closed valve that the trip was protecting.
First-out: which fault came first
When a machine trips, faults cascade. A compressor loses oil pressure, the motor trips on overload a fraction of a second later, discharge pressure falls, and three alarms appear on the screen within one second. The one that matters is the first. The others are consequences, and an engineer who chases the overload will replace a perfectly good motor starter.
A first-out record captures the cause. The rule is simple: when an alarm becomes active and no first-out is recorded yet, record this one. Leave the record alone until all alarms have cleared.
This is the natural place for an array of structures (chapter 15): one STRUCT per alarm holding its state, an array of them, a parallel array of delay timers, and one FOR loop that applies the same rules to all of them.
TYPE ST_Alarm :
STRUCT
Present : BOOL; (* condition, after its delay *)
Shown : BOOL; (* on the alarm list *)
Unacked : BOOL; (* waiting for the operator *)
END_STRUCT
END_TYPE
PROGRAM Main
VAR CONSTANT
N_ALARMS : INT := 3;
END_VAR
VAR
LowOilPress : BOOL; (* input *)
HighBearingTemp : BOOL; (* input *)
MotorOverload : BOOL; (* input *)
Ack : BOOL; (* input *)
Cond : ARRAY[1..3] OF BOOL;
Delay : ARRAY[1..3] OF TON;
Alarms : ARRAY[1..3] OF ST_Alarm;
AckEdge : R_TRIG;
FirstOut : INT; (* 0 = none, else the alarm number *)
AnyShown : BOOL;
Horn : BOOL;
i : INT;
END_VAR
Cond[1] := LowOilPress;
Cond[2] := HighBearingTemp;
Cond[3] := MotorOverload;
AckEdge(CLK := Ack);
AnyShown := FALSE;
Horn := FALSE;
FOR i := 1 TO N_ALARMS DO
Delay[i](IN := Cond[i], PT := T#300ms);
Alarms[i].Present := Delay[i].Q;
IF Alarms[i].Present AND NOT Alarms[i].Shown THEN
Alarms[i].Shown := TRUE;
Alarms[i].Unacked := TRUE;
IF FirstOut = 0 THEN
FirstOut := i;
END_IF;
END_IF;
IF AckEdge.Q THEN
Alarms[i].Unacked := FALSE;
END_IF;
IF NOT Alarms[i].Present AND NOT Alarms[i].Unacked THEN
Alarms[i].Shown := FALSE;
END_IF;
AnyShown := AnyShown OR Alarms[i].Shown;
Horn := Horn OR Alarms[i].Unacked;
END_FOR;
IF NOT AnyShown THEN
FirstOut := 0; (* list empty: ready for the next event *)
END_IF;
END_PROGRAMSwitch on HighBearingTemp, wait a moment, then MotorOverload. FirstOut shows 2 and stays 2. Acknowledge, switch both off, and once the list is empty FirstOut returns to 0. Adding a fourth alarm means one more line in the mapping section and a bigger array — the rules themselves are written once.
A safe state for every output
When a trip latches, the program must drive every affected output to its safe state. The beginner's version is "turn everything off". On many machines that is right. On many others it is dangerous.
Consider a 2,000-litre jacketed reactor running an exothermic batch:
| Output | Normal | Safe state on a trip | Why |
|---|---|---|---|
| Feed pump | Running | Off | Stop adding reactant |
| Steam valve | Modulating | Closed | Stop adding heat |
| Cooling water valve | Modulating | Fully open | Remove heat from a reaction that keeps going |
| Agitator | Running | Keeps running (unless it is the fault) | A still batch develops hot spots |
| Discharge valve | Closed | Closed | Never dump a batch mid-reaction |
The same thinking applies on discrete machines. A gripper holding a 30 kg part should hold, not open. A vertical axis should brake, not coast. A press should stop at the top of the stroke if it can. The safe state is a process decision, made with the people who know the process, written down per output, and implemented in one place: the command section, where each output has exactly one owner (chapter 21).
PROGRAM Main
VAR
Running : BOOL; (* input: batch in progress *)
Trip : BOOL; (* input: latched process trip *)
FeedPump : BOOL; (* output *)
SteamValve : BOOL; (* output, TRUE = open *)
CoolingValve : BOOL; (* output, TRUE = open *)
Agitator : BOOL; (* output *)
END_VAR
(* Command section: the trip is applied once, per output, in its own direction *)
FeedPump := Running AND NOT Trip;
SteamValve := Running AND NOT Trip;
CoolingValve := Trip; (* normal temperature control omitted here *)
Agitator := Running OR Trip;
END_PROGRAMSwitch Running on, then Trip. Feed and steam stop, cooling opens fully, and the agitator keeps turning even if Running drops. Every output names its trip behaviour in its own line. Nobody has to hunt for a "fault routine" that overrides outputs from somewhere else.
No automatic restart
When a trip clears, the machine must not start by itself. EN 60204-1 states it plainly for stop functions and power restoration: resetting must not on its own initiate a restart. The machine waits in a stopped state until an operator has reset the trip and then gives a fresh, deliberate start command.
The classic bug is a start latch that survives the trip. A conveyor's run latch is written Run := (Start OR Run) AND NOT Stop;, and the trip is applied only at the output: Motor := Run AND NOT Tripped;. The motor stops on a trip. Then the fitter clears the jam, somebody presses reset — and the conveyor lurches forward with the fitter's hand still on the belt, because Run never dropped.
Two rules prevent it:
- The trip drops the run latch itself, not just the output. After a trip, the machine's memory says "stopped".
- Start on an edge. A start button that is held, jammed or bridged by a stuck HMI bit while the reset is pressed must not count as a new start. Use an
R_TRIG, and only accept the edge while the machine is not tripped. An edge that arrives while tripped is simply lost; the operator presses again.
Restart rules also cover power-up. After a power failure, retained variables come back with their old values. A VAR RETAIN run latch would restart a mixer the moment the 24 V supply returns. Run latches belong in non-retained memory, so the machine powers up stopped; counts, totals and recipe values are what you retain.
From handling faults to finding them
You can now build alarm handling the way plants expect it: detect with a qualified, delayed condition, latch until acknowledged, keep acknowledge and reset apart, record the first-out cause, give every output a deliberate safe state and never restart without a person's say-so.
You have also seen how much of this logic only shows its bugs in a particular sequence — an acknowledge before the condition clears, a start held through a reset. Finding those sequences before the machine does is a skill of its own. The next chapter is about testing ST with plant cases, watching and forcing values online, and debugging from a hypothesis instead of from hope.