anti-windup-y-saturacion-actuadores

Anti-windup and actuator saturation

  • 4 min

The saturation of an actuator is the physical limit that prevents applying more command even though the PID demands it, such as the maximum voltage of a battery or the limit angle of a servo.

In the previous article, we implemented a “perfect” digital PID. Mathematically it was flawless, but it had an original sin: it assumed our hardware has infinite resources. On paper, if our PID calculates that we need 5000V to correct an error, the formula gives us 5000V. But in the real world, if your power supply is 12V, you have 12V and that’s it.

Today we will see why this physical limitation can spoil the response and how we can correct it with anti-windup techniques.

The physical limit: saturation

All real-world actuators have limits.

  • A PWM output has a maximum value, such as 255 when using an 8-bit resolution.
  • A motor driver saturates at its VCC.
  • A hydraulic valve saturates when it is 100% open.

When the error is very large (for example, when starting the system), the PID will calculate a huge output. We, in our code, will apply saturation (a constrain or clamp) to avoid overflowing the registers:

float calculated_output = P + I + D;
float final_output = constrain(calculated_output, 0, 255); 
Copied!

So far, there seems to be no problem. The motor will run at full speed and that’s it. However, the Integral term remains alive inside the microcontroller’s memory.

The integral windup phenomenon

Imagine we have a mechanically locked motor. The PID detects that the position error is large and starts acting:

  1. The P term goes to the maximum. The output saturates at 255.
  2. Since the motor does not move, the error persists.
  3. The I term starts adding that error cycle after cycle.
  4. Seconds pass. The output remains saturated at 255, but internally, the integral_accumulated variable has grown to be worth millions.

Now, suddenly, we unlock the motor. The motor accelerates and reaches the Setpoint. The error becomes zero. In an ideal world, the motor should brake. But, surprise!, the motor continues at full power.

Why? Because for the output to drop from 255, the error must first become negative for a long time in order to “drain” those millions accumulated in the integral. The result is a gigantic overshoot and a system that seems to have lost common sense.

Windup occurs because the control loop is “broken” when the actuator saturates; the controller believes it is acting, but the plant no longer responds to more power.

Anti-windup algorithms

To avoid this problem, we must prevent the integral from continuing to grow in the direction of saturation. There are several techniques; one of the simplest is conditional clamping.

The rule is simple: Stop integrating if these two conditions are met simultaneously:

  1. The output is already saturated (at maximum or minimum).
  2. The error has the same sign as the output (that is, the integral wants to continue growing in the direction of saturation).

Implementing clamping in C++

Let’s update our code from the previous article to make it resistant to saturation. We will introduce the actuator limits and the logic for stopping the integral.

// Define the hardware limits
const float LIMIT_MAX = 255.0;
const float LIMIT_MIN = 0.0;

float Kp = 2.0, Ki = 5.0, Kd = 1.0;
float integral_accumulated = 0.0;
float previous_error = 0.0;

float calculatePID(float setpoint, float pv, float dt) {
    float error = setpoint - pv;
    
    // 1. Proportional Action
    float P = Kp * error;
    
    // 2. Derivative Action
    float D = Kd * (error - previous_error) / dt;
    
    // 3. Integral action with anti-windup (conditional clamping)
    float current_output = P + D + (Ki * integral_accumulated);
    bool saturates_up = current_output >= LIMIT_MAX;
    bool saturates_down = current_output <= LIMIT_MIN;

    // Integrate in the linear zone or if the error helps exit the limit
    bool allows_integrate = (!saturates_up && !saturates_down)
                          || (saturates_up && error < 0.0)
                          || (saturates_down && error > 0.0);

    if (allows_integrate) {
        integral_accumulated += error * dt;
    }

    // 4. Recalculate and apply physical saturation
    float total_output = P + D + (Ki * integral_accumulated);
    float saturated_output = constrain(total_output, LIMIT_MIN, LIMIT_MAX);
    
    previous_error = error;
    return saturated_output;
}
Copied!

Conditional clamping is simple and consumes few resources. Other techniques, such as back-calculation or output tracking, can offer smoother recovery in specific plants and controllers.