Skip to main content

posts

Normalize PLC Values at the System Boundary

Why normalizing analog values to a common range makes TwinCAT PLC code simpler, safer, and easier to test.

In PLC programs, raw analog values often travel much farther than they should.

An analog input may deliver a value from 0 to 1023. The value is then passed into a PID controller, an HMI bar graph, a valve command, or a drive command. Before long, the number 1023 appears everywhere.

valuePid := LIMIT(0.0, valuePid, 1023.0);
valueBarGraph := 100.0 * valuePid / 1023.0;

The problem is not the calculation itself. The problem is the convention hidden inside it.

The code assumes that 1023 is the maximum value of the signal. If that assumption is used in one place, it is manageable. If it is spread over ten thousand lines, the range of one input terminal has become part of the architecture. An impressive achievement, if the goal was to make a hardware detail everybody’s problem.

The better approach is to normalize the value once, close to the boundary of the system.

The Boundary Should Know the Raw Range

Normalization means converting one range into another common range.

For control signals, a range from 0.0 to 1.0 is often practical. It can be interpreted as a relative command, or as a percentage divided by 100.

The raw ADC range stays near the input handling code. The rest of the application receives a normalized value.

Instead of this:

valueBarGraph := 100.0 * valuePid / 1023.0;

the application can work with this:

valueBarGraph := 100.0 * valueNormalized;

The important change is not saving one division. The important change is that the rest of the code no longer needs to know whether the input came from a 10-bit ADC, a 12-bit ADC, an EtherCAT terminal, a simulation value, or a fieldbus device with a vendor-specific range.

A Linear Transform

The general transform from one range to another is a linear equation.

output = (outputMax - outputMin) / (inputMax - inputMin) * (input - inputMin) + outputMin

In Structured Text, this can be implemented as a small function.

FUNCTION transformLinear : LREAL
VAR_INPUT
    valueInput : LREAL;
    valueInputMin : LREAL;
    valueInputMax : LREAL;
    valueOutputMin : LREAL;
    valueOutputMax : LREAL;
    limitOutput : BOOL;
END_VAR

IF ABS(valueInputMax - valueInputMin) > 1E-9 THEN
    transformLinear :=
        (valueOutputMax - valueOutputMin)
        / (valueInputMax - valueInputMin)
        * (valueInput - valueInputMin)
        + valueOutputMin;

    IF limitOutput THEN
        IF valueOutputMax >= valueOutputMin THEN
            transformLinear := LIMIT(
                valueOutputMin,
                transformLinear,
                valueOutputMax);
        ELSE
            transformLinear := LIMIT(
                valueOutputMax,
                transformLinear,
                valueOutputMin);
        END_IF
    END_IF
ELSE
    transformLinear := valueOutputMin;
END_IF

The function also handles reversed output ranges and avoids division by zero. Returning the minimum output value for an invalid input range is a design choice. In production code, the caller may also need an error state or diagnostic flag. Quietly returning a number is not always enough.

A Potentiometer Example

Assume a potentiometer is connected to an analog input. The raw value is converted into a relative value from 0.0 to 1.0.

FUNCTION_BLOCK Potentiometer
VAR
    valueRelative : LREAL;
END_VAR
VAR CONSTANT
    VALUE_RAW_MIN : LREAL := 0.0;
    VALUE_RAW_MAX : LREAL := 1023.0;
END_VAR

The update method performs the conversion.

METHOD update
VAR_INPUT
    valueRaw : DINT;
END_VAR

valueRelative := transformLinear(
    valueInput := TO_LREAL(valueRaw),
    valueInputMin := VALUE_RAW_MIN,
    valueInputMax := VALUE_RAW_MAX,
    valueOutputMin := 0.0,
    valueOutputMax := 1.0,
    limitOutput := TRUE);

The public property returns the normalized value.

PROPERTY Value : LREAL

Getter:

Value := valueRelative;

From this point on, the rest of the application does not use 1023. It uses potentiometer.Value.

That value has a simple contract:

0.0 = minimum
1.0 = maximum

Use the Normalized Contract

A variable frequency drive can expose an operation method with relative values.

INTERFACE IDrive

METHOD executeOperation : BOOL
VAR_INPUT
    speedRelative : LREAL;
    torqueRelative : LREAL;
END_VAR

Both inputs use the same convention:

0.0 = no command
1.0 = maximum configured command

Now the potentiometer can be connected to different meanings without changing its own implementation.

CASE choice OF

    1:
        // Fixed speed, variable torque.
        drive.executeOperation(
            speedRelative := 0.1,
            torqueRelative := potentiometer.Value);

    2:
        // Variable speed, full torque.
        drive.executeOperation(
            speedRelative := potentiometer.Value,
            torqueRelative := 1.0);

ELSE
    drive.executeOperation(
        speedRelative := 0.0,
        torqueRelative := 0.0);

END_CASE

This works because both sides agree on the normalized range. The potentiometer does not need to know the maximum speed of the drive. The drive does not need to know the ADC range of the potentiometer.

The conversion back to an engineering value happens inside the drive or close to the drive.

speedSetpoint := speedRelative * speedMax;
torqueSetpoint := torqueRelative * torqueMax;

The raw range and the physical range stay at the boundaries. The application logic works with the relative command.

Where Normalization Helps

Normalized values are useful when a signal represents a relative command or relative feedback.

Typical examples are analog inputs, valve positions, drive speed commands, torque limits, PID outputs, HMI sliders, and simulation inputs.

PID controllers are a good example. If the output is normalized to 0.0..1.0, it can be connected directly to a relative valve position, torque command, or speed command. The controller does not need to know whether the final actuator is a proportional valve, a VFD, or a simulated load.

The same idea also helps testing. A test can set a fake potentiometer to 0.25 without knowing the raw ADC value. The test checks behavior against the application contract, not against one hardware terminal.

Limits

Normalization is not a reason to delete engineering units from the program.

Pressure, temperature, length, speed, and force should often be represented in engineering units inside the application. A pressure controller should usually reason about bar or pascals, not only about 0.0..1.0.

Normalized values are best for relative commands and generic interfaces. Engineering values are better when the physical meaning matters.

The rule is simple:

Normalize hardware-specific ranges at the boundary. Keep physical values in engineering units when the physics matters.

Also document the convention. If speedRelative = 1.0 means speedMax, then speedMax must be visible and well defined. If a signal can exceed the normal range, decide whether to clamp it, report it, or use it for diagnostics.

Conclusion

Normalization prevents hardware ranges from leaking through the whole PLC project.

The ADC limit belongs near the analog input. The maximum speed belongs near the drive. The application logic between them should depend on a clear contract, not on scattered constants.

For many PLC systems, 0.0..1.0 is a useful range for relative signals. It makes interfaces simpler, tests easier, and code less dependent on one piece of hardware.

The practical rule is boring, which is usually a good sign:

Convert once at the boundary. Use the normalized value in the application. Convert back only where the physical output requires it.