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_VAR

Temperature := ((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_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;

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_VAR

WireBreak := 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

Platform4-20mA Range0-10V RangeResolution
Siemens S70–276480–2764816-bit
Allen-Bradley3277–163840–3276715-bit
CODESYSConfigurableConfigurableModule-dependent
Beckhoff0–327670–3276716-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_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;

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.