10
\$\begingroup\$

Short Question
Is there a common way to handle very large anomalies (order of magnitude) within an otherwise uniform control region?

Background
I am working on a control algorithm that drives a motor across a generally uniform control region. With no / minimal loading the PID control works great (fast response, little to no overshoot). The issue I'm running into is there will usually be at least one high load location. The position is determined by the user during installation, so there is no reasonable way for me to know when / where to expect it.

When I tune the PID to handle the high load location, it causes large over shoots on the non-loaded areas (which I fully expected). While it is OK to overshoot mid travel, there are no mechanical hard stops on the enclosure. The lack of hardstops means that any significant overshoot can / does cause the control arm to be disconnected from the motor (yielding a dead unit).

Things I'm Prototyping

  • Nested PIDs (very agressive when far away from target, conservative when close by)
  • Fixed gain when far away, PID when close
  • Conservative PID (works with no load) + an external control that looks for the PID to stall and apply additional energy until either: the target is achieved or rapid rate of change is detected (ie leaving the high load area)

Limitations

  • Full travel defined
  • Hardstops cannot be added (at this point in time)
  • Error will likely never zero out
  • The high load could have be obtained from a less than 10% travel (meaning no "running start")
\$\endgroup\$
2
\$\begingroup\$

Your error calculation does not seem to accumulate error when working with the derivative term, and you may want to modify this since only derivative term is able to react to fast changes in the process.

If I got it right your code

// Determine the error delta
dE = abs(last_error - new_error);
last_error = new_error;

will always calculate control term based on the current error, which is the traditional way how the PID was implemented. It is fine since the I term is supposed to take care of accumulated error anyway.

However, I had a customer who came up with the following idea you might want to try out. Since you have a part of the process curve where more aggressive changes are needed, you can let even the the D part error to accumulate:

if(TD)                                                 // Calculate D term
{  
   Last_C += (Error - Last_C) / TD;                    // D term simulates
   Dterm = (Error - Last_C) * KD;                      // capacitor discharging
}
else    
   Dterm = 0;                                          // D term is OFF (TD is 0)

There are two interesting things to note here:

  • The TD value is not the derivative gain (which is KD) but the derivative time, a user constant which controls the time for error to accumulate. If it was set to zero, the D part of the PID is disabled disregarding the KD gain value set.

  • Note how the current error was used to 'charge' the Last_C value before taking it over to the D part calculation. The Last_C variable is acting like a capacitor, it would build up while the error was large, so that your derivative part would act based also on a recent 'history' of the error, and after that (when error was smaller) this 'history' will discharge like a capacitor.

Of course, you should limit the total output the way you probably already do (anti windup reset, bumpless auto to manual transfer, and other usual stuff).

I can post more details about other terms of my PID algorithm if you find it useful, but you might want to try this and see what happens. It served my customer well for years.

\$\endgroup\$
  • \$\begingroup\$ Thanks for the input. I'll have to give this a try. At a quick glance it seems to make sense. \$\endgroup\$ – Adam Lewis Mar 12 '13 at 16:37
  • \$\begingroup\$ I see, so you have a D term contribution within your 'main' PID plus whatever stall detection brings to the calculation. \$\endgroup\$ – Drazen Cika Mar 12 '13 at 16:44
  • 1
    \$\begingroup\$ That's correct. The PID's Dterm is used, tho not very aggressive, during tuning. What makes this issue even more difficult is that the load can break free in a very short time span. IE a linkage being disengaged. This abrupt removal of force causes large overshoots when there is any smoothing function (summation) applied to the stall forces. \$\endgroup\$ – Adam Lewis Mar 12 '13 at 20:09
  • \$\begingroup\$ Wicked problem, it would be interesting to know how well would some fuzzy logic algorithm handle this. At least you could build more of your problem related experience into the algorithm, instead of staying confined within the standard solutions. Anyway, good luck with this :-) \$\endgroup\$ – Drazen Cika Mar 13 '13 at 8:28
1
\$\begingroup\$

Initial Solution

stalled_pwm_output = PWM / | ΔE |

PWM = Max PWM value
ΔE = last_error - new_error

The initial relationship successfully ramps up the PWM output based on the lack of change in the motor. See the graph below for the sample output.

This approach makes since for the situation where the non-aggressive PID stalled. However, it has the unfortunate (and obvious) issue that when the non-aggressive PID is capable of achieving the setpoint and attempts to slow, the stalled_pwm_output ramps up. This ramp up causes a large overshoot when traveling to a non-loaded position.

1/ΔE vs ΔE

Current Solution

Theory

stalled_pwm_output = (kE * PID_PWM) / | ΔE |

kE = Scaling Constant
PID_PWM = Current PWM request from the non-agressive PID
ΔE = last_error - new_error

My current relationship still uses the 1/ΔE concept, but uses the non-aggressive PID PWM output to determine the stall_pwm_output. This allows the PID to throttle back the stall_pwm_output when it starts getting close to the target setpoint, yet allows 100% PWM output when stalled. The scaling constant kE is needed to ensure the PWM gets into the saturation point (above 10,000 in graphs below).

Pseudo Code

Note that the result from the cal_stall_pwm is added to the PID PWM output in my current control logic.

int calc_stall_pwm(int pid_pwm, int new_error)
{
    int ret = 0;
    int dE = 0;
    static int last_error = 0;
    const int kE = 1;

    // Allow the stall_control until the setpoint is achived
    if( FALSE == motor_has_reached_target())
    {
        // Determine the error delta
        dE = abs(last_error - new_error);
        last_error = new_error;

        // Protect from divide by zeros
        dE = (dE == 0) ? 1 : dE;

        // Determine the stall_pwm_output
        ret = (kE * pid_pwm) / dE;
    }

    return ret;
}

Output Data

Stalled PWM Output Stalled PWM Output

Note that in the stalled PWM output graph the sudden PWM drop at ~3400 is a built in safety feature activated because the motor was unable to reach position within a given time.

Non-Loaded PWM Output No-Load PWM Output

\$\endgroup\$
1
\$\begingroup\$

You don't say what it is you are controlling ... motor speed? position? Well whatever it is, the first step would be to define what an acceptable error would be. For example if the control is for speed a max error of within 1% of target could be set. Without defining the acceptable error you can not determine how much resolution you need for the ADCs or PWM count. Without that, the PID compensation could be perfect, but would still have limit cycle oscillations.

Then you need to know the dynamics of the open loop system. Without that you can not know what gains are needed for the proportional (P), integral (I), and derivative (D) parts of the loop. You can measure the dynamic with input step (step change in drive level or PWM), or step changes in load (seems like this would be relevant to you).

Using the cycle-to-cycle error change, in the denominator of your control algo, to modify the PWM value ensures that the loop will never settle out. This ensures a limit cycle oscillation in the control. Most customers wouldn't put up with that.

The P part of the loop takes care of the immediate error (responds to an error promptly). But it will have finite gain so some error will be left. The I part of the loop reacts slowly over time to apply infinite gain (infinite time for infinite gain) to correct that error which was left over by the P part.

Since the I part is slow it can get out of phase with the correction needed for error minimization, even if you have the right gain set for it. So, it gets wound up, taking a long time to recover. Or, it is left in opposition to the P part.

The best way to handle wind up is to limit the maximum stored value in the integrator to just a little more than is needed to correct for the proportional error at worst case. If the integrator get out of phase and in opposition to the P apart, the best thing to do is set the integrator value to zero. The algo can be designed to sense this and reset the integrator when necessary.

\$\endgroup\$

Not the answer you're looking for? Browse other questions tagged or ask your own question.