Intermediate
PLC Analog Signal Scaling: Converting Raw Values to Engineering Units
Convert raw analog signals (0–27648, 4–20mA, 0–10V) to real-world engineering units with reusable Structured Text scaling functions.
From raw counts to engineering units
A PLC reads an analog input as a raw integer. Depending on the platform that is typically 0 to 27648 (Siemens), 0 to 32767, or 0 to 4095. A raw count means nothing to an operator, so you convert it to engineering units such as °C, PSI, gallons per minute or percent.
The linear scaling formula
Scaling is a straight-line interpolation between the raw range and the engineering range:
EngValue = ((RawValue - RawMin) / (RawMax - RawMin)) * (EngMax - EngMin) + EngMin
Basic scaling in Structured Text
PROGRAM AnalogScaling
VAR
RawInput : INT := 0; // Raw value from analog input
Temperature : REAL := 0.0; // Scaled value in °C
RawMin : REAL := 0.0; // 4mA = 0
RawMax : REAL := 27648.0; // 20mA = 27648
EngMin : REAL := 0.0; // 0°C
EngMax : REAL := 200.0; // 200°C
END_VAR
Temperature := ((INT_TO_REAL(RawInput) - RawMin) / (RawMax - RawMin))
* (EngMax - EngMin) + EngMin;
A reusable scaling function block
Put the formula in a reusable function block, with range checking and optional clamping, and call one instance per channel:
FUNCTION_BLOCK FB_AnalogScale
VAR_INPUT
RawValue : INT;
RawLow : REAL := 0.0;
RawHigh : REAL := 27648.0;
EngLow : REAL := 0.0;
EngHigh : REAL := 100.0;
ClampOutput : BOOL := TRUE;
END_VAR
VAR_OUTPUT
ScaledValue : REAL;
OutOfRange : BOOL;
END_VAR
VAR
RawReal : REAL;
END_VAR
RawReal := INT_TO_REAL(RawValue);
// Check for out-of-range
OutOfRange := (RawReal < RawLow) OR (RawReal > RawHigh);
// Scale
ScaledValue := ((RawReal - RawLow) / (RawHigh - RawLow))
* (EngHigh - EngLow) + EngLow;
// Clamp to engineering range
IF ClampOutput THEN
IF ScaledValue < EngLow THEN
ScaledValue := EngLow;
ELSIF ScaledValue > EngHigh THEN
ScaledValue := EngHigh;
END_IF;
END_IF;
Using the scaling FB
PROGRAM ProcessInputs
VAR
TempScale : FB_AnalogScale;
PressureScale : FB_AnalogScale;
FlowScale : FB_AnalogScale;
AI_Temperature : INT; // From hardware
AI_Pressure : INT;
AI_Flow : INT;
Temperature : REAL;
Pressure : REAL;
FlowRate : REAL;
SensorFault : BOOL;
END_VAR
TempScale(RawValue := AI_Temperature, EngLow := -50.0, EngHigh := 300.0);
Temperature := TempScale.ScaledValue;
PressureScale(RawValue := AI_Pressure, EngLow := 0.0, EngHigh := 150.0);
Pressure := PressureScale.ScaledValue;
FlowScale(RawValue := AI_Flow, EngLow := 0.0, EngHigh := 500.0);
FlowRate := FlowScale.ScaledValue;
SensorFault := TempScale.OutOfRange OR PressureScale.OutOfRange OR FlowScale.OutOfRange;
Detecting a wire break on 4-20mA
With a 4-20mA signal, a reading well below 4mA (raw 0 on a Siemens channel) indicates a wire break. The simple check below compares the raw value with a fixed threshold; the NE 43 section further down shows where the limits really belong:
PROGRAM WireBreakDetection
VAR
RawInput : INT;
WireBreak : BOOL := FALSE;
SensorOK : BOOL := FALSE;
LiveZeroThreshold : INT := 1382; // ~5% of 27648 ≈ 1mA
END_VAR
WireBreak := RawInput < LiveZeroThreshold;
SensorOK := NOT WireBreak AND (RawInput <= 27648);
Scaling an analog output (the reverse)
For an analog output, run the formula the other way and convert engineering units to raw counts. Clamp the input first, so an out-of-range setpoint cannot produce an invalid raw value:
FUNCTION_BLOCK FB_AnalogOutput
VAR_INPUT
EngValue : REAL;
EngLow : REAL := 0.0;
EngHigh : REAL := 100.0;
RawLow : REAL := 0.0;
RawHigh : REAL := 27648.0;
END_VAR
VAR_OUTPUT
RawOutput : INT;
END_VAR
VAR
Clamped : REAL;
Scaled : REAL;
END_VAR
// Clamp input
Clamped := EngValue;
IF Clamped < EngLow THEN Clamped := EngLow; END_IF;
IF Clamped > EngHigh THEN Clamped := EngHigh; END_IF;
// Reverse scale
Scaled := ((Clamped - EngLow) / (EngHigh - EngLow))
* (RawHigh - RawLow) + RawLow;
RawOutput := REAL_TO_INT(Scaled);
Common analog ranges by platform
| Platform | 4-20mA Range | 0-10V Range | Resolution |
|---|---|---|---|
| Siemens S7 | 0–27648 | 0–27648 | 16-bit |
| Allen-Bradley | 3277–16384 | 0–32767 | 15-bit |
| CODESYS | Configurable | Configurable | Module-dependent |
| Beckhoff | 0–32767 | 0–32767 | 16-bit |
Try it yourself
You can build a multi-sensor scaling program in the free PLC simulator. The function block templates include an analog scaling FB you can start from.
Choosing 4-20 mA or 0-10 V: decide by where the cable runs
A voltage input is high impedance and referenced to the module's analog common. That is fine inside the panel and wrong once the wire leaves it. Two volts of ground offset between panels lands straight on your reading, and a high-impedance node is an antenna: 0-10 V run alongside VFD motor cable picks up switching noise that no software filter cleanly removes. A current loop shrugs off most of that. The current is identical at every point in a series circuit, so cable resistance, terminal resistance and a corroded ferrule drop out of the measurement entirely, and the low-impedance path picks up far less induced noise. A current loop still has limits, though. Earthed at two points, it is a ground loop like any other, and that is what loop isolators are for.
In exchange you have to respect loop compliance. The 24 V supply must push 20 mA through every burden in series: the transmitter's own minimum operating voltage (commonly 10-12 V), the sense resistor, and any isolator or panel meter added later. A 250 ohm input alone drops 5 V at 20 mA. If you run out of compliance, the loop reads perfectly at 4 mA but clips somewhere short of full scale, because the burden voltage rises with the current. The process then appears to plateau below its real maximum, and everyone blames the sensor.
What live zero buys beyond wire break
The 4 mA is a diagnostic floor, and it is also the transmitter's own supply current, which is what makes two-wire devices possible. A 0-10 V or 0-20 mA signal cannot encode "I am broken", because zero is a legal reading.
That leads to a configuration trap. On a Siemens channel parameterised 0-20 mA instead of 4-20 mA, 4 mA lands at 5530 counts, not 0. The scaling still produces plausible engineering units, and a severed wire reads 0 counts, which that scaling reports as a clean bottom of range, so nothing alarms. Confirm that the channel range in the hardware config matches what your scaling assumes.
What the count table leaves out
Nominal 0-27648 is only the middle band, and Siemens modules keep reporting outside it. Roughly 27649-32511 is overrange and 32767 is overflow; negative counts down to about -4864 cover the underrange region below 4 mA, and -32768 is underflow. If you clamp to 0-27648 on your first line of code, you have thrown the diagnosis away before looking at it. Negative counts are normal at the bottom of the span, so a junior who writes IF Raw < 0 THEN Fault gets a nuisance trip every time the tank really does empty.
On Rockwell hardware the count range is something you configure per module. A 1756-IF8 in its floating-point data format scales inside the module and hands the controller a REAL, so rescaling it again in logic leaves you with two sources of truth. Configure the same module for integer data and you lose the on-board scaling and are back to raw counts. Either works, as long as only one of the two places does the conversion.
Reading NE 43 instead of inventing thresholds
NAMUR NE 43 fixes the vocabulary. 3.8-20.5 mA is the valid measuring range. At or below 3.6 mA, and at or above 21.0 mA, the transmitter is declaring a failure. Inside the valid range sit the two saturation bands, 3.8-4 mA and 20-20.5 mA, where the reading still tracks the process but has left the calibrated span. The gaps in between, 3.6-3.8 mA and 20.5-21.0 mA, are differentiation zones: deliberate dead bands that a compliant transmitter is not supposed to sit in. NE 43 tells the control system to read a stray value there as a measurement rather than as a fault. On a Siemens channel, 27648 counts span 16 mA, which is 1728 counts per mA.
TYPE E_SigQuality : (Q_GOOD, Q_UNDER_RANGE, Q_OVER_RANGE, Q_BAD, Q_SUSPECT); END_TYPE
FUNCTION_BLOCK FB_AI_Conditioner
(* Siemens-style 4..20 mA channel: 0 counts = 4 mA, 27648 counts = 20 mA,
so 27648 / 16 mA = 1728 counts per mA.
NE 43 converted at that rate:
3.6 mA = -691 at or below: transmitter is declaring a fault
3.8 mA = -346 lowest a compliant transmitter will drive
20.5 mA = 28512 highest a compliant transmitter will drive
21.0 mA = 29376 at or above: transmitter is declaring a fault
The bands that still track the process are the saturation bands either side
of the calibrated span: 3.8..4 mA (counts -346..0) and 20..20.5 mA
(counts 27648..28512). The gaps at 3.6..3.8 mA and 20.5..21.0 mA are NE 43
differentiation zones - a compliant transmitter does not sit there, and a
value that lands in one is read as measurement, not as a fault. So the span
edges, not the differentiation zones, are the under/overrange boundaries. *)
VAR_INPUT
Raw : INT; // straight from the PIW, unclamped
EngLow : REAL; // must equal the transmitter LRV
EngHigh : REAL; // must equal the transmitter URV
SampleTime : REAL := 0.1; // period of the calling task, seconds
FilterTC : REAL := 0.0; // filter time constant, seconds; 0 = off
MaxSlew : REAL := 0.0; // eng units / second; 0 = no slew check
SuspectLimit : INT := 3; // scans before a fast step is accepted as real
END_VAR
VAR_OUTPUT
PV : REAL;
Quality : E_SigQuality;
HoldLastGood : BOOL; // consumer must go manual / stop integrating
END_VAR
VAR CONSTANT
NE43_FAIL_LO : INT := -691; // 3.6 mA
SPAN_LO : INT := 0; // 4 mA
SPAN_HI : INT := 27648; // 20 mA
NE43_FAIL_HI : INT := 29376; // 21.0 mA
MOD_OVERFLOW : INT := 32767;
MOD_UNDERFLOW : INT := -32768;
END_VAR
VAR
Unfiltered : REAL;
LastGood : REAL;
Alpha : REAL;
SuspectScans : INT := 0;
Started : BOOL := FALSE;
END_VAR
// 1. Classify before scaling - the raw word carries the diagnosis.
// Test both module rails: which one appears on an open circuit depends
// on the module family and on whether wire-break diagnostics are enabled.
IF (Raw = MOD_OVERFLOW) OR (Raw = MOD_UNDERFLOW) THEN
Quality := Q_BAD;
ELSIF (Raw <= NE43_FAIL_LO) OR (Raw >= NE43_FAIL_HI) THEN
Quality := Q_BAD; // transmitter is declaring a fault
ELSIF Raw < SPAN_LO THEN
Quality := Q_UNDER_RANGE; // below 4 mA: usable, off the calibrated
// span - informational, NOT an alarm
ELSIF Raw > SPAN_HI THEN
Quality := Q_OVER_RANGE; // above 20 mA, same deal
ELSE
Quality := Q_GOOD;
END_IF;
Unfiltered := EngLow + (INT_TO_REAL(Raw) / 27648.0) * (EngHigh - EngLow);
// Never seed the filter from an untrusted first sample.
IF (NOT Started) AND (Quality <> Q_BAD) THEN
PV := Unfiltered;
LastGood := Unfiltered;
Started := TRUE;
END_IF;
// 2. Slew check catches loose ferrules and VFD transients, but must not
// latch: a step that persists is a real process event, so accept it.
IF (MaxSlew > 0.0) AND (Quality = Q_GOOD)
AND (ABS(Unfiltered - LastGood) > (MaxSlew * SampleTime)) THEN
SuspectScans := SuspectScans + 1;
IF SuspectScans < SuspectLimit THEN
Quality := Q_SUSPECT;
ELSE
SuspectScans := 0; // the step was real - re-track
END_IF;
ELSE
SuspectScans := 0;
END_IF;
HoldLastGood := (Quality = Q_BAD) OR (Quality = Q_SUSPECT) OR (NOT Started);
// 3. Filter coefficient derived from SampleTime, so retiming the task does
// not silently retune every filter in the machine.
IF HoldLastGood THEN
PV := LastGood;
ELSE
IF FilterTC > 0.0 THEN
Alpha := SampleTime / (FilterTC + SampleTime);
PV := PV + Alpha * (Unfiltered - PV);
ELSE
PV := Unfiltered;
END_IF;
LastGood := PV;
END_IF;
When the block holds the last good value, it sets HoldLastGood, so the consumer always knows the PV is a held value. Only Q_BAD (and Q_SUSPECT, for the few scans of a fast step) stops the consumer; the two range qualities do not. An under-range reading is the tank being empty, and the instrument is fine.
Filtering without lying to the loop
Do the cheap filtering in hardware first. Siemens analog modules have a parameterisable integration time, typically 2.5 / 16.67 / 20 / 100 ms, which gives interference suppression at 400 / 60 / 50 / 10 Hz. Match it to your mains frequency and most of the hum is gone before a line of code runs.
To a PID loop, filter lag is dead time. Keep the filter time constant well below the process time constant (a tenth or less is a common rule), or you will detune the loop and blame the gains. And never put an averaging filter ahead of a trip. A median of three samples rejects a single-scan transient with one scan of delay, where averaging smears it across the whole window.
Calibration drift and the re-ranged transmitter
A zero shift adds the same error everywhere, while a span error grows with the reading, so always check at 0, 50 and 100 percent.
The drift that bites hardest, though, is not electronic. Somebody re-ranges a transmitter in the field from 0-10 bar to 0-6 bar, nobody edits EngHigh, and the loop still swings a healthy 4-20 mA. NE43 sees nothing and no alarm fires, while the HMI reads 10 bar with the process at 6. Check the PLC value against the device's local display, not against your calibrator.
When a reading is off, trim the transmitter. If you "fix" it in EngLow and EngHigh instead, the local display, the HART value and the HMI stop agreeing, and nobody can tell which one is lying. Check the turndown before you re-range, too. Accuracy is quoted as a percentage of calibrated span only while you stay inside the device's turndown ratio; past it, the specification reverts to a percentage of the upper range limit. Squeezing a 0-100 bar transmitter down to 0-5 bar gives you finer resolution, but nowhere near twenty times the accuracy.