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:
| Card | 4 mA | 20 mA | Notes |
|---|---|---|---|
| Siemens S7-1500 / ET 200SP AI | 0 | 27,648 | Overrange up to 32,511; 32,767 (7FFFh) when a diagnosed fault such as wire break is active |
| Beckhoff EL3052 / EL3152 | 0 | 32,767 | 16-bit signed; underrange shows as 0, error bits in the status word |
| Rockwell CompactLogix 5069-IF8 | configurable | configurable | The module usually scales to engineering units itself; check the module properties before scaling again |
| CODESYS-based I/O (WAGO 750-455 etc.) | 0 | 32,760 or 32,767 | Read 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_PROGRAMType 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 current | Meaning |
|---|---|
| 3.8 – 20.5 mA | Valid measurement (slightly outside 4–20 is still a reading, just saturated) |
| 3.6 – 3.8 mA or 20.5 – 21.0 mA | Out of range — the process is beyond what the transmitter is set up for |
| below 3.6 mA | Failure: transmitter fault signal, or a broken wire / blown fuse at 0 mA |
| above 21.0 mA | Failure: 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_PROGRAMWatch 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_PROGRAMType 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_PROGRAMOpen 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.