PID Control for multiple linear actuators

Hi everyone,
I'm working on a part of a project in which I have to raise a square base vertically up while keeping the model on the top horizontally level. There are 4 linear actuators, one at each corner of the base, with a model (mars) on top, as shown.

Please note:This actuator does not provide any feedback on length, nor can it be controlled by position input. It only has a 'positive' and 'negative' for the terminals of the motor.

My initial approach was to use a BNO055 mounted on the top face of the platform to measure the angle in the Y and Z axis. I treated it as two separate systems by pairing the actuators in diagonals and having the slower actuator in each pair raise at its maximum speed, while the other actuator in the pair (diagonally opposite) tried to level the platform in that axis by varying its speed via a PID controller.
The BNO055 is mounted at a 45-degree angle in the X-axis so that the Y and Z axis of the sensor are along the diagonals of the platform.

The problem I'm facing is that the actuators that are under PID control never really level with the other actuator in their pair since the actuators' max speeds are quite slow and the PID starts a bit late so even when the PID output reaches maximum speed, it takes very long for the actuator to catch up, if at all it does.

The second issue is with the difference between the speeds of the uncontrolled actuators in each pair (the ones that start rasing at max speed from the start). This difference leads to scenarios where the platform is resting level on 3 of the 4 legs with the 4th one not in contact with the ground at all (due to it being slow). But from the sensor's point of view it won't see this problem at all since the platform is level and so the angle is 0 on both Y and Z axis.

Here is the code that does what I described above:

#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BNO055.h>
#include <utility/imumaths.h>
#include <PID_v1.h>
#include <EEPROM.h>

double setpoint = 0, input_z, out_z, input_y, out_y; 
double z_Kp = 10, z_Ki = 8, z_Kd = 0;                               // Z-axis: Kp, Ki and Kd are PID tuning parameters
double y_Kp = 10, y_Ki = 8, y_Kd = 0;                              // Y-axis: Kp, Ki and Kd are PID tuning parameters
int z_pair[5] = {8, 9, 6, 7};                                       // array holding pin numbers for z-axis actuator pair in the form {IN1, IN2, EN1/PWM1, EN2/PWM2}
int y_pair[5] = {2, 4, 3, 5};                                       // array holding pin numbers for y-axis actuator pair in the form {IN1, IN2, EN1/PWM1, EN2/PWM2}
PID pid_z(&input_z, &out_z, &setpoint, z_Kp, z_Ki, z_Kd, DIRECT);
PID pid_y(&input_y, &out_y, &setpoint, y_Kp, y_Ki, y_Kd, DIRECT);

// BNO055 and Unified sensor lib variables
Adafruit_BNO055 IMU = Adafruit_BNO055(55);
sensors_event_t event;
adafruit_bno055_offsets_t calibrationData;

void setup() {
  Serial.begin(115200);
  // Set pins to output
  for (int pin = 0; pin < 4; pin++) {
    pinMode(z_pair[pin], OUTPUT);
    pinMode(y_pair[pin], OUTPUT);    
  }
  // Set the direction of the actuators via motor driver I/O

  digitalWrite(z_pair[0], LOW);
  digitalWrite(z_pair[1], HIGH);
  digitalWrite(y_pair[0], LOW);
  digitalWrite(y_pair[1], HIGH);
  
  // Set PWM of uncontrolled actuator in each pair to full speed
  digitalWrite(y_pair[3], HIGH);
  digitalWrite(z_pair[3], HIGH);  
  
  if(!IMU.begin())
  {
    Serial.print("No BNO055 detected!");
    while(1);
  }
  delay(1000);
  
  IMU.setExtCrystalUse(true);
  
  int eeAddress = sizeof(long);
  EEPROM.get(eeAddress, calibrationData);
  displaySensorOffsets(calibrationData);
  Serial.println("\n\nRestoring Calibration data to the BNO055...");
  IMU.setSensorOffsets(calibrationData);
  Serial.println("\n\nCalibration data loaded into BNO055");
  
  IMU.getEvent(&event);
  input_z = event.orientation.z;
  input_y = event.orientation.y;
  setpoint = 0;
  
  pid_z.SetOutputLimits(223, 255);   // functional range is 225 to 255 (<225 ==> 0)
  pid_y.SetOutputLimits(223, 255);   // functional range is 225 to 255 (<225 ==> 0)
  pid_z.SetMode(AUTOMATIC);
  pid_y.SetMode(AUTOMATIC);
}

void loop() {  
  IMU.getEvent(&event);
  input_z = event.orientation.z;
  input_y = event.orientation.y;
  pid_z.Compute();
  pid_y.Compute();

  if (out_z < 225) {
    out_z = 0;
  }
  if (out_y < 225) {
    out_y = 0;
  }
  
  Serial.println((String)input_z + ", " + out_z + ", " + input_y + ", " + out_y);    // Debugging Info
  
  // Set actuator speeds from PID
  analogWrite(z_pair[2], out_z);
  analogWrite(y_pair[2], out_y);
  delay(25);
}

Any help on how to fix these issues or any other approach to tackling this challenge? Thank you so much in advance.

P.S: I'm also now considering using the VL530X on each linear actuator to use the lengths of each actuator rather than an IMU, but I'm really not sure on how to implement PID(s) with 4 sensor readings.

system overview - mars model.png

is there any reason why you can't use 3 actuators instead of 4?
with 4 actuators can you have a contact on the foot to detect contact with the ground?
2 actuators attached to PID control the other 2 linked to the foot contact sensors on all feet. if a foot lifts off the actuator needs to extend further even if it offsets the balance which the PID loop will compensate.
With only 3 legs you can avoid the contact detection switches.

PID advise
NOTE:

  1. setpoint is 0° or flat and motors are all off not powered 0 PWM. Integral is not needed.
  2. The derivative is applied only when there is a change between readings and is only used when we are trying to eliminate the slow oscillation caused by the proportional lagging behind the motors. too much Derivative will cause jittering.
  3. So start tuning with only proportional and get it working nicely. Then add Derivative until you get jittering and then back down.
  4. the motors don't start turning until PWM is decently high. we will need to skip over this dead area and immediately switch from forward to reverse without having to go all the way through zero.
    This is how I insert a dead zone. the output from the PID is a number from -500 to +500 the output from the Deadzone function is a number from -255 to +255 use analogWrite(abs(output)); to adjust the PWM for the power of the motor.
int16_t Deadzone(int16_t V, uint16_t deadzone){
	int16_t absV = abs(V);
	absV = constrain(map(absV, 0, 500, deadzone ,255),0,255) ; // Inserts a Deadzone
	V = (V >= 0) ? absV :  -absV; // Restores a FullRange Value +-
	return(V);
}

Anything less than zero will drive the motors in reverse anything more than zero will drive the motors forward. use separate outputs to switch the direction of your motor.

These are my thoughts

Z

I'm using 4 motors simply due to it being a square base which weighs a lot. 4 motors will be needed to lift it and since they can only be mounted at the corners, 4 are needed to have a stable configuration.

zhomeslice:
with 4 actuators can you have a contact on the foot to detect contact with the ground?
2 actuators attached to PID control the other 2 linked to the foot contact sensors on all feet. if a foot lifts off the actuator needs to extend further even if it offsets the balance which the PID loop will compensate.

Yes, that would provide the data on whether or not the leg is in contact with the ground so might work. I was just trying to do it in the simplest way possible.

With regards to integration, isn't it needed? Since the actuators' are slow and the Integration value is what causes a greater correction as it looks at past error values and if they aren't decreasing at a good rate, it increases the correction. This was my understanding, please let me know if I'm wrong.

zhomeslice:
4) the motors don't start turning until PWM is decently high. we will need to skip over this dead area and immediately switch from forward to reverse without having to go all the way through zero.
This is how I insert a dead zone. the output from the PID is a number from -500 to +500 the output from the Deadzone function is a number from -255 to +255 use analogWrite(abs(output)); to adjust the PWM for the power of the motor.

int16_t Deadzone(int16_t V, uint16_t deadzone){
int16_t absV = abs(V);
absV = constrain(map(absV, 0, 500, deadzone ,255),0,255) ; // Inserts a Deadzone
V = (V >= 0) ? absV :  -absV; // Restores a FullRange Value +-
return(V);

}




Anything less than zero will drive the motors in reverse anything more than zero will drive the motors forward. use separate outputs to switch the direction of your motor.

I have already accounted for this by setting the PID output limits between 223 and 255 in the 3rd and 4th last lines within the setup(). I used 225 to 255 to be the functional range and anything < 255 gets set to 0 so as to not stall/drift the actuators unnecessarily.

If you have two actuators , one moving and the other following under PID control , then the following motor needs to be faster than the other ( X2?) otherwise it will always lag and control won’t be possible.

hammy:
If you have two actuators , one moving and the other following under PID control , then the following motor needs to be faster than the other ( X2?) otherwise it will always lag and control won’t be possible.

Yes, this is absolutely the case and I have it so that faster motor in the pair is the one controlled by PID. Do you believe the magnitude of difference in the maximum speeds of both actuators isn't high enough for the PID controlled actuator to catch up?

If so, can this be addressed by making the uncontrolled motor to raise at a slower speed rather than it's maximum speed?

You need to look at what is happening when it’s running to judge it . I’d go for twice as quick as a starter.
Yes you could slow down the lead motor or measure the error between the two and if above a certain value stop the lead motor until it catches up.

If it doesn’t slow things up too much print out the PID calculations as it runs whilst experimenting

hammy:
You need to look at what is happening when it’s running to judge it . I’d go for twice as quick as a starter.
Yes you could slow down the lead motor or measure the error between the two and if above a certain value stop the lead motor until it catches up.

If it doesn’t slow things up too much print out the PID calculations as it runs whilst experimenting

Ok, I'll try that. Out of curiosity, if you had 4 sensor inputs to control a single system how would you go about implementing a PID for that?

For example, if I had a VL530X to measure the distance (length of the actuator) at each actuator (so 4 in total). How can I use data from all these 4 sensors in a PID as all 4 actuators affect the same system (the base)?

This might not be the best solution, but if it works it will certainly be the easiest: What if you just wired all the motors in parallel? Then you wouldn't have to include PID controllers for platform stabilization.

Assuming the electrical/power attributes of all the motors are nearly identical (which wouldn't be a bad assumption IMHO), then the speed of all motors will be exactly the same and the platform will remain level. You just have to make sure the platform is level when the device is built/initially turned on.

Power_Broker:
This might not be the best solution, but if it works it will certainly be the easiest: What if you just wired all the motors in parallel? Then you wouldn't have to include PID controllers for platform stabilization.

Assuming the electrical/power attributes of all the motors are nearly identical (which wouldn't be a bad assumption IMHO), then the speed of all motors will be exactly the same and the platform will remain level. You just have to make sure the platform is level when the device is built/initially turned on.

The mars model on top of the base is an uneven load so the motor's don't rise at the same rate due to different loading on each actuator. There are also noticeable differences in motor performance under no load.