Please help: how to control a linear actuator with a PID fed by a loadcell?

Hi all!

I have a stepper driven linear actuator (leadscrew type) which pulls a string and brings it up to a pre-defined tension. The tension is measured with a loadcell at the other end of the string. The readings from the loadcell are filtered / smoothed and fed into the PID. The PID outputs speed commands for the stepper such as "increase speed to X rpms", "decrease speed to X rpms", "decrease speed to zero", or "increase speed from zero to X clock wise" etc. These speed commands are transmitted to the stepper via a serial line.

As the tension changes dynamically (though not very fast) I can't estimate the target position of the stepper. The string constantly changes it's elasticity (because it's treated in various ways). The goal is to keep the tension seen by the loadcell constant: just as if the linear actuator was replaced by a weight which stretches the string.

Basically my system works, but I struggle to tune the PID for a smooth response without hunting or overshooting. I am fiddling with the values since quite some time, so I would like to hear your advice: is my approach OK? What filter should I use for smoothing the loadcell readings? How can I try to optimize the PID step by step in the right order?

(EDIT: coding is not my problem at the moment, so I didn't post the code in order to not waste your time with reading / commenting my code...)

It is not so easy to explain in a few lines how to perform the correct parameterization. It may be possible to better understand how the PID works through the documents made available by Microchip. AN937, AN_2558 AVR221, AN964, AN2373 etc.

Microchip: PID

jpk:
(EDIT: coding is not my problem at the moment, so I didn't post the code in order to not waste your time with reading / commenting my code...)

1.) There's no harm in posting it - forum code is always an interesting adventure and sometimes quite entertaining, lol
2.) It's hard to trust people when they say "Hey, can you help me fix my project because it doesn't work and I don't know why - but the code (where most errors/bugs/sources of problems occur) is completely perfect!" Soooo...

Some tips for tuning:
Increase P for faster response time
Increase I for faster settling time
Increase D for better smoothing

THAT BEING SAID: Increasing ANY of the three values by an obscene amount can and will cause your system to become unstable. Do some research on how PID works in theory - Google is your friend.

Hi

Depends a little on the PID controller but one quick and dirty way is to have no integral or derivative and increase the gain until the system oscillates

Reduce the gain by half and then add the reset time as the oscillation period

The classic response of a well tuned PID loop is oscillations that are initially about a 1/3 of the step change and then successive 1/3 under and overshoots.

I must admit i never use differential for non complex things and usually over damp rather than have overshoots

Thanks for the input! Of course I studied lot's of tutorials on how to tune a PID, but...

RodMcM:
The classic response of a well tuned PID loop is oscillations that are initially about a 1/3 of the step change and then successive 1/3 under and overshoots.

Thanks, I will try that! But before I start fiddling with the parameters again I need to understand if generally my approach (as described in my first post) will do...

Power_Broker:
There's no harm in posting it

OK here is the code:

#include <HX711_ADC.h>
#include <HampelFilter.h>
#include <PID_v1.h>

HX711_ADC LoadCell(0, 1);

double Setpoint, Input, Output;

float Kp = 6.5f;
float Ki = 0.1f;
float Kd = 0.9f;

PID myPID(&Input, &Output, &Tune, Kp, Ki, Kd, REVERSE);

double pid_out_limit;
double tension_over_kg = 0.8L;

HampelFilter scaleHampel_slow = HampelFilter(0.00, 15, 0.02);   // only odd numbers as window size
HampelFilter scaleHampel_fast = HampelFilter(0.00, 5, 0.02);

bool holdtension;
bool stepper_start_dir;
double stepper_rpm_factor; // for changing stepper speed on the fly
int pid_auto_stop;
bool override_flag_stepper = 0;
int countdown = -1; // this is needed for a stopwatch like function...
double stepper_step_frequency_max = 7000.0L;
double stepper_rpm_factor_old;

// values for driving the stepper: max = pulse frequency, acc = acceleration, and there are slow and fast modes in this project...
double maxT;
int maxT_fast, maxT_slow = 100;
int accT, accT_fast, accT_slow;

elapsedMillis scale_timestamp = 0;
uint64_t scale_interval;

double kilogramm; // tension in Kg
double kilogramm_dec;
double kilogramm_old = 999.9L; // the value is dummy in order to display initial 0Kg
double kilogramm_max;
bool scale_highspeed = 1; // set scale speed mode

void setup() {
  myPID.SetSampleTime(30);
  myPID.SetMode(AUTOMATIC);
  pid_out_limit = stepper_step_frequency_max / maxT_slow;
  myPID.SetOutputLimits(-pid_out_limit, pid_out_limit);
}

void loop() {
  LoadCell.update(); // update() should be called at least as often as sample rate: >10Hz@10SPS, >80Hz@80SPS
  if (scale_timestamp >= scale_interval) {
    kilogramm = LoadCell.getData();
    if (scale_highspeed == 1) {scaleHampel_fast.write(kilogramm); kilogramm = scaleHampel_fast.readMedian();}
    else {scaleHampel_slow.write(kilogramm); kilogramm = scaleHampel_slow.readMedian();}
    if (kilogramm < 0) {kilogramm = 0;}
    else {
      kilogramm_dec = kilogramm;
      if (kilogramm_dec > kilogramm_max) {kilogramm_max = kilogramm_dec;}
      kilogramm = kilogramm + 0.05;
      kilogramm = (int)(kilogramm * 10);
      kilogramm = kilogramm / 10;
    }
    if (kilogramm_old != kilogramm) {
      update_display();
      kilogramm_old = kilogramm;
    }
    scale_timestamp = 0;
  }
  if (holdtension == 1) {hold_tension();}
}

void hold_tension_start() { // if I press a button this will be called
  holdtension = 1;
  Setpoint = kilogramm_dec;
  maxT = round(maxT_slow);
  accT = accT_fast;
  if (kilogramm_dec > tension_over_kg) {hold_tension();}
}

void hold_tension() {
  Input = kilogramm_dec - Setpoint;
  if (myPID.Compute()) {
    if (override_flag_stepper == 0) {
      if (Output < 0) {stepper_start_dir = 0;} else {stepper_start_dir = 1;}
      move_stepper(stepper_start_dir);
      override_flag_stepper = 1;
    }
    else {
      // char text1[30]; char text2[30]; dtostrf(Input, 10, 10, text1); dtostrf(Output, 10, 10, text2); char text[65]; snprintf(text, 65, "%s,0,%s", text1, text2); debug->println(text);
      if (stepper_start_dir == 0) {stepper_rpm_factor = -Output;}
      else {stepper_rpm_factor = Output;}
      if (countdown <= 0) {if (abs(Input) < 0.1) {pid_auto_stop++;} else {pid_auto_stop = 0;}}
      if (pid_auto_stop > 100 || kilogramm_dec < tension_over_kg) {hold_tension_stop();} else {
        if (stepper_rpm_factor != stepper_rpm_factor_old) {
          stepper_rpm_factor_old = stepper_rpm_factor;
          update_stepper_speed(); // can even change dir if value changes sign
        }
      }
    }
  }
}

void hold_tension_stop() {
  holdtension = 0;
  override_flag_stepper = 0;
  pid_auto_stop = 0;
  stop_stepper(); // stops motor after target tension is reached
}

There are other filters, for example the Exponential Filter.

Also there are similar questions in various forums, but I didn't find much help reading them. The best source I found at Robotics Stack Exchange.

RodMcM:
The classic response of a well tuned PID loop is oscillations that are initially about a 1/3 of the step change

WHOAH, 33% overshoot? Depending on the application, that's crazy talk...

4% overshoot is probably a better target for most PID applications

I did 2 PID projects that generated too much overshot, because of the very long response time, so I left the setpoint below the desired setpoint so that no overshot would happen. After the setpoint is reached, then the setpoint is set again to the final value.

Thanks rtek1000, that's an interesting approach, but I am not sure if I got what you mean, so I include some pseudo code:

desired_setpoint = 100;
setpoint = 90;

while {
  input = sensor();
  if (input > (setpoint-10)) setpoint = desired_setpoint;
  output();
}

Please let me know if that is what you mean!

Another similar approach to avoid integral windup is to reserve PID for when you're close to the set point. If you're trying to heat something and you're way below the setpoint, you know the PID will call for maximum heat for a good while when you start.

So just turn the heat to max and watch the temperature. When you're close enough, switch over to PID control.

jpk:
Hi all!

I have a stepper driven linear actuator (leadscrew type) which pulls a string and brings it up to a pre-defined tension. The tension is measured with a loadcell at the other end of the string. The readings from the loadcell are filtered / smoothed and fed into the PID.

Probably a bad idea - filtering may introduce instability into the loop. Pass raw values to the loop so its not subject to any unnecessary latency(*). The loop itself and the inertia of the motor can act as the low pass filter.

(*) latency in a control loop drectly causes oscillation which can only be cured by either turning down the loop gain, or compensating for the latency with a Smith Predictor or similar.

wildbill:
So just turn the heat to max and watch the temperature. When you're close enough, switch over to PID control.

That's good advice, thanks!

MarkT:
or compensating for the latency with a Smith Predictor or similar

I thought the problem is that the tension constantly changes (not only due to the PID output) so there is not much predictability possible...?

wildbill:
So just turn the heat to max and watch the temperature. When you're close enough, switch over to PID control.

You could also stick with PID control 100% of the time, but have a set max/min value for the integrated error value.

Power_Broker:
You could also stick with PID control 100% of the time, but have a set max/min value for the integrated error value.

Thanks for that suggestion! But I think the heat example is quite different from my situation which I try to discribe:

  • I bring up the tension to a desired value
  • I save this value
  • I start the PID
  • I manipulate the material and thus the tension drifts away from the desired value
  • I want the PID to keep the tension as close as possible to the value saved in 2.
    My goal is to get the PID behaving like a physical weight which pulls the object and keeps the tension constant.

Can you explain the simple mechanics of the system?

Can your lead screw be back driven?

When the desired tension is reached, does the motor stop turning or do you stall?

It seems that if the tension is too high, you are out too far. If it is too low, you are back too much. The actual position of the stepper is not needed. You only have to know if you are near the limits, and simple micro switches can serve that purpose.

I just did something similar with a load cell and actuator. I did not use the PID library. Just took the difference between actual force and desired force and multiplied by a constant.

The stiction in the actuator was easily overcome by adding or subtracting a fixed number depending on the direction required. The actuator would not begin to move with less than 15 PWM signal so I did not send it less than that, unless the drive signal was (almost) zero.

MorganS:
I just did something similar with a load cell and actuator. I did not use the PID library. Just took the difference between actual force and desired force and multiplied by a constant.

That sounds interesting! Do you have some material online such as videos, and/or would you mind to share some code?

MorganS:
The stiction in the actuator was easily overcome by adding or subtracting a fixed number depending on the direction required.

I am not sure which stiction you mean: do you mean the asymmetrical behaviour because the tension "tries" to move the actuator towards the direction of lesser tension?

MorganS:
The actuator would not begin to move with less than 15 PWM signal so I did not send it less than that, unless the drive signal was (almost) zero.

I don't understand fully what you are saying, but as I feed serial commands to my motor your PWM aspect should be of no relevance for my case...?

"Serial commands to the motor"? What motor is this?

MorganS:
"Serial commands to the motor"? What motor is this?

A stepper (as stated in the thread opening post): the stepper has a drive which receives step/dir signals from a separate controller. Through this second controller I can command the motor to execute speed changes and at the same time know it's raw position (i.e. how many pulsed it has moved).

Step and direction is not Serial.

Yes, as a stepper, some of my advice is not relevant.

MikeLittle:
It seems that if the tension is too high, you are out too far. If it is too low, you are back too much. The actual position of the stepper is not needed.

That's good news for me!

MikeLittle:
You only have to know if you are near the limits, and simple micro switches can serve that purpose.

That's exactly how I designed the system!

MikeLittle:
When the desired tension is reached, does the motor stop turning or do you stall?

I can't run it as a torque controlled servo: I have to stop the motor when manipulation of my test-material is finished and the desired tension is reached.

MikeLittle:
Can your lead screw be back driven?

The tension is way too low to move the ballscrew, I can move it only by the motor.

Do you have any advice for me?