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

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