Using PID Loops with 2 outputs

Hi All,

I've been working on a project of building a thrust vectoring gimbal system on and off for a while and think I'm nearing a point where things are working,

A brief outline on whats going on. I am using an MPU9250 to gather IMU data and feeding this through a function I created to calculate the PID output. I put it into a function so I could call it with different axis in the loop just for the sake of simplicity (See below)

//Calculate output based on axis 
double OutputCalc(double axis){
  Input = axis;
  myPID.Compute();
  double Calc = Output;
  if (Calc >= MAXANGLE){
    Calc = MAXANGLE;
  }
  if (Calc <= MINANGLE){
    Calc = MINANGLE;
  }

  return Calc;
}

This being my first time dealing with control systems I'm really just winging it so it's entirely possible this is bad practice, if so please let me know. A couple other bits I've just as logical solutions which may also be poor practice is an if statement to limit the output. I did it this way so that the output wouldn't just be clipped after 90 but would return to 90 each time so as to prevent any 'windup' of the output. I then took this output and mapped the values from the initial -90 - 90 to 0 - 180. I was thinking to perhaps initialise the output to 90 instead and then change the min and max limit to 0 - 180 but this way has worked so far.

The main part I'm struggling with is setting the setpoint for each. My initial plan was to ensure that the starting position of the vehicle was recorded in setup and these positions would be the setpoints. A few problems with that is for some getting an average of the readings has proven to be more difficult for me than expected as you will see in a previous post I've raised. The other thing is how to store the different setpoints. I'm not confident enough in my coding abilities to go in and try to link another variable to library so I was thinking to add this into the function too and add another variable to call.

Below all the code in context.


#include "MPU9250.h"
#include <Arduino.h>
#include <ESP32Servo.h>
#include <SFE_BMP180.h>
#include <esp_now.h>
#include <WiFi.h>
#include <PID_v1.h>
#include "Filter.h"

//GlobalVariables  --------------------------------------
//Set Min/Max values for PID Output
int MAXANGLE = 90;
int MINANGLE = -90;

//Set Servo Pins
int XPin = 16;
int YPin = 17;
int ESCPin = 4;

//PID Tuning Parameters
double Kp=0.6;
double Ki=0.5;
double Kd=0.125;

float RollOutput = 0;
float PitchOutput = 0;

float RawPitch = 0;
float RawRoll = 0;

float FilteredPitch = 0;
float FilteredRoll = 0;
//Create PID Variables
double Setpoint, Input, Output;

double PitchAVG = 0;
double RollAVG= 0;

//Initialisers -----------------------------------------
//Create Servos
Servo XAxis;
Servo YAxis;
Servo ESC;

//Create pressure sesor variables
SFE_BMP180 pressure;

//Create IMU Variable
MPU9250 mpu;

//Filter Variables
ExponentialFilter<long> FilterPitch(25, 0);
ExponentialFilter<long> FilterRoll(25, 0);

double baseline;

PID myPID(&Input, &Output, &Setpoint, Kp, Ki, Kd, DIRECT);
//--------------------------------------------------------

                   
//RX Data struct
typedef struct struct_message {
  int val;
} struct_message;
struct_message myData;

//Calculate output based on axis 
double OutputCalc(double axis){
  Input = axis;
  myPID.Compute();
  double Calc = Output;
  if (Calc >= MAXANGLE){
    Calc = MAXANGLE;
  }
  if (Calc <= MINANGLE){
    Calc = MINANGLE;
  }

  return Calc;
}

//Recieve ESP32 Data from controller
void OnDataRecv(const uint8_t * mac, const uint8_t *incomingData, int len) {
  memcpy(&myData, incomingData, sizeof(myData));
}

//Pressure get funcion
double getPressure()
{
  char status;
  double T,P,p0,a;

  status = pressure.startTemperature();
  if (status != 0)
  {


    delay(status);

    status = pressure.getTemperature(T);
    if (status != 0)
    {

      status = pressure.startPressure(3);
      if (status != 0)
      {

        delay(status);

        status = pressure.getPressure(P,T);
        if (status != 0)
        {
          return(P);
        }
        else Serial.println("error retrieving pressure measurement\n");
      }
      else Serial.println("error starting pressure measurement\n");
    }
    else Serial.println("error retrieving temperature measurement\n");
  }
  else Serial.println("error starting temperature measurement\n");
}

void print_calibration() {
  Serial.println("< calibration parameters >");
  Serial.println("accel bias [g]: ");
  Serial.print(mpu.getAccBiasX() * 1000.f / (float)MPU9250::CALIB_ACCEL_SENSITIVITY);
  Serial.print(", ");
  Serial.print(mpu.getAccBiasY() * 1000.f / (float)MPU9250::CALIB_ACCEL_SENSITIVITY);
  Serial.print(", ");
  Serial.print(mpu.getAccBiasZ() * 1000.f / (float)MPU9250::CALIB_ACCEL_SENSITIVITY);
  Serial.println();
  Serial.println("gyro bias [deg/s]: ");
  Serial.print(mpu.getGyroBiasX() / (float)MPU9250::CALIB_GYRO_SENSITIVITY);
  Serial.print(", ");
  Serial.print(mpu.getGyroBiasY() / (float)MPU9250::CALIB_GYRO_SENSITIVITY);
  Serial.print(", ");
  Serial.print(mpu.getGyroBiasZ() / (float)MPU9250::CALIB_GYRO_SENSITIVITY);
  Serial.println();
}

void setup() {

  Serial.begin(115200);
  Wire.begin();
  delay(2000);

  WiFi.mode(WIFI_STA);

  // Init ESP-NOW
  if (esp_now_init() != ESP_OK) {
    Serial.println("Error initializing ESP-NOW");
    return;
  }
  
  // Once ESPNow is successfully Init, we will register for recv CB to
  // get recv packer info
  esp_now_register_recv_cb(OnDataRecv);

  if (!mpu.setup(0x68)) {  // change to your own address
  while (1) {
    Serial.println("MPU connection failed. Please check your connection with `connection_check` example.");
    delay(5000);
    }
    }
  if (pressure.begin()){
    Serial.println("BMP180 init success");
  }else{
    Serial.println("BMP180 init fail (disconnected?)\n\n");
    while(1); // Pause forever.
    }


  // calibrate anytime you want to
  Serial.println("Accel Gyro calibration will start in 5sec.");
  Serial.println("Please leave the device still on the flat plane.");
  mpu.verbose(true);
  delay(5000);
  mpu.calibrateAccelGyro();

  print_calibration();
  mpu.verbose(false);

  ESP32PWM::allocateTimer(0);
  ESP32PWM::allocateTimer(1);
  ESP32PWM::allocateTimer(2);
  ESP32PWM::allocateTimer(3);
  XAxis.setPeriodHertz(50);
  XAxis.attach(XPin, 500, 2400); 
  YAxis.setPeriodHertz(50);
  YAxis.attach(YPin, 500, 2400); 
  ESC.setPeriodHertz(50);
  ESC.attach(ESCPin, 1000, 2000);

  baseline = getPressure();
  
  Serial.print("baseline pressure: ");
  Serial.print(baseline);
  Serial.println(" mb");  

  Setpoint = 0;
  myPID.SetMode(AUTOMATIC);

  XAxis.write(90);
  YAxis.write(90);
}

void loop() {
  if(mpu.update()){

    RawPitch = mpu.getPitch();
    RawRoll = mpu.getYaw();

    FilterPitch.Filter(RawPitch);
    FilterRoll.Filter(RawRoll);

    FilteredPitch = FilterPitch.Current();
    FilteredRoll = FilterRoll.Current();
    
    PitchOutput = OutputCalc(FilteredPitch);
    RollOutput = OutputCalc(FilteredRoll);

    XAxis.write(map(PitchOutput, -90, 90, 0, 180));
    YAxis.write(map(RollOutput, -90, 90, 0, 180));

    ESC.write(myData.val);


    Serial.print("RawPitch:");
    Serial.print(RawPitch);
    Serial.print(",");
    Serial.print("FilteredPitch:");
    Serial.print(FilteredPitch);
    Serial.print(",");
    Serial.print("Pitch Output:");
    Serial.print(map(PitchOutput, -90, 90, 0, 180));
    Serial.print(",");
    Serial.print("RawRoll");
    Serial.print(RawRoll);
    Serial.print(",");
    Serial.print("FilteredRoll:");
    Serial.print(FilteredRoll);
    Serial.print(",");
    Serial.print("Roll Output:");
    Serial.println(map(RollOutput, -90, 90, 0, 180));
    // Serial.print(",");
    // Serial.print("Pressure:");
    // Serial.println(getPressure());

  }


}


Any help and constructive criticism of my code would be greatly appreciated

Kind Regards

Aman

For two inputs and two outputs, two separate PID objects/loops are required.

If the inputs are orientation angles, keep in mind that angles interact, and the order of applying the rotations matters a great deal. They are not independent variables.

The inputs are orientation angles that will be used to generate an output to stabilise the vehicle. Because of my gimbaling mechanism as far as I'm aware I can manipulate both axis at once by gimbaling the motor diagonally across the axis. For this reason I haven't considered this in more depth but presumably this will be something to consider in greater detail if and when I take yaw into consideration more and when I begin to set a co ordinate system and navigation for the vehicle, at such point I will have to consider the Euler's angles and order of rotation as you have kindly enlightened me on in a previous post.

For now I have looked into the PID library a bit more and realised that it isn't too difficult at all to make a separate object/loop. I can simply make another PID variable and assign it similar variables and the same tuning parameters as such.

PID myPID(&Input1, &Output1, &Setpoint1, Kp, Ki, Kd, DIRECT);
PID myPID2(&Input2, &Output2, &Setpoint2, Kp, Ki, Kd, DIRECT);

The other thing is averaging the MPU data... probably the simplest thing I need to do and this will be a huge face palm for anyone who has tried to help me on a previous post I ended up asking about this in.

Because this project has been so on/off I've basically re written most of it since then. Below is a function I was testing to calculate the average on my most recent attempt.

float PitchAVG = 0;
for(int i = 1; i < 10; i ++){
  mpu.update();
  PitchAVG = PitchAVG + mpu.getPitch();
  delay(200);
}
  PitchAVG = PitchAVG/10;

I've tried several variations of this frustratingly simple function. I was originally initialising the int i to 0 but I figured that in actuality this would store 11 values so I then changed that to 1...Still returning seemingly arbitrary values. I'm running this in the setup so perhaps it could be something to do with the bias not being calculated (though this is the last thing in the setup, after the bias is calculated and applied) I'm slightly stumped as to why this of all of things is what is tripping me up but and suggestions would be great.

These two for loop initializers will each average 10 items:

for (int i = 0;  i < 10; i++) {
for (int i = 1;  i <= 10; i++) {

I doubt it will help to average orientation angles. The loop sample time becomes MUCH longer (which is a problem), and again, the angles interact with each other.

This isn't for use in the main loop. I am using an exponential filter for the orientation data. It does only output int values for now, I intend to test the stabilisation using the exponential filter and if higher resolution data is required I shall substitute it for a float and add significant figures as required until I've found the optimum resolution

So upon further testing there appears to be a huge spike in the data for roll and pitch values when the IMU is initialised which is presumably the reason/ one of the reasons the data has been corrupt. For the first half second or so the Pitch data spikes from 75 back down to 0.25 and the roll does a similar thing. Any suggestions on how to overcome this or a better way of setting the setpoint?

My next idea would have a 20 second calculation period where a reading is taken every second and stored into an array. I would then take the last 10 readings from the array and average those therefore comfortably missing the initial spike and reading the stabilised actual data. This does just seem an unnecessary long a complicated solution for what seems a simple issues so again, I'd love to hear any better solutions.

I've tested this with the below code.... No luck, still the readings don't correlate to the readings from the main loop.

  float averagep [20] = {0};
  float averager [20] = {0};
  float averagep1 = 0;
  float averager1 = 0;

  Serial.println("Calculating Average pitch. This will take 20 seconds");

  for(int i = 0; i < 20; i++){
    if(mpu.update()){
      averagep[i] = mpu.getPitch();
      Serial.print("Reading ");
      Serial.print(i);
      Serial.print(" = ");
      Serial.println(averagep[i]);
      delay(1000);
    }
  }

    for(int i = 10; i <20; i++){
    averagep1 += averagep[i];
  }

  PitchAVG = averagep1 / 10;

  Serial.print("Average Pitch = ");
  Serial.println(PitchAVG);

  Serial.println("Calculating Average roll. This will take 20 seconds");
  
  for(int i = 0; i < 20; i++){
    if(mpu.update()){
      averager[i] = mpu.getRoll();
      Serial.print("Reading ");
      Serial.print(i);
      Serial.print(" = ");
      Serial.println(averager[i]);
      delay(1000);
    }
  }

  for(int i = 10; i <20; i++){
    averager1 += averager[i];
  }
  RollAVG = averager1 / 10;

  Serial.print("Average Roll = ");
  Serial.println(RollAVG);

  Setpoint1 = PitchAVG;

I printed all the readings so I could see what was actually being put into the array. For context below are the readings from my loop.

22:15:04.546 -> Calculating Average pitch. This will take 20 seconds
22:15:04.546 -> Reading 0 = -80.12
22:15:05.543 -> Reading 1 = -26.12
22:15:06.530 -> Reading 2 = 11.17
22:15:07.572 -> Reading 3 = -16.64
22:15:08.565 -> Reading 4 = 12.74
22:15:09.543 -> Reading 5 = -16.51
22:15:10.556 -> Reading 6 = 14.13
22:15:11.556 -> Reading 7 = -12.62
22:15:12.566 -> Reading 8 = 15.97
22:15:13.567 -> Reading 9 = -17.71
22:15:14.564 -> Reading 10 = 12.68
22:15:15.582 -> Reading 11 = -10.62
22:15:16.603 -> Reading 12 = 16.57
22:15:17.607 -> Reading 13 = -15.39
22:15:18.606 -> Reading 14 = 16.43
22:15:19.601 -> Reading 15 = -16.26
22:15:20.597 -> Reading 16 = 13.13
22:15:21.589 -> Reading 17 = -14.36
22:15:22.620 -> Reading 18 = 17.04
22:15:23.610 -> Reading 19 = -15.91
22:15:24.627 -> Average Pitch = 0.33
22:15:24.627 -> Calculating Average roll. This will take 20 seconds
22:15:24.627 -> Reading 0 = 167.95
22:15:25.615 -> Reading 1 = -160.98
22:15:26.611 -> Reading 2 = 165.44
22:15:27.635 -> Reading 3 = -161.88
22:15:28.622 -> Reading 4 = 167.35
22:15:29.650 -> Reading 5 = -162.12
22:15:30.652 -> Reading 6 = 168.81
22:15:31.659 -> Reading 7 = -161.89
22:15:32.634 -> Reading 8 = 169.98
22:15:33.668 -> Reading 9 = -160.40
22:15:34.661 -> Reading 10 = 170.89
22:15:35.671 -> Reading 11 = -160.40
22:15:36.668 -> Reading 12 = 172.47
22:15:37.678 -> Reading 13 = -160.69
22:15:38.661 -> Reading 14 = 167.27
22:15:39.697 -> Reading 15 = -161.14
22:15:40.677 -> Reading 16 = 171.67
22:15:41.691 -> Reading 17 = -164.39
22:15:42.683 -> Reading 18 = 173.64
22:15:43.686 -> Reading 19 = -165.12
22:15:44.685 -> Average Roll = 4.42

This is the then the readings that show in the main loop (this is when the vehicle is stationary with just some some slight wobbles of me typing and clicking away.

22:15:46.712 -> RawPitch:0.17  ,  FilteredPitch:0.00  ,    ,  RawRoll46.07  ,  FilteredRoll:46.00
22:15:46.712 -> RawPitch:0.17  ,  FilteredPitch:0.00  ,    ,  RawRoll46.07  ,  FilteredRoll:46.00
22:15:46.712 -> RawPitch:0.17  ,  FilteredPitch:0.00  ,    ,  RawRoll46.08  ,  FilteredRoll:46.00
22:15:46.712 -> RawPitch:0.22  ,  FilteredPitch:0.00  ,    ,  RawRoll46.10  ,  FilteredRoll:46.00
22:15:46.712 -> RawPitch:0.22  ,  FilteredPitch:0.00  ,    ,  RawRoll46.10  ,  FilteredRoll:46.00
22:15:46.747 -> RawPitch:0.13  ,  FilteredPitch:0.00  ,    ,  RawRoll46.13  ,  FilteredRoll:46.00
22:15:46.747 -> RawPitch:0.19  ,  FilteredPitch:0.00  ,    ,  RawRoll46.15  ,  FilteredRoll:46.00
22:15:46.747 -> RawPitch:0.17  ,  FilteredPitch:0.00  ,    ,  RawRoll46.16  ,  FilteredRoll:46.00
22:15:46.747 -> RawPitch:0.17  ,  FilteredPitch:0.00  ,    ,  RawRoll46.16  ,  FilteredRoll:46.00
22:15:46.747 -> RawPitch:0.17  ,  FilteredPitch:0.00  ,    ,  RawRoll46.17  ,  FilteredRoll:46.00
22:15:46.781 -> RawPitch:0.15  ,  FilteredPitch:0.00  ,    ,  RawRoll46.17  ,  FilteredRoll:46.00
22:15:46.781 -> RawPitch:0.15  ,  FilteredPitch:0.00  ,    ,  RawRoll46.19  ,  FilteredRoll:46.00
22:15:46.781 -> RawPitch:0.15  ,  FilteredPitch:0.00  ,    ,  RawRoll46.19  ,  FilteredRoll:46.00
22:15:46.781 -> RawPitch:0.14  ,  FilteredPitch:0.00  ,    ,  RawRoll46.19  ,  FilteredRoll:46.00
22:15:46.781 -> RawPitch:0.13  ,  FilteredPitch:0.00  ,    ,  RawRoll46.22  ,  FilteredRoll:46.00
22:15:46.814 -> RawPitch:0.13  ,  FilteredPitch:0.00  ,    ,  RawRoll46.22  ,  FilteredRoll:46.00
22:15:46.814 -> RawPitch:0.09  ,  FilteredPitch:0.00  ,    ,  RawRoll46.19  ,  FilteredRoll:46.00
22:15:46.814 -> RawPitch:0.09  ,  FilteredPitch:0.00  ,    ,  RawRoll46.19  ,  FilteredRoll:46.00
22:15:46.814 -> RawPitch:0.07  ,  FilteredPitch:0.00  ,    ,  RawRoll46.19  ,  FilteredRoll:46.00
22:15:46.848 -> RawPitch:0.07  ,  FilteredPitch:0.00  ,    ,  RawRoll46.19  ,  FilteredRoll:46.00
22:15:46.848 -> RawPitch:0.05  ,  FilteredPitch:0.00  ,    ,  RawRoll46.20  ,  FilteredRoll:46.00
22:15:46.848 -> RawPitch:0.05  ,  FilteredPitch:0.00  ,    ,  RawRoll46.18  ,  FilteredRoll:46.00
22:15:46.848 -> RawPitch:0.04  ,  FilteredPitch:0.00  ,    ,  RawRoll46.21  ,  FilteredRoll:46.00
22:15:46.848 -> RawPitch:0.04  ,  FilteredPitch:0.00  ,    ,  RawRoll46.21  ,  FilteredRoll:46.00

The readings it is taking in the array are just a mess and I have no idea why. they are fluctuating from positive and negative for some unknown reason are just plainly wrong.... Help?!

Where do you set averagep1 to zero, before summing?

It WILL NOT WORK to have PID loops with update times of seconds. Updates must be fast, and stable loop timing is absolutely critical.

This won't be in the actual PID loop at all. This will be run one off in the setup just to generate the setpoint value which will be a constant value, not run in the main loop at all so the main loop is still less than 50 ms.

Here. Unless it needs to be initialised in the loop itself?

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