Pid loop not writing over error

Can I please have help getting my PID code to work. I'm not sure where it's going wrong. The angle value isn't changing, and therefore error is not changing. This makes the motor_output keep increasing instead of changing to what it needs to be, this means that the servo motor won't move, which is constrained to a position of 30 to 150 degrees. I am using an IMU MPU6050 three axis gyroscope and accelerometer. I can't change the code to much, and I can't use a different sensor because of constraints, mostly because this is the first time I've done something like this and am new to C++ and Arduino.
Here is my code below:

Hey guys, sorry, like I said I'm new to this. Here it is formatted properly.

#include <Servo.h>
#include "MPU6050.h"
#define GYROSCALE 0.00763358778626
#define ACCELSCALE 0.00006103515625

MPU6050 accelgyro;
//Add the includes for the sensor libraries

//Setup the gains for your control loop here
#define KP 0.001
#define KI 0
#define KD 0

//Setup the variables for your control
Servo fin_servo; //create servo object to control a servo

float set_point = 0;

int16_t ax, ay, az;
int16_t gx, gy, gz;

float f_gx, f_gy, f_gz;
float ang_vel = 0;
int16_t bias;

float error;
float motor_output;
float angle_value;

long sum;

float f_angle;

float integral = 0;
float derivative;
float error_previous = 0;

//Setup the time recording variables, these are unsigned long as they can only be positive integers and can be quite large values.
unsigned long DT, end_time, start_time;

void setup() {
  // put your setup code here, to run once:
  //Setup serial port and baud rate, ensure baudrate in serial monitor matches up!
  Serial.begin(38400);

  fin_servo.write(75);
  
  // get or set setpoint value
  set_point = 75; // desired angle in degrees, change this to what is required.

  //attach your servo motor to pin 8, check if your hardware uses pin 8 for the servo motor
  signal.
  fin_servo.attach(8);

  //calibrate the sensor
  for(int i = 0; i<500; i++){
    accelgyro.getMotion6(&ax, &ay, &az, &gx, &gy, &gz);
    sum = sum + gx;
    delay(10);
    }
  bias = sum/500;
}

void loop() {
  // put your main code here, to run repeatedly:
 //get starttime
  start_time = millis();
  
 //get sensor value, this needs to be returned in angvelocity (this should be calibrated as 
 well)
 accelgyro.getMotion6(&ax, &ay, &az, &gx, &gy, &gz);
 ang_vel = (gx - bias) * GYROSCALE;

 //integrate the angular vel to get angle.
 angle_value = angle_value + ang_vel * DT/1000.0;

 //calc error
  error = set_point - angle_value;
  // print error to serial to check
  Serial.print("Error is: ");
  Serial.println(error);
  
  //calc integral error
  integral = integral + error*DT/1000.0;

  //calc derivative error
  derivative = (error - error_previous)/DT/1000.0; 
  //calc motor output 
  motor_output = KP*error + KI*integral + KD*derivative;

  //check if motor output is within its limits of the servo motor.
  
  if (150 >= motor_output && motor_output >= 30) {Serial.println("Motor output is within the 
    limits of the servo motor");
    fin_servo.write(motor_output);
    } 
    else {Serial.println("Motor output is outside of the limits of the servo motor");
    }
  
  //printout serial motor_output for debug
  Serial.println(motor_output);

  //set error_previous = error for derivative error calc next time around
  error_previous = error;
  
  //get the endtime
  end_time = millis();
  
  DT = end_time - start_time; //calc loop time, this should be less that 50ms but greater than 10ms
  
  //print DT to serial to check
  Serial.println(DT);
}

Thanks

Welcome to the forum

Please post your sketch, using code tags when you do. This prevents parts of it being interpreted as HTML coding and makes it easier to copy for examination

In my experience the easiest way to tidy up the code and add the code tags is as follows
Start by tidying up your code by using Tools/Auto Format in the IDE to make it easier to read. Then use Edit/Copy for Forum and paste what was copied in a new reply. Code tags will have been added to the code to make it easy to read in the forum thus making it easier to provide help.

Hi @sean_flano ,

here your sketch in a better shape for the forum:

#include <Servo.h>
#include "MPU6050.h"
#define GYROSCALE 0.00763358778626
#define ACCELSCALE 0.00006103515625

MPU6050 accelgyro;
//Add the includes for the sensor libraries

//Setup the gains for your control loop here
#define KP 0.001
#define KI 0
#define KD 0

//Setup the variables for your control
Servo fin_servo; //create servo object to control a servo
float set_point = 0;
int16_t ax, ay, az;
int16_t gx, gy, gz;
float f_gx, f_gy, f_gz;
float ang_vel = 0;
float error;
float motor_output;
float angle_value;
int16_t bias;
long sum;
float f_angle;
float integral = 0;
float derivative;
float error_previous = 0;

//Setup the time recording variables, these are unsigned long as they can only be positive integers and can be quite large values.
unsigned long DT, end_time, start_time;

void setup() {
  // put your setup code here, to run once:
  //Setup serial port and baud rate, ensure baudrate in serial monitor matches up!
  Serial.begin(38400);

  fin_servo.write(75);

  // get or set setpoint value
  set_point = 75; // desired angle in degrees, change this to what is required.

  //attach your servo motor to pin 8, check if your hardware uses pin 8 for the servo motor signal.
  fin_servo.attach(8);

  //calibrate the sensor
  for (int i = 0; i < 500; i++) {
    accelgyro.getMotion6(&ax, &ay, &az, &gx, &gy, &gz);
    sum = sum + gx;
    delay(10);
  }
  bias = sum / 500;
}

void loop() {
  // put your main code here, to run repeatedly:
  //get starttime
  start_time = millis();

  //get sensor value, this needs to be returned in angvelocity (this should be calibrated as well)
  accelgyro.getMotion6(&ax, &ay, &az, &gx, &gy, &gz);
  ang_vel = (gx - bias) * GYROSCALE;

  //integrate the angular vel to get angle.
  angle_value = angle_value + ang_vel * DT / 1000.0;

  //calc error
  error = set_point - angle_value;
  // print error to serial to check
  Serial.print("Error is: ");
  Serial.println(error);

  //calc integral error
  integral = integral + error * DT / 1000.0;

  //calc derivative error
  derivative = (error - error_previous) / DT / 1000.0;
  //calc motor output
  motor_output = KPerror + KIintegral + KD * derivative;

  //check if motor output is within its limits of the servo motor.

  if (150 >= motor_output && motor_output >= 30) {
    Serial.println("Motor output is within the limits of the servo motor");
    fin_servo.write(motor_output);
  }
  else {
    Serial.println("Motor output is outside of the limits of the servo motor");
  }

  //printout serial motor_output for debug
  Serial.println(motor_output);

  //set error_previous = error for derivative error calc next time around
  error_previous = error;

  //get the endtime
  end_time = millis();

  DT = end_time - start_time; //calc loop time, this should be less that 50ms but greater than 10ms

  //print DT to serial to check
  Serial.println(DT);
}

It does not compile ...

sketch.ino: In function 'void loop()':
sketch.ino:80:18: error: 'KPerror' was not declared in this scope
   motor_output = KPerror + KIintegral + KD * derivative;
                  ^~~~~~~
sketch.ino:80:18: note: suggested alternative: 'error'
   motor_output = KPerror + KIintegral + KD * derivative;
                  ^~~~~~~
                  error
sketch.ino:80:28: error: 'KIintegral' was not declared in this scope
   motor_output = KPerror + KIintegral + KD * derivative;
                            ^~~~~~~~~~
sketch.ino:80:28: note: suggested alternative: 'integral'
   motor_output = KPerror + KIintegral + KD * derivative;
                            ^~~~~~~~~~
                            integral

Error during build: exit status 1

I copied your sketch to Wokwi for formatting (plus removal of some blank lines) and testing:

https://wokwi.com/projects/413546141063186433

Are you sure that you posted the recent version?

ec2021

KPerror should be KP * error.
KIintegral should be KI * integral.
KDderivative shoild be KD * derivative.

Copy/paste error between two sketches sketch and forum.

Hi @xfpd ,

I made the changes you suggested and it's compiling:

https://wokwi.com/projects/413570225211763713

@sean_flano

Does this match your recent version?

Yes, that is my most recent version.

Ok.

With KI and KD defined as 0 the sketch only uses KP, the proportional part.

Usually it is helpful to implement the algorithm in a spreadsheet or create a small test program to print a the table for different input values.

[Edit]:

Here a sketch that prints the most relevant data in one line:

https://wokwi.com/projects/413621226543292417

However, you are using a servo not a stepper motor. Does it really make sense to change the servo position based on an angle value calculated from integrating the angular velocity of a gyro? If I do not miss something there is no reference to the real starting angle, the software always starts with zero.

The line " fin_servo.attach(8);" must be called before the line " fin_servo.write(75);" in setup()!

Thanks. I am trying to base the movement of the servo motor of moving the device, and that's why I'm basing motor output off the angle value calculated of a gyro value. I didn't see that last error though, and I do know that your order is the correct order for it to work. Thanks for pointing out my mistake.

My understanding:

  • There is a device with a MPU6050 attached to it
  • The device can rotate
  • The angular velocity of the device shall be integrated to determine the rotation angle
  • The servo shall be commanded to point to this angle

Does this describe your application?

Just some further issues:

  set_point = 75; // desired angle in degrees, change this to what is required.

and

 //calc error
  error = set_point - angle_value;

From those two lines it is clear that you want to compare angles in degrees (of 360°). The output of the gyro library function function

 ang_vel = (gx - bias) * GYROSCALE;

seems to be the angular velocity in rad/sec but not degrees/sec. To get degrees/sec you have to multiply the ang_vel by (180.0/3.1415).

Now an integration over the time between two measurements will calculate the angle_value in degrees.

angle_value = angle_value + ang_vel * (DT / 1000.0) * (180.0/3.1415):

As you have set KI and KD to zero the calculation of motor_output is only depending on the proportional part:

//calc motor output
 motor_output = KP * error;           // + KI * integral + KD * derivative;

With KP = 0.001 and error = (set_point - angle_value) motor_output will be

motor_output = (set_point-angle_value) * 0.001

This means the servo will point to (angular difference divided by 1000).

With other words:

To move the servo to e.g. 30 degrees it would require an error of 30000 degrees etc.

Am I mistaken?

1 Like

Hi,
Your understanding is correct.

I did, however, look at the datasheet of the gyroscope I am using, the IMU MPU6050, and it says the angle is outputted in degrees per second. This just needs to be converted from analogue output to something readable by humans, which is what my conversions do.

On the second point, I recently just tested it and found that I haven't been getting the values from the gyroscope, or at least, there is an error concerning those values in my code. I had set it to 0.001 because I was seeing how it responded to different values. Because I do not seem to be receiving the values for the gyroscope, or if I am, they just aren't having effects on my calculations, the KP, KI, and KD values aren't influencing things too much, only that if I set KI to a non-zero value, the error just keeps on increasing without seeming to slow down or stop.
I have some code that successfully gets the values from the gyroscope, so I'll look at this against that. If I find anything I did wrong, I'll post it.
Thanks

This function should return raw data in a range of (-32767 to + 32767).

The calculation for the gyro data should be

image

where "range" depends on the range chosen from this list:

MPU6050_GYR_RANGE_250, // +/- 250 deg/s (default)
MPU6050_GYR_RANGE_500, // +/- 500 deg/s
MPU6050_GYR_RANGE_1000, // +/- 1000 deg/s
MPU6050_GYR_RANGE_2000 // +/- 2000 deg/s

This website may be of assistance:

https://wolles-elektronikkiste.de/en/mpu6050-accelerometer-and-gyroscope

Good luck and success!
ec2021

BTW: Feel free to check the changed degree/s calculation at Wokwi:

https://wokwi.com/projects/413660436808326145

Integrating the MPU-6050's gyroscope's °/s measurement to get an angle is going to give you a pretty terrible input to your PID.

Starting up sitting still, it could be biased by +/-20°/sec and rapidly integrate away from 0.

How fast do you expect the system spin in real life? If the fin was at one extreme, would it spin at 250°/sec? 500°/sec? More?

One way to get reasonable PID Kp starting values is to look at them as a units conversion and do the math -- If you had 75° of error, how much output fin deflection would you want the system to produce? 60° of servo? then Kp should be on the order of 60°/75° = 0.8°ServoDefection/°AngleError.

Integration of a gyro could rapidly go to much more than 75° of error What would you want the system to do if your integration showed more than than 180° or 360° or more turns of error? Unwind all the way back? Or slow down and turn to the closest angle?

Since you don't have a magnetometer to measure actual angle, maybe it would be best to try to get the gyro rate down to 0 before you deal with controlling the integration of the gyro.

Thanks, you really helped. Your post helped me realise I forgot to initialise the MPU6050, by not including these two lines:

  accelgyro.initialize();
  accelgyro.setFullScaleGyroRange(0);

Some of this might seem different, as the particular library I'm using is from GitHub, and is designed by Jeff Rowberg, who seems to be from MIT. I used particular library this on a recommendation. I am fairly certain, but not 100% sure, that it requires this startup code to run the MPU6050.
The final line automatically sets the gyroscope range to +/-250 degrees, and to change the range you just increase the 0 to 1, 2, ...

As for bias, I used a for loop to calculate the mean of the first 500 values as bias, then subtracted this from all further values to remove potential bias, which gets the values fairly close to zero. I know this isn't the best method, but it is fairly fast, and when I used slower methods, or used this method multiple times, the Arduino just wouldn't run the program as it was too slow. I also won't be moving the actual device to fast, and I'll be keeping it within a range of movement from 30 to 150 degrees. Here's the for loop I mentioned earlier:

  for (int i = 0; i < 500; i++) {
    accelgyro.getMotion6(&ax, &ay, &az, &gx, &gy, &gz);
    sum = sum + gx;
    delay(10);
  }
  bias = sum / 500;

I also multiplied the Gyroscope values by the Gyroscale value to convert the values from raw to degrees per second.
Thanks again

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.