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.
Why Analog Scaling Matters
PLCs read analog inputs as raw integer values — typically 0 to 27648 (Siemens), 0 to 32767, or 0 to 4095 depending on the platform. These numbers mean nothing to operators. You need to convert them to engineering units like °C, PSI, gallons per minute, or percent.
The Linear Scaling Formula
The core formula for linear interpolation:
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_VARTemperature := ((INT_TO_REAL(RawInput) - RawMin) / (RawMax - RawMin))
* (EngMax - EngMin) + EngMin;
Reusable Scaling Function Block
A professional approach — create a reusable FB:
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_VARRawReal := 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_VARTempScale(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;
Handling 4-20mA Wire Break Detection
With 4-20mA signals, a reading below 4mA (raw ~0) indicates a wire break:
PROGRAM WireBreakDetection
VAR
RawInput : INT;
WireBreak : BOOL := FALSE;
SensorOK : BOOL := FALSE;
LiveZeroThreshold : INT := 1382; // ~5% of 27648 ≈ 1mA
END_VARWireBreak := RawInput < LiveZeroThreshold;
SensorOK := NOT WireBreak AND (RawInput <= 27648);
Analog Output Scaling (Reverse)
For analog outputs, reverse the formula — convert engineering units to raw:
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
Build a multi-sensor scaling program in our free PLC simulator. Our function block templates include a ready-to-use analog scaling FB.
Choosing 4-20 mA or 0-10 V: Decide by Where the Cable Runs
A voltage input is high-impedance, referenced to the module's analog common: fine inside the panel, 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 no software filter cleanly removes. A current loop shrugs off most of that, because the current is identical at every point in a series circuit: 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. It is not magic, though — a loop earthed at two points is still a ground loop, which is what loop isolators exist for.
What you accept in exchange is 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. Run out of compliance and the loop reads perfectly at 4 mA but clips somewhere short of full scale, because burden voltage rises with current — so the process appears to plateau below its real maximum and everyone blames the sensor.
What live zero buys beyond wire break
The 4 mA is not only a diagnostic floor, it is 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.
Hence the configuration trap: on a Siemens channel parameterised 0-20 mA instead of 4-20 mA, 4 mA lands at 5530 counts, not 0. Scaling still produces plausible engineering units, and a severed wire reads 0 counts, which that scaling reports as a clean bottom of range. Nothing alarms. Confirm the channel range in hardware config matches what your scaling assumes.
What the Count Table Leaves Out
Nominal 0-27648 is only the middle band. Siemens modules keep reporting outside it: roughly 27649-32511 is overrange, 32767 overflow, negative counts to about -4864 cover the underrange region below 4 mA, and -32768 is underflow. Clamp to 0-27648 on your first line of code and you have discarded the diagnosis before looking at it. Negative counts are normal at the bottom of span, so a junior writing IF Raw < 0 THEN Fault nuisance-trips every time the tank genuinely empties.
On Rockwell hardware the count range is a configuration choice, not a platform constant: a 1756-IF8 in its floating-point data format hands the controller a REAL and scales inside the module, so rescaling in logic leaves two sources of truth. Configure the same module for integer data instead and you lose the on-board scaling and are back to raw counts — which is fine, as long as only one of the two places is doing the conversion.
Reading NE43 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: the reading still tracks the process, it has just 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 a compliant transmitter is not supposed to park in, and NE 43 tells the control system to read a stray value there as measurement rather than as a fault. On a Siemens channel, 27648 counts span 16 mA: 1728 counts per mA.
TYPE E_SigQuality : (Q_GOOD, Q_UNDER_RANGE, Q_OVER_RANGE, Q_BAD, Q_SUSPECT); END_TYPEFUNCTION_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;
Note what the block refuses to do: it never quietly substitutes a value. When it holds, it says so. And note the split between Q_BAD and the two range qualities — only the first stops the consumer. Under-range is the tank being empty, not the instrument being broken.
Filtering Without Lying to the Loop
Do the cheap filtering in hardware first. Siemens analog modules expose a parameterisable integration time, typically 2.5 / 16.67 / 20 / 100 ms, giving interference suppression at 400 / 60 / 50 / 10 Hz. Match it to your mains frequency and most of the hum disappears before a line of code runs.
Filter lag is dead time to a PID loop, so keep the filter time constant well below the process time constant — a tenth or less is a common rule — or you will detune a loop and blame the gains. And never 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; a span error grows with the reading, so always check 0, 50 and 100 percent.
The drift that actually bites, though, is not electronic. Somebody re-ranges a transmitter in the field, 0-10 bar becomes 0-6 bar, nobody edits EngHigh, and the loop still swings a healthy 4-20 mA. NE43 sees nothing, no alarm fires, and the HMI reads 10 bar while the process sits at 6. Check the PLC value against the device's local display, not your calibrator.
When a reading is off, trim the transmitter — do not "fix" it in EngLow and EngHigh, or the local display, the HART value and the HMI stop agreeing and nobody can tell which one is lying. And check the turndown before you re-range: accuracy is quoted as a percentage of calibrated span only while you stay inside the device's turndown ratio, and 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.