Skip to main content

posts

PLC Programming: Choosing a Low-Pass Filter for Analog Signals

How to choose the coefficient for a simple first-order PLC low-pass filter, relate it to cutoff frequency, and understand the trade-off between noise reduction and delay.

The Problem

Analog PLC signals are rarely clean.

Pressure, temperature, force, and position signals often contain electrical noise, quantization noise, or mechanical vibration. A simple first-order low-pass filter is often enough to make the signal usable.

The hard part is not the filter equation. The hard part is choosing the coefficient without guessing.

This article assumes:

  • the PLC task cycle time is constant
  • the filter runs once per task cycle
  • the input value is already scaled to engineering units
  • the relevant signal content is below the Nyquist frequency
  • the filter is used for signal conditioning, not for safety-critical decisions

A Simple First-Order Low-Pass Filter

A common PLC low-pass filter is:

$$ a = \frac{T_s}{T_f + 0.5T_s} $$$$ y_f[k] = y_f[k-1] + a\left(y[k] - y_f[k-1]\right) $$
SymbolDescription
\(T_s\)PLC cycle time
\(T_f\)Filter time
\(y_f[k-1]\)Filter value last cycle
\(y_f[k]\)Filtered value
\(y[k]\)Input value
\(a\)Filter coefficient

The same equation can also be written as:

$$ y_f[k] = (1-a)y_f[k-1] + ay[k] $$

That form makes the meaning of \(a\) easier to see. The new filtered value is a weighted combination of the previous filtered value and the current input value.

For \(a = 0.01\), the filtered value moves 1% of the remaining distance from the previous filtered value toward the current input value every cycle. It does not simply add 1% of the input. It adds 1% of the current error between input and filtered value.

Useful practical limits are:

  • \(0 < a < 1\): normal smoothing behavior
  • \(a = 1\): no filtering
  • \(1 < a < 2\): mathematically stable, but usually not useful because the response can alternate after a step
  • \(a \le 0\) or \(a \ge 2\): invalid or unstable for this use

In PLC code, I usually clamp or reject coefficients outside \(0 < a \le 1\).

Why Task Cycle Time Matters

The filter depends on the task cycle time. The same coefficient behaves differently in a 1 ms task and a 10 ms task.

The ratio \(T_f / T_s\) determines how many PLC cycles the filter has to react. If the filter time is close to the cycle time, the discrete approximation becomes coarse. If the filter time is much larger than the cycle time, the behavior is easier to predict.

For this reason, \(T_f\) should normally be much larger than \(T_s\). That rule is useful, but it is not very convenient when selecting a filter for a real signal. A cutoff frequency is often easier to reason about.

The cutoff frequency is the frequency where the filter magnitude has dropped by 3 dB. That is about 70.7% of the input amplitude.

The following chart shows the magnitude response for different values of \(T_f\), with \(T_s = 10\,\text{ms}\).

Magnitude response for one low-pass filter stageMagnitude response for one low-pass filter stage

Small coefficients give stronger smoothing and lower cutoff frequencies. They also add more delay. There is no free filter hiding in the control cabinet.

Choosing The Coefficient From Cutoff Frequency

For practical work, it is useful to choose the desired cutoff frequency and calculate the coefficient from it.

The following Python function calculates \(a\) for one or more identical filter stages. The argument stage_count is the number of filters connected in series. For example, use stage_count=2 when two identical first-order filters are chained and the combined response should reach -3 dB at the requested cutoff frequency.

from __future__ import annotations

import numpy as np


def coefficient_from_cutoff(
    cutoff_frequency_hz: np.ndarray | float,
    sample_frequency_hz: float,
    stage_count: int = 1,
) -> np.ndarray | float:
    """Return the low-pass coefficient for a target -3 dB cutoff frequency."""
    if sample_frequency_hz <= 0.0:
        raise ValueError("sample_frequency_hz must be positive.")
    if stage_count <= 0:
        raise ValueError("stage_count must be positive.")

    cutoff = np.asarray(cutoff_frequency_hz, dtype=float)
    if np.any(cutoff < 0.0) or np.any(cutoff > sample_frequency_hz / 2.0):
        raise ValueError("cutoff_frequency_hz must be in the range 0 <= fc <= fs/2.")

    beta = 10.0 ** (-3.0 / (10.0 * stage_count))
    omega_c = 2.0 * np.pi * cutoff / sample_frequency_hz
    distance = 1.0 - np.cos(omega_c)
    radicand = beta**2 * distance**2 + 2.0 * beta * distance * (1.0 - beta)
    coefficient = (-beta * distance + np.sqrt(radicand)) / (1.0 - beta)

    if np.isscalar(cutoff_frequency_hz):
        return float(coefficient)
    return coefficient

Example for a PLC task running at 100 Hz with a desired cutoff frequency of 2 Hz:

coefficient = coefficient_from_cutoff(
    cutoff_frequency_hz=2.0,
    sample_frequency_hz=100.0,
    stage_count=1,
)

The result can be used directly as the coefficient in the PLC filter.

One Stage Versus Two Stages

Two identical first-order filters in series create a steeper roll-off than one filter. This can be useful when high-frequency noise should be reduced more strongly.

The trade-off is more delay. For control loops, that delay can reduce stability margin. For HMI values or slow diagnostics, it is often acceptable.

Magnitude response for two low-pass filter stagesMagnitude response for two low-pass filter stages

When two filters are chained, each stage must be less aggressive if the combined response should have the same -3 dB cutoff frequency. That is why the Python function includes stage_count.

Structured Text Implementation

The filter itself is small. The important details are initialization and coefficient validation.

On startup or reset, initialize the filtered value to the current input. Otherwise the filter may start from zero and create an artificial transient.

FUNCTION_BLOCK AnalogLowPassFilter
VAR
    signalFiltered : LREAL;
    initialized : BOOL;
    coefficientValid : BOOL;
END_VAR
METHOD update
VAR_INPUT
    signalRaw : LREAL;
    coefficient : LREAL;
    reset : BOOL;
END_VAR

coefficientValid := (coefficient > 0.0) AND (coefficient <= 1.0);

IF NOT coefficientValid THEN
    RETURN;
END_IF

IF reset OR NOT initialized THEN
    signalFiltered := signalRaw;
    initialized := TRUE;
    RETURN;
END_IF

signalFiltered := signalFiltered + coefficient * (signalRaw - signalFiltered);
END_METHOD

Expose the filtered value through a property or through the project’s existing signal interface. Expose coefficientValid as diagnostics if the coefficient comes from a recipe or HMI. Avoid writing the filter state directly from outside the function block.

PROPERTY SignalFiltered : LREAL

Getter implementation:

SignalFiltered := signalFiltered;
PROPERTY CoefficientValid : BOOL

Getter implementation:

CoefficientValid := coefficientValid;

For a two-stage filter, instantiate two filters and call them in order. The output of the first stage becomes the input of the second stage. Both stages should use the coefficient calculated with stage_count=2.

Limitations

A low-pass filter is not a repair tool for a bad signal chain.

It can reduce noise, but it cannot fix:

  • bad shielding
  • poor grounding
  • loose terminals
  • incorrect analog input configuration
  • aliasing from high-frequency noise
  • broken or drifting sensors

Filtering also adds delay. That matters in closed-loop control. A pressure value shown on an HMI can tolerate more delay than a feedback signal used by a fast controller.

Invalid sensor states should be handled explicitly. If the analog terminal reports an error, the filter should usually hold, reset, or mark its output invalid. Silently filtering invalid data is just hiding a fault with extra math.

Conclusion

A first-order low-pass filter is useful for analog PLC signals because it is cheap, deterministic, and easy to implement.

The coefficient should not be guessed. Choose it from the task cycle time and the desired cutoff frequency. Use one stage for simple smoothing. Use two stages only when the stronger attenuation is worth the added delay.

As usual in PLC work, the formula is the easy part. The engineering decision is deciding how much delay the machine can tolerate.