filtrado-de-senales-ruido-derivada

Signal filtering and noise in the PID derivative

  • 6 min

Digital signal filtering is the process of attenuating noise from a sampled signal to preserve the useful information that our control algorithm needs.

In the previous articles we built a digital PID and dealt with saturation using anti-windup. When we take it to a real robot, we may still find motors emitting a sharp whine, vibrating, or overheating more than expected.

The culprit? Electrical and mechanical noise. The victim? The Derivative (D) action of our PID.

Today, in the last article of this mini-series, we will understand why this happens by connecting it to what we learned in Bode plots, and we will program a very common solution: the digital low-pass filter.

Sensor noise

In the physical world, sensors do not draw perfect lines.

  • An MPU6050 accelerometer picks up micro-vibrations from the chassis.
  • An analog-to-digital converter (ADC) reading a potentiometer suffers from electrical jitter.
  • An optical encoder has quantization error due to its discrete “steps”.

If we plot this, we will see a useful signal with small variations superimposed. A significant part of the noise that bothers the derivative has high-frequency content, although there are also slow errors, biases, and drifts that a low-pass filter does not correct.

Why the derivative amplifies noise

To understand why this harmless “fuzz” wrecks a €10,000 industrial robot, we need to look at the math.

If you remember the article on the Transfer Function, taking a derivative in the Laplace domain is equivalent to multiplying by s. In the Bode plots article, we saw that frequency is represented as s=jω.

Therefore, the gain of a pure differentiator is directly proportional to the frequency ω. The faster the signal, the larger the derivative. A differentiator is an infinite amplifier of high frequencies (in Bode, its plot rises forever at +20 dB/decade).

The mathematical disaster

Imagine we have a tiny noise in our sensor, modeled as a sine wave with an amplitude of only 0.01 units, but vibrating at 1000 rad/s:

When our derivative term tries to calculate the slope of this noise, mathematically it does this:

The output amplitude has been multiplied by a thousand! An imperceptible noise in the position has just turned into gigantic command spikes in our motor signal. This is what causes the famous “chattering” and heats up drivers until they burn out.

Interactive simulator

To experience this firsthand, try the following simulator.

Leave the filter strength at minimum and increase the noise amplitude a bit. You will see that the sensor signal barely seems to change, but the derivative plot becomes a solid block of chaotic noise.

Then increase the filter strength gradually. The derivative regains its useful shape, although some delay appears in return.

Exponential Moving Average (EMA) filter

We need to block high frequencies (noise) before calculating the derivative. We could store the last 10 readings in an array and calculate the average, but that uses a lot of RAM and CPU cycles.

A very lightweight alternative is the Exponential Moving Average (EMA) filter. It is a first-order digital low-pass filter (IIR) that requires only one memory variable.

The formula is very simple:

Where:

  • x[k] is the current (raw) sensor reading.
  • y[k] is the filtered value we will use in this cycle.
  • y[k−1] is the filtered value from the previous cycle.
  • α (Alpha) is the smoothing factor, a number between 0 and 1.

How to choose the Alpha (α) value:

  • If α=1, the filter does nothing. The output is exactly the current reading.
  • If α=0.1, you trust 10% in the new reading and 90% in past history. The signal will be very smooth, but it will have delay.
  • If α=0.01, the filter is very strong. It will remove all noise, but your system will react so late that the PID could become unstable (remember that delay is the enemy of phase margin in Bode).

The effect of α depends on the sampling period. If we want to maintain a filter time constant τ_f when changing T_s, we can calculate α = T_s / (τ_f + T_s) as a common approximation.

Derivative filter implementation in C++

There are two main strategies: filter the entire process variable (PV) or filter only the signal used by the derivative term. Filtering only the derivative preserves a faster proportional response, although the decision depends on the noise and the rest of the system.

This way, the proportional action still reacts to the measurement without that additional filter, while the derivative attenuates fast spikes.

Let’s extend the previous code by combining the PID, conditional clamping, and the derivative filter. This is still a didactic example: a production implementation also needs to validate parameters, handle sensor failures, and define a safe error strategy.

class PIDController {
private:
    float Kp, Ki, Kd;
    float integral_accumulator;
    float pv_previous;
    float filtered_derivative;
    float previous_output;
    bool initialized;

    // Physical parameters
    float max_limit, min_limit;
    float filter_alpha; // Smoothing factor (0.0 to 1.0)

public:
    PIDController(float p, float i, float d, float min, float max, float alpha) {
        Kp = p; Ki = i; Kd = d;
        min_limit = min; max_limit = max;
        filter_alpha = alpha;

        integral_accumulator = 0;
        pv_previous = 0;
        filtered_derivative = 0;
        previous_output = 0;
        initialized = false;
    }

    float calculate(float setpoint, float pv, float dt) {
        if (dt <= 0.0) {
            return previous_output;
        }

        float error = setpoint - pv;

        // --- Proportional Action ---
        float P = Kp * error;

        // --- Derivative on the measurement with EMA filter ---
        // Avoids derivative kick when the setpoint changes
        float raw_derivative = 0.0;
        if (initialized) {
            raw_derivative = -(pv - pv_previous) / dt;
        }

        filtered_derivative = (filter_alpha * raw_derivative)
                            + ((1.0 - filter_alpha) * filtered_derivative);

        float D = Kd * filtered_derivative;

        // --- Integral with anti-windup (conditional clamping) ---
        float current_output = P + D + (Ki * integral_accumulator);
        bool saturates_up = current_output >= max_limit;
        bool saturates_down = current_output <= min_limit;

        bool allows_integration = (!saturates_up && !saturates_down)
                                || (saturates_up && error < 0.0)
                                || (saturates_down && error > 0.0);

        if (allows_integration) {
            integral_accumulator += error * dt;
        }

        float total_output = P + D + (Ki * integral_accumulator);
        float final_output = constrain(total_output, min_limit, max_limit);

        // --- Update memories for the next cycle ---
        pv_previous = pv;
        previous_output = final_output;
        initialized = true;

        return final_output;
    }
};
Copied!