Arduino Quadcopter PID Tuning Help

I am writing my own flight controller code for my quadcopter on an Arduino Zero. I attempted my maiden flight yesterday, it went about 2 feet up in the air and began oscillating uncontrollably/rapidly and crashed a few seconds later.

I am using a transmitter, an MPU6050 (gyroscope only), converting those values into degrees per second and feeding all of that information into a PID loop. I have held the quadcopter in my hands, turned on the throttle to abut 40% and move it around to see how it would respond and it fought my movement. Obviously, it wasn't perfectly smooth because it wasn't tuned.

I have a PID loop for each axis (Yaw, Pitch, Roll) and different PID gain values for Pitch/Roll and Yaw. For my maiden flight, I read online that a PID controller is tuned with P first, then I and D last. I read to set my P Gain values for my Pitch/Roll low, slowly increase it until it begins to oscillate and cut that value in half. I never got to that step because the quadcopter wasn't stable at all and I couldn't control it.

Extra Information: I am using a 11.1V 2.2A 3S Lipo along with 4 30A ESC's from HobbyKing. I did notice that my top left ESC was getting hot after some testing and none of my other ESC's were hot. I'm not sure if that's a major issue, I used my multimeter to check and all 4 ESC's were receiving the required voltage.

This is my PID loop:

void PID()
{
  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 = 270, pid_max_pitch = 180, pid_max_roll = 180; //maximum output = degrees/s maximum range
  static int previous_pid_output_yaw, previous_pid_output_pitch, previous_pid_output_roll;

  if (reset_pid)
  {
    pError_yaw = 0; pError_pitch = 0; pError_roll = 0;
    iError_yaw = 0; iError_pitch = 0; iError_roll = 0;
    dError_yaw = 0; dError_pitch = 0; dError_roll = 0;
    pid_i_yaw = 0; pid_i_pitch = 0; pid_i_roll = 0;
    pid_output_yaw = 0;
    pid_output_pitch = 0;
    pid_output_roll = 0;
    reset_pid = false;
  }

  //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 = pid_i_gain_yaw * iError_yaw; //Integral Calculations
  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;
  dError_yaw = (pError_yaw - previouspError_yaw);

  pid_output_yaw = (pid_p_gain_yaw * pError_yaw) + pid_i_yaw + (pid_d_gain_yaw * dError_yaw); //Output calculation
  if (pid_output_yaw > pid_max_yaw) pid_output_yaw = pid_max_yaw;
  else if (pid_output_yaw < pid_max_yaw * -1) pid_output_yaw = pid_max_yaw * -1;

  //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 = pid_i_gain_pitch * iError_pitch; //integral calculations
  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;
  dError_pitch = (pError_pitch - previouspError_pitch);

  pid_output_pitch = (pid_p_gain_pitch * pError_pitch) + pid_i_pitch + (pid_d_gain_pitch * dError_pitch) ; //Output calculation
  if (pid_output_pitch > pid_max_pitch) pid_output_pitch = pid_max_pitch;
  else if (pid_output_pitch < pid_max_pitch * -1) pid_output_pitch = pid_max_pitch * -1;

  //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 = pid_i_gain_roll * 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;
  dError_roll = (pError_roll - previouspError_roll);

  pid_output_roll = (pid_p_gain_roll * pError_roll) + pid_i_roll + (pid_d_gain_roll * dError_roll); //Output calculation
  if (pid_output_roll > pid_max_roll) pid_output_roll = pid_max_roll;
  else if (pid_output_roll < pid_max_roll * -1) pid_output_roll = pid_max_roll * -1;


  //Remember previous values
  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 my motor mixing loop:

void motor_output() //output the correct values to the ESC's
{
  static const int throttle_max = 1900 * .9, throttle_min = 1200; //throttle max = 90% of 1900, throttle mix

  /*Motor Orientation
      1      2
       \    /
       /    \
      4      3
  */

  if (motors_armed)
  {
    if (throttle < throttle_min) throttle = throttle_min; //if the throttle is less than the minimum, set it to the minimum

    //if any motor output is greater the preset max throttle, reduce the throttle by difference
    if (motor1output > throttle_max) throttle -= (motor1output - throttle);
    if (motor2output > throttle_max) throttle -= (motor2output - throttle);
    if (motor3output > throttle_max) throttle -= (motor3output - throttle);
    if (motor4output > throttle_max) throttle -= (motor4output - throttle);

    //calculate the values to be output to the motors(ESC's)
    motor1output = throttle - pid_output_pitch - pid_output_roll + pid_output_yaw; //front left
    motor2output = throttle - pid_output_pitch + pid_output_roll - pid_output_yaw; //front right
    motor3output = throttle + pid_output_pitch + pid_output_roll + pid_output_yaw; //bottom right
    motor4output = throttle + pid_output_pitch - pid_output_roll - pid_output_yaw; //bottom left

    //limit the outputs between 1200 - 1900 us (1200 is the minimum to keep the motors running while armed)
    if (motor1output < 1200) motor1output = 1200;
    if (motor2output < 1200) motor2output = 1200;
    if (motor3output < 1200) motor3output = 1200;
    if (motor4output < 1200) motor4output = 1200;

    if (motor1output > 1900) motor1output = 1900;
    if (motor2output > 1900) motor2output = 1900;
    if (motor3output > 1900) motor3output = 1900;
    if (motor4output > 1900) motor4output = 1900;

    if (throttle >= throttle_min && throttle <= throttle_max) //if the throttle if within the range, send values to the ESC's
    {
      esc1.writeMicroseconds(motor1output); //Motor 1 (top left, CW)
      esc2.writeMicroseconds(motor2output); //Motor 2 (top right, CCW)
      esc3.writeMicroseconds(motor3output); //Motor 3 (bottom right, CW)
      esc4.writeMicroseconds(motor4output); //Motor 4 (bottom left, CCW)
    }
  }
  else   //if the motors aren't armed, keep the motor outputs at 1000us (off)
  {
    esc1.writeMicroseconds(1000); //Motor 1 (top left, CW)
    esc2.writeMicroseconds(1000); //Motor 2 (top right, CCW)
    esc3.writeMicroseconds(1000); //Motor 3 (bottom right, CW)
    esc4.writeMicroseconds(1000); //Motor 4 (bottom left, CCW)
  }
}

I am not sure how to tune my quad if it's uncontrollably in the air. Maybe my center of gravity is off? Maybe my PID loop has some type of error? Maybe my motor mixing is wrong? I'm not sure what to do. Any advice to proceed would be greatly appreciated.

(I attached my source code if anyone has other concerns about my setup)

PID_TUNING_ZERO_V5.ino (25.3 KB)

sukalo98:
I am writing my own flight controller code for my quadcopter on an Arduino Zero. I attempted my maiden flight yesterday, it went about 2 feet up in the air and began oscillating uncontrollably/rapidly and crashed a few seconds later.

This means that your D value is too low and isn't dampening out the oscillations like it should. Increase D, try another flight, and repeat until you get a stable response.

sukalo98:
I am using a transmitter, an MPU6050 (gyroscope only)

Why in the world would you not also use the accelerometer data? A complimentary filter (see this link for more info) uses both gyro and accelerometer data to putout very accurate Euler Angels.

sukalo98:
converting those values into degrees per second and feeding all of that information into a PID loop.

I think what you should be doing is calculating the total orientation of the copter (Euler Angles in degrees) and then using the PID loop to get you to 0 degrees from both the x and y axis (or whatever your setpoints are).

Power_Broker:
This means that your D value is too low and isn't dampening out the oscillations like it should. Increase D, try another flight, and repeat until you get a stable response.

Okay, I will try that. Do you have any recommendations regarding how much I should be incrementing my D value (assuming just for my Pitch/Roll)? For example: should I add 0.5, 1, 5, 10 each time I increase D?

Power_Broker:
Why in the world would you not also use the accelerometer data? A complimentary filter (see this link for more info) uses both gyro and accelerometer data to putout very accurate Euler Angels.

I think what you should be doing is calculating the total orientation of the copter (Euler Angles in degrees) and then using the PID loop to get you to 0 degrees from both the x and y axis (or whatever your setpoints are).

After reading many forums online, most experts recommend you begin designing a flight controller in rate mode (degrees per second) to get a better understanding of how a quadcopter flies, understand how to tune and then move on to fusing the accelerometer/gyroscope data to put out Euler angles. Then, I can try auto-level and all that.

Once I figure flying in rate mode (which is supposed to be easier), I do plan to move on to using Euler angles.

sukalo98:
Once I figure flying in rate mode (which is supposed to be easier), I do plan to move on to using Euler angles.

I highly doubt that "rate mode" is easier. Honestly I think it's ridiculous and here's why: Let's say your PID loops correctly get you to 0 deg/sec in both x and y axis. That doesn't mean that your copter is level - it could be in any orientation (including upside-down!). Therefore, it isn't that useful to begin with.

If you follow the linked tutorial, you can easily do sensor fusion without much prior knowledge.

sukalo98:
Okay, I will try that. Do you have any recommendations regarding how much I should be incrementing my D value (assuming just for my Pitch/Roll)? For example: should I add 0.5, 1, 5, 10 each time I increase D?

No idea. Time for you to experiment!

Power_Broker:
I highly doubt that "rate mode" is easier. Honestly I think it's ridiculous and here's why: Let's say your PID loops correctly get you to 0 deg/sec in both x and y axis. That doesn't mean that your copter is level - it could be in any orientation (including upside-down!). Therefore, it isn't that useful, to begin with.

If you follow the linked tutorial, you can easily do sensor fusion without much prior knowledge.

I understand your opinion about them, I used to think the same thing. I read it's for pilots that want more control of their quads movement. This Link explains what it's all about. The PID loop should be able to level my quad whether in Acro (rate) or Auto-Level mode. I'm so close to this quad flying in acro (rate) mode, I don't want to give up and switch to AutoLevel mode just yet.

Power_Broker:
No idea. Time for you to experiment!

Fun!

I increased my D gain slightly and the quadcopter began oscillating even more rapidly. What is wrong?

sukalo98:
I increased my D gain slightly and the quadcopter began oscillating even more rapidly. What is wrong?

Was there a wind gust? Did you forget to upload the code? Try increasing D even more.

No, there was no wind gust and I uploaded the correct code. I even tried feeling for the corrections in my hand but with the D gain barely increased, it began to oscillate uncontrollably. I have a question though, isn't proper practice to tune the P gain first? Also, should I start with tuning the Pitch, Roll or Yaw axis first?

Tune pitch and roll. Unless your quad is rectangular, they should use the same constants. Yaw should be easier so it may be close to OK to begin with.

MorganS:
Tune pitch and roll. Unless your quad is rectangular, they should use the same constants. Yaw should be easier so it may be close to OK to begin with.

I held the quadcopter in my hand and got a decent value for my Pitch/Roll P gain. Next, I attempted to tune the I gain. I increased the I gain value by 0.01 and immediately, the top left and bottom motors turned off causing the quadcopter to permanently tilt to the side. I tried increasing and decreasing I but this problem persisted. I believe this may be an Integral windup. I'm going to review what values my PID output is outputting.

I was attempting to tune my Pitch/Roll Axis for my quadcopter, I tuned P gain to a reasonable setting. I tried to tune my I gain by holding it in my hand and feeling how well the quad fights external movements. I incremented the I gain by 0.001 and when I reach 0.01, suddenly there was a huge jump in the iError and the quad was stuck rolling to one side. I attached an example of my PID Output, I am not sure what is wrong. Is there something wrong with my PID algorithm?

PID Algorithm:

void PID()
{
  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 = 270, pid_max_pitch = 180, pid_max_roll = pid_max_pitch; //maximum output = degrees/s maximum range
  static int previous_pid_output_yaw, previous_pid_output_pitch, previous_pid_output_roll;

  if (reset_pid)
  {
    pError_yaw = 0; pError_pitch = 0; pError_roll = 0;
    iError_yaw = 0; iError_pitch = 0; iError_roll = 0;
    dError_yaw = 0; dError_pitch = 0; dError_roll = 0;
    pid_i_yaw = 0; pid_i_pitch = 0; pid_i_roll = 0;
    pid_output_yaw = 0;
    pid_output_pitch = 0;
    pid_output_roll = 0;
    reset_pid = false;
  }

  //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 = pid_i_gain_yaw * iError_yaw; //Integral Calculations
  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;
  dError_yaw = (pError_yaw - previouspError_yaw);

  pid_output_yaw = (pid_p_gain_yaw * pError_yaw) + pid_i_yaw + (pid_d_gain_yaw * dError_yaw); //Output calculation
  if (pid_output_yaw > pid_max_yaw) pid_output_yaw = pid_max_yaw;
  else if (pid_output_yaw < pid_max_yaw * -1) pid_output_yaw = pid_max_yaw * -1;

  //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 = pid_i_gain_pitch * iError_pitch; //integral calculations
  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;
  dError_pitch = (pError_pitch - previouspError_pitch);

  pid_output_pitch = (pid_p_gain_pitch * pError_pitch) + pid_i_pitch + (pid_d_gain_pitch * dError_pitch); //Output calculation
  if (pid_output_pitch > pid_max_pitch) pid_output_pitch = pid_max_pitch;
  else if (pid_output_pitch < pid_max_pitch * -1) pid_output_pitch = pid_max_pitch * -1;

  //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 = pid_i_gain_roll * 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;
  dError_roll = (pError_roll - previouspError_roll);

  pid_output_roll = (pid_p_gain_roll * pError_roll) + pid_i_roll + (pid_d_gain_roll * dError_roll); //Output calculation
  if (pid_output_roll > pid_max_roll) pid_output_roll = pid_max_roll;
  else if (pid_output_roll < pid_max_roll * -1) pid_output_roll = pid_max_roll * -1;


  //Remember previous values
  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;
}

sukalo98:
I held the quadcopter in my hand and got a decent value for my Pitch/Roll P gain. Next, I attempted to tune the I gain.

No. You tune I LAST. The I coefficient takes care of any long term offset. You should've tuned the D value after finding a good P value.

Power_Broker:
No. You tune I LAST. The I coefficient takes care of any long term offset. You should've tuned the D value after finding a good P value.

Okay, I will try to tune D, then I tomorrow. I would like to know, does it matter if I attempt to tune the quad in my hand, rather than tied down with string or in the air (I'm a novice pilot who has never flown in rate mode)?

sukalo98:
Okay, I will try to tune D, then I tomorrow. I would like to know, does it matter if I attempt to tune the quad in my hand, rather than tied down with string or in the air (I'm a novice pilot who has never flown in rate mode)?

Holding it in your hand sounds like a bad idea, but I am no expert on quadcopters. You should do some googling to see what type of setups other people use to tune their quadcopters.

I attempted to fly it today. It went about 8 feet in the air, hovered steadily for almost a second, then began to oscillate slowly, then rapidly, then finally flipped over in the air and crashed. I attached thisvideo to give a fairly accurate description of what happened. It broke one propeller.

I started my P gain for Pitch/Roll PID at 0.8. I and D gains were at 0 for Pitch/Roll Loops. P, I, D gains was set at 0 for Yaw loop. I verified the motors responded in the right directions to my PID loops with the gyro and controller inputs.

Is it my fault I didn't immediately move the quadcopter around? Should I not let it attempt to hover untuned and immediately move it around and start tuning? Would it be better in my case to tie the quadcopter down and try to tune it that way because of what happened? Did I set the P gain for Pitch/Roll too high to begin with? Any advice would be greatly appreciated.

PS, I never got the opportunity to attempt to tune the quad..

Take a look at this. This guy can help better than most on this forum.

Power_Broker:
Take a look at this. This guy can help better than most on this forum.

I'm familiar with the YMFC-3D videos. His tutorials helped guide me through setting up the PID calculations. His tuning method is slightly abnormal because he starts tuning the D gain first, but I can give that another try when my new propellers come in.

sukalo98:
I'm familiar with the YMFC-3D videos. His tutorials helped guide me through setting up the PID calculations. His tuning method is slightly abnormal because he starts tuning the D gain first, but I can give that another try when my new propellers come in.

I think tuning quadcopters and airplanes are a special case and probably should be tuned differently.

Either way, you should look into building a test stand that holds your drone stationary, but allows it to tilt in all directions. That way you can tune it without crashing (even if you wanted to, lol).

Power_Broker:
I think tuning quadcopters and airplanes are a special case and probably should be tuned differently.

Either way, you should look into building a test stand that holds your drone stationary, but allows it to tilt in all directions. That way you can tune it without crashing (even if you wanted to, lol).

Lol I will look into that. That seems like the best route since I have never flown a quadcopter in acro/rate mode, I'm used to auto level.