Chapter 19 / 24 · Part 5 · Reusable code

Analog signals and alarms

Scale 4–20 mA raw counts to engineering units, filter noise, detect broken wires and raise delayed alarms.

From a current loop to a number

Most plant measurements still travel as a current between 4 and 20 milliamps. A level transmitter on a 30 m³ buffer tank, a Rosemount 3051 on a pump discharge, a temperature transmitter on a jacketed mixer: each pushes a current through a two-wire loop, and the analog card in the rack measures it.

The 4 mA bottom is not an accident. A two-wire transmitter powers itself from the loop, so it needs a few milliamps even at zero. And a "live zero" means 0 mA is not a valid reading: it means a broken wire, a blown fuse or a dead transmitter.

The card does not give your program milliamps. It gives an integer, and the integer range depends on the vendor:

Card4 mA20 mANotes
Siemens S7-1500 / ET 200SP AI027,648Overrange up to 32,511; 32,767 (7FFFh) when a diagnosed fault such as wire break is active
Beckhoff EL3052 / EL3152032,76716-bit signed; underrange shows as 0, error bits in the status word
Rockwell CompactLogix 5069-IF8configurableconfigurableThe module usually scales to engineering units itself; check the module properties before scaling again
CODESYS-based I/O (WAGO 750-455 etc.)032,760 or 32,767Read the module datasheet; the last few counts differ

Two rules follow. Look the range up for the exact module — never assume 32,767. And keep raw and engineering values in separate variables: diagnostics need the first, control the second.

Linear scaling

Turning counts into engineering units is a straight line through two known points: (x0, y0) at the bottom of the range and (x1, y1) at the top. For any reading x:

y = (x − x0) · (y1 − y0) / (x1 − x0) + y0

On a Siemens card for a 0–5 m level transmitter that is x0 = 0, x1 = 27,648, y0 = 0.0, y1 = 5.0. The same formula scales counts to milliamps (y0 = 4, y1 = 20), milliamps to percent, or anything linear to anything linear. It belongs in a function, written once:

FUNCTION F_Scale : REAL
VAR_INPUT
    X  : REAL;   (* reading *)
    X0 : REAL;   (* reading at the bottom of the range *)
    X1 : REAL;   (* reading at the top of the range *)
    Y0 : REAL;   (* engineering value at X0 *)
    Y1 : REAL;   (* engineering value at X1 *)
END_VAR
IF X1 = X0 THEN
    F_Scale := Y0;          (* misconfigured range: never divide by zero *)
    RETURN;
END_IF;
F_Scale := (X - X0) * (Y1 - Y0) / (X1 - X0) + Y0;
END_FUNCTION

PROGRAM Main
VAR
    LoopmA      : REAL := 12.0;   (* input: current in the loop *)
    RawS7       : INT;            (* what a Siemens card would report *)
    RawEL       : INT;            (* what a Beckhoff EL3052 would report *)
    LevelFromS7 : REAL;           (* m *)
    LevelFromEL : REAL;           (* m *)
END_VAR

(* Simulate the two cards measuring the same current *)
RawS7 := REAL_TO_INT(F_Scale(X := LoopmA, X0 := 4.0, X1 := 20.0, Y0 := 0.0, Y1 := 27648.0));
RawEL := REAL_TO_INT(F_Scale(X := LoopmA, X0 := 4.0, X1 := 20.0, Y0 := 0.0, Y1 := 32767.0));

(* What your program does: counts to metres *)
LevelFromS7 := F_Scale(X := INT_TO_REAL(RawS7), X0 := 0.0, X1 := 27648.0, Y0 := 0.0, Y1 := 5.0);
LevelFromEL := F_Scale(X := INT_TO_REAL(RawEL), X0 := 0.0, X1 := 32767.0, Y0 := 0.0, Y1 := 5.0);

END_PROGRAM
Live example — open this page with JavaScript to run and edit it. Inputs you can drive: LoopmA.

Type 4, 12 and 20 mA into LoopmA. Different counts, same level — as long as each program uses its own card's range. Then type 2 mA. Both cards produce negative counts and the "level" goes negative. That is the next problem.

Do the scaling in REAL. Integer versions like Raw * 500 / 27648 look tempting for a level in centimetres, but 13824 * 500 is 6.9 million, far beyond an INT. Whether the intermediate result wraps depends on the compiler — some widen it to 32 bits, some do not — so convert to REAL first and the question never arises. TIA Portal's NORM_X / SCALE_X pair and the old SCALE FC do the same job; in Studio 5000 ladder you would reach for SCP or a CPT. In ST the formula is shorter than the call.

Is the signal believable? Wire break and NAMUR NE 43

A scaled value is only useful if the signal is real. NAMUR recommendation NE 43 gives transmitters and control systems a shared language for this:

Loop currentMeaning
3.8 – 20.5 mAValid measurement (slightly outside 4–20 is still a reading, just saturated)
3.6 – 3.8 mA or 20.5 – 21.0 mAOut of range — the process is beyond what the transmitter is set up for
below 3.6 mAFailure: transmitter fault signal, or a broken wire / blown fuse at 0 mA
above 21.0 mAFailure: transmitter fault signal, or a short circuit

A NAMUR transmitter that detects an internal fault deliberately drives its output to ≤ 3.6 mA or ≥ 21 mA, so these thresholds catch transmitter failures as well as wiring faults. Your program should compute the current from the raw value, test it against these limits and treat the reading as bad when it fails. Ignore this and a broken wire does something specific and dangerous:

What to do with a bad signal is a process decision: hold the last good value for a few seconds and then go to a safe state, substitute a fixed value, or switch the loop to manual. Whichever you choose, make it visible — an alarm, a quality bit on the HMI — so nobody spends a night chasing a "process problem" that is a loose terminal. The first challenge below builds this check.

Filtering noise

Analog signals are noisy. A level transmitter on an agitated tank sees the surface slop; a flow signal jitters with pump pulsation. The standard PLC answer is a first-order filter, also called exponential smoothing. Each scan, the filtered value moves a fixed fraction of the way towards the raw value:

Filtered := Filtered + K · (Raw − Filtered), with K = Ts / (Tf + Ts)

Ts is the scan (or task) time and Tf the filter time constant. With Ts = 0.1 s and Tf = 1 s, K is about 0.09: each scan the output covers 9 % of the remaining gap. After one time constant it has covered about 63 % of a step, after three about 95 %.

PROGRAM Main
VAR
    Tf       : REAL := 1.0;     (* input: filter time constant, s *)
    StepUp   : BOOL;            (* input: process level steps from 40 to 60 % *)
    Seed     : DINT := 12345;   (* pseudo-random noise generator *)
    Signal   : REAL;            (* what the transmitter sends: level + noise *)
    Filtered : REAL;
    K        : REAL;
    First    : BOOL := TRUE;
    Gap      : REAL;
END_VAR

Seed   := (Seed * 1103 + 12345) MOD 32768;
Signal := SEL(StepUp, 40.0, 60.0) + DINT_TO_REAL(Seed MOD 101) / 20.0 - 2.5;   (* ±2.5 % noise *)

K := 0.1 / (MAX(Tf, 0.0) + 0.1);    (* Ts = 0.1 s on this page *)
IF First THEN
    Filtered := Signal;             (* start at the signal, not at zero *)
    First := FALSE;
END_IF;
Gap      := Signal - Filtered;
Filtered := Filtered + K * Gap;

END_PROGRAM
Live example — open this page with JavaScript to run and edit it. Inputs you can drive: Tf, StepUp.

Watch Signal jump around by ±2.5 % while Filtered stays within a few tenths. Flip StepUp and notice the price: the filtered value takes seconds to follow a real change. Try Tf = 5 and the lag becomes obvious; try 0 and the filter switches off.

Cards filter too: an S7-1500 AI module has an integration time against 50/60 Hz mains noise and a smoothing setting; Beckhoff EL3xxx terminals have filters in their CoE objects. Use the card for electrical noise, the program for process noise, and record both in the I/O list.

HH, H, L and LL alarms

A typical tank level carries four alarm limits: HH (high-high, trips the inlet), H (high, warns the operator), L (low, warns) and LL (low-low, trips the pump to protect it from running dry). Each limit needs two refinements or it will chatter:

  • Hysteresis (dead band): the alarm comes on above the limit and goes off only below the limit minus a margin. A level sitting at 90.0 % with 0.3 % of noise would otherwise flick the H alarm on and off ten times a second, and the alarm log fills with rubbish. Chapter 6 introduced hysteresis; here it lives in a reusable block.
  • On-delay: the condition must persist for a set time before the alarm is raised. A wave on the surface should not trip the plant. Typical values are 2–10 s for process alarms, much shorter for trips.

One generic block covers all four limits — a natural use of the function blocks from chapter 18:

FUNCTION_BLOCK FB_LimitAlarm
VAR_INPUT
    Value : REAL;
    Limit : REAL;
    Hyst  : REAL;              (* dead band below a high limit / above a low one *)
    High  : BOOL;              (* TRUE = alarm above the limit *)
    Delay : TIME;              (* condition must persist this long *)
END_VAR
VAR_OUTPUT
    Active : BOOL;
END_VAR
VAR
    Dly : TON;
    Beyond : BOOL;
    Clear  : BOOL;
END_VAR

IF High THEN
    Beyond := Value > Limit;
    Clear  := Value < Limit - Hyst;
ELSE
    Beyond := Value < Limit;
    Clear  := Value > Limit + Hyst;
END_IF;

Dly(IN := Beyond, PT := Delay);
IF Dly.Q THEN
    Active := TRUE;
ELSIF Clear THEN
    Active := FALSE;
END_IF;

END_FUNCTION_BLOCK

PROGRAM Main
VAR
    Level : REAL := 50.0;   (* input: tank level, % *)
    AlmHH : FB_LimitAlarm;
    AlmH  : FB_LimitAlarm;
    AlmL  : FB_LimitAlarm;
    AlmLL : FB_LimitAlarm;
END_VAR

AlmHH(Value := Level, Limit := 95.0, Hyst := 2.0, High := TRUE,  Delay := T#1s);
AlmH(Value := Level, Limit := 85.0, Hyst := 2.0, High := TRUE,  Delay := T#3s);
AlmL(Value := Level, Limit := 20.0, Hyst := 2.0, High := FALSE, Delay := T#3s);
AlmLL(Value := Level, Limit := 10.0, Hyst := 2.0, High := FALSE, Delay := T#1s);

END_PROGRAM
Live example — open this page with JavaScript to run and edit it. Inputs you can drive: Level.

Type 86 and watch AlmH.Dly.ET count to 3 s before AlmH.Active comes on. Type 84: the alarm stays on, because 84 is inside the dead band. Type 82.9 and it clears. Type 86 for a second and back to 80, and nothing happens — the delay filtered out the wave.

Notice the structure: the delay applies to raising the alarm, the hysteresis to clearing it. Latching alarms until an operator acknowledges them, first-out logic and alarm priorities are the business of chapter 22. This block only answers "is the condition true, convincingly?"

Rate of change

Some faults show in how fast a value moves, not where it is. A level falling 1.5 %/s with every outlet shut is a leak; a bearing temperature climbing 5 °C per minute is in trouble long before its high limit; a jump of 40 % in one scan is a failed transmitter.

Rate of change is the difference between two readings divided by the time between them. Do not compute it scan to scan: on a 10 ms task, ordinary noise of 0.2 % becomes an apparent rate of 20 %/s. Sample at a fixed, longer interval instead — one second is typical for levels — and compare samples:

PROGRAM Main
VAR
    OutletOpen : BOOL;          (* input: outlet valve open *)
    Leak       : BOOL;          (* input: simulate a leaking flange *)
    Level      : REAL := 80.0;  (* simulated tank level, % *)
    Prev       : REAL;
    Rate       : REAL;          (* %/s, negative = falling *)
    SampleCnt  : INT;
    First      : BOOL := TRUE;
    LeakAlarm  : BOOL;
END_VAR

(* Plant simulation, per 100 ms scan *)
IF OutletOpen THEN Level := Level - 0.05; END_IF;   (* normal draw: 0.5 %/s *)
IF Leak THEN Level := Level - 0.15; END_IF;         (* leak: 1.5 %/s *)

(* Sample once per second: 10 scans of this 100 ms task *)
IF First THEN
    Prev := Level;
    First := FALSE;
END_IF;
SampleCnt := SampleCnt + 1;
IF SampleCnt >= 10 THEN
    SampleCnt := 0;
    Rate := Level - Prev;       (* change over 1 s, so already %/s *)
    Prev := Level;
END_IF;

LeakAlarm := NOT OutletOpen AND Rate < -0.3;

END_PROGRAM
Live example — open this page with JavaScript to run and edit it. Inputs you can drive: OutletOpen, Leak.

Open the outlet: the rate settles at −0.5 %/s and no alarm, because the drop is explained. Close it and switch Leak on: −1.5 %/s with the outlet shut raises LeakAlarm. On a real controller, put this in a cyclic task with a fixed period — a 100 ms cyclic interrupt OB on Siemens, a fixed-interval task in CODESYS or TwinCAT — so that counting calls really does count time. In a free-running task the scan time varies, and you would measure the sample interval with a timer instead. (The long decimals in the watch are this engine's 64-bit REAL arithmetic; a PLC's 32-bit REAL would show different rounding, and neither should ever be compared with =.)

From measuring to controlling

You can now take a raw count from any card, scale it with one formula, decide whether to believe it, smooth the noise without hiding real changes, and raise alarms that neither chatter nor sleep through a real event.

So far the program has only watched the process. The next chapter closes the loop: ramping a setpoint so a heater or pump changes gently, and writing a PI controller that drives a valve or a drive speed until the measured value meets the setpoint — with a simulated tank in the same program, so you can watch it settle.

Practise this chapter