Quadcopter PID Integral Windup

Hello,

I am attempting to write my own flight controller for a multirotor (along with a million other people). I am using the MPU6050 as my IMU along with other sensors. My quadcopter flies with an angular system. The pilot’s input is converted to deg/s values and I convert the gyroscope measurements from my IMU to deg/s as well. I throw the pilot’s values (setpoint) and the gyroscope’s measurements (input) into the PID (not using D for now) loop for calculations. The pilot’s maximum input rate is around ±180°/s for roll/ pitch and around ±270°/s for the yaw axis.

I am close to completing my PID loop but after some minor testing, I ran into the issue of integral windup. I did some research and most people recommend determining a maximum iTerm output and never allow the iTerm to pass that output. I am not sure if this is a good enough solution though.

One of the recourses I used: Link

This is my PID Loop:

void PID(float& ki, float& kp, float& kd)
{
  /*CURRENTLY NOT USING THE DERIVATIVE IN THE PID LOOP, JUST PI FOR NOW*/
  static unsigned long now, last_time, time_change;
  static float pError_yaw, iError_yaw, dError_yaw, previouspError_yaw;
  static float pError_pitch, iError_pitch, dError_pitch, previouspError_pitch;
  static float pError_roll, iError_roll, dError_roll, previouspError_roll;
  static float pid_i_yaw, pid_i_pitch, pid_i_roll; //store integral calculations to monitor windup
  static const int pid_max_yaw = 540, pid_max_pitch = 360, pid_max_roll = 360; //maximum output = degrees/s maximum range
  static int previous_pid_output_yaw, previous_pid_output_pitch, previous_pid_output_roll;

  if (reset_pid)
  {
    last_time = 0;
    previouspError_yaw = 0;
    previouspError_pitch = 0;
    previouspError_roll = 0;
    previous_pid_output_yaw = 0;
    previous_pid_output_pitch = 0;
    previous_pid_output_roll = 0;
    reset_pid = false;
  }

  now = millis(); //How long has it been since last calculation
  time_change = now - last_time;

  //YAW
  pError_yaw = pid_input_yaw - pid_setpoint_yaw; //Looks at the input, compare it to where you want it to be
  iError_yaw += pError_yaw; // Looks at the pError qnd compares how long it has been that way
  pid_i_yaw = ki * iError_yaw;
  if (pid_i_yaw > pid_max_yaw) pid_i_yaw = pid_max_yaw; //prevent integral windup by limiting the integral to the max yaw
  else if (pid_i_yaw < pid_max_yaw * -1) pid_i_yaw = pid_max_yaw * -1;
  pid_output_yaw = (kp * pError_yaw) + pid_i_yaw; //Output calculation
  if (pid_output_yaw != previous_pid_output_yaw)
  {
    Serial.println("\n---------------YAW---------------");
    Serial.print("pError: "); Serial.println(pError_yaw);
    if (pid_i_yaw == pid_max_yaw) {
      Serial.print(">> iTerm Max: ");
      Serial.println(pid_i_yaw);
    }
    else if (pid_i_yaw == pid_max_yaw * -1) {
      Serial.print("<< iTerm Min: ");
      Serial.println(pid_i_yaw );
    }
    else  {
      Serial.print("iError: ");
      Serial.println(iError_yaw);
    }
    Serial.print("\nInput: "); Serial.println(pid_input_yaw);
    Serial.print("Setpoint: "); Serial.println(pid_setpoint_yaw);
    Serial.print("Output: "); Serial.println(pid_output_yaw);
  }

  //PITCH
  pError_pitch = pid_input_pitch - pid_setpoint_pitch; //Looks at the input, compare it to where you want it to be
  iError_pitch += pError_pitch; // Looks at the pError qnd compares how long it has been that way
  pid_i_pitch = kp * iError_pitch;
  if (pid_i_pitch > pid_max_pitch) pid_i_pitch = pid_max_pitch; //prevent integral windup by limiting the integral to the max pitch
  else if (pid_i_pitch < pid_max_pitch * -1) pid_i_pitch = pid_max_pitch * -1;
  pid_output_pitch = (kp * pError_pitch) + pid_i_pitch ; //Output calculation
  if (pid_output_pitch != previous_pid_output_pitch)
  {
    Serial.println("\n---------------PITCH---------------");
    Serial.print("pError: "); Serial.println(pError_pitch);
    if (pid_i_pitch == pid_max_pitch) {
      Serial.print(">> iTerm Max: ");
      Serial.println(pid_i_pitch);
    }
    else if (pid_i_pitch == pid_max_pitch * -1) {
      Serial.print("<< iTerm Min: ");
      Serial.println(pid_i_pitch);
    }
    else {
      Serial.print("iError: ");
      Serial.println(iError_pitch);
    }
    Serial.print("\nInput: "); Serial.println(pid_input_pitch);
    Serial.print("Setpoint: "); Serial.println(pid_setpoint_pitch);
    Serial.print("Output: "); Serial.println(pid_output_pitch);
  }

  //ROLL
  pError_roll = pid_input_roll - pid_setpoint_roll; //Looks at the input, compare it to where you want it to be
  iError_roll += pError_roll; // Looks at the pError qnd compares how long it has been that way
  pid_i_roll = ki * iError_roll;
  if (pid_i_roll > pid_max_roll) pid_i_roll = pid_max_roll; //prevent integral windup by limiting the integral to the maximum roll
  else if (pid_i_roll < pid_max_roll * -1) pid_i_roll = pid_max_roll * -1;
  pid_output_roll = (kp * pError_roll) + pid_i_roll; //Output calculation
  if (pid_output_roll != previous_pid_output_roll)
  {
    Serial.println("\n---------------ROLL---------------");
    Serial.print("pError: "); Serial.println(pError_roll);
    if (pid_i_roll == pid_max_roll) {
      Serial.print(">> iTerm Max: ");
      Serial.println(pid_i_roll);
    }
    else if (pid_i_roll == pid_max_roll * -1) {
      Serial.print("<< iTerm Min: ");
      Serial.println(pid_i_roll);
    }
    else {
      Serial.print("iError: ");
      Serial.println(iError_roll);
    }
    Serial.print("\nInput: "); Serial.println(pid_input_roll);
    Serial.print("Setpoint: "); Serial.println(pid_setpoint_roll);
    Serial.print("Output: "); Serial.println(pid_output_roll);
  }

  //Remember previous values
  last_time = now;
  previouspError_yaw = pError_yaw;
  previouspError_pitch = pError_pitch;
  previouspError_roll = pError_roll;
  previous_pid_output_yaw = pid_output_yaw;
  previous_pid_output_pitch = pid_output_pitch;
  previous_pid_output_roll = pid_output_roll;
}

This is an example portion of code I added to limit integral windup:

 static const int pid_max_yaw = 540, pid_max_pitch = 360, pid_max_roll = 360; //maximum output = degrees/s maximum range
pid_i_yaw = ki * iError_yaw;
  if (pid_i_yaw > pid_max_yaw) pid_i_yaw = pid_max_yaw; //prevent integral windup by limiting the integral to the max yaw
  else if (pid_i_yaw < pid_max_yaw * -1) pid_i_yaw = pid_max_yaw * -1;

My question is: Is this the most efficient way to prevent integral windup? Is there a better solution? Any suggestions would be appreciated.

(I wasn’t sure if sample data was needed for general advice so I left it off. If anybody needs some, please say so.)

From Wikipedia's page on integral windup:

  • Increasing the setpoint in a suitable ramp
  • Disabling the integral function until the to-be-controlled process variable (PV) has entered the controllable region
  • Preventing the integral term from accumulating above or below pre-determined bounds

Too many problems with that code to know where to begin...

First, you're using the same Kp and Ki parameters for all three axis - that 'aint gonna fly.

Overall, you should have left the PID loop code as-was and then called it three times with the different tuning values required for each loop.

You really need to start over again and structure the code properly with one PID subroutine rather than having duplicating the code, adding the _yaw, _pitch and _roll suffixes on the end of the variables.