Archive for the ‘PID’ Category

Proportional on Measurement – The Code

Tuesday, June 20th, 2017

In the previous post I spent all my time explaining the benefits of Proportional on Measurement. In this post I’ll explain the code. People seemed to appreciate the step-by-step way I explained things last time, so that’s what I’ll do here. The 3 passes below detail how I went about adding PonM to the PID library.

First Pass – Initial Input and Proportional-Mode selection

/*working variables*/
unsigned long lastTime;
double Input, Output, Setpoint;
double ITerm, lastInput;
double kp, ki, kd;
int SampleTime = 1000; //1 sec
double outMin, outMax;
bool inAuto = false;
 
#define MANUAL 0
#define AUTOMATIC 1
 
#define DIRECT 0
#define REVERSE 1
int controllerDirection = DIRECT;
 
#define P_ON_M 0
#define P_ON_E 1
bool pOnE = true;
double initInput;

void Compute()
{
   if(!inAuto) return;
   unsigned long now = millis();
   int timeChange = (now - lastTime);
   if(timeChange>=SampleTime)
   {
      /*Compute all the working error variables*/
      double error = Setpoint - Input;
      ITerm+= (ki * error);
      if(ITerm > outMax) ITerm= outMax;
      else if(ITerm < outMin) ITerm= outMin;
      double dInput = (Input - lastInput);
 
      /*Compute P-Term*/ 
      if(pOnE) Output = kp * error;
      else Output = -kp * (Input-initInput); 

      /*Compute Rest of PID Output*/ 
      Output += ITerm - kd * dInput;
      if(Output > outMax) Output = outMax;
      else if(Output < outMin) Output = outMin;
 
      /*Remember some variables for next time*/
      lastInput = Input;
      lastTime = now;
   }
}
 
void SetTunings(double Kp, double Ki, double Kd, int pOn)
{
   if (Kp<0 || Ki<0|| Kd<0) return;
 
   pOnE = pOn == P_ON_E;
  
   double SampleTimeInSec = ((double)SampleTime)/1000;
   kp = Kp;
   ki = Ki * SampleTimeInSec;
   kd = Kd / SampleTimeInSec;
 
  if(controllerDirection ==REVERSE)
   {
      kp = (0 - kp);
      ki = (0 - ki);
      kd = (0 - kd);
   }
}
 
void SetSampleTime(int NewSampleTime)
{
   if (NewSampleTime > 0)
   {
      double ratio  = (double)NewSampleTime
                      / (double)SampleTime;
      ki *= ratio;
      kd /= ratio;
      SampleTime = (unsigned long)NewSampleTime;
   }
}
 
void SetOutputLimits(double Min, double Max)
{
   if(Min > Max) return;
   outMin = Min;
   outMax = Max;
 
   if(Output > outMax) Output = outMax;
   else if(Output < outMin) Output = outMin;
 
   if(ITerm > outMax) ITerm= outMax;
   else if(ITerm < outMin) ITerm= outMin;
}
 
void SetMode(int Mode)
{
    bool newAuto = (Mode == AUTOMATIC);
    if(newAuto == !inAuto)
    {  /*we just went from manual to auto*/
        Initialize();
    }
    inAuto = newAuto;
}
 
void Initialize()
{
   lastInput = Input;
   initInput = Input;
   ITerm = Output;
   if(ITerm > outMax) ITerm= outMax;
   else if(ITerm < outMin) ITerm= outMin;
}
 
void SetControllerDirection(int Direction)
{
   controllerDirection = Direction;
}

Proportional on Measurement provides increasing resistance as the input changes, but without a frame of reference our performance would be a little wonky. If the PID Input is 10000 when we first turn on the controller, do we really want to start resisting with Kp*10000? No. We want to use our initial input as a reference point (line 108,) the increase or decrease resistance as the input changes from there (line 38.)

The other thing we need to do is allow the user to select whether they want to do Proportional on Error or Measurement. After the last post it might seem like PonE is useless, but it’s important to remember that for many loops it works well. As such, we need to let the user choose which mode they want (lines 51&55) and then act accordingly in the calculation (lines 37&38).

Second Pass – On-the-fly tuning changes

While the code above does indeed work, it has a problem that we’ve seen before. When tuning parameters are changed at runtime, we get an undesirable blip.

PonM-blip

Why is this happening?

PonM-blip-math

The last time we saw this, it was that the integral was being rescaled by a new Ki. This time, it’s that (Input – initInput) is being rescaled by Kp. the solution I chose is similar to what I did for Ki: instead of treating the Input – initInput as a monolithic unit multiplied by the current Kp, I broke it into individual steps multiplied by the Kp at that time:

PonM expansion

/*working variables*/
unsigned long lastTime;
double Input, Output, Setpoint;
double ITerm, lastInput;
double kp, ki, kd;
int SampleTime = 1000; //1 sec
double outMin, outMax;
bool inAuto = false;
 
#define MANUAL 0
#define AUTOMATIC 1
 
#define DIRECT 0
#define REVERSE 1
int controllerDirection = DIRECT;
 
#define P_ON_M 0
#define P_ON_E 1
bool pOnE = true;
double PTerm;

void Compute()
{
   if(!inAuto) return;
   unsigned long now = millis();
   int timeChange = (now - lastTime);
   if(timeChange>=SampleTime)
   {
   
      /*Compute all the working error variables*/	 	
      double error = Setpoint - Input;	 
      ITerm+= (ki * error);	 
      if(ITerm > outMax) ITerm= outMax;	 	
      else if(ITerm < outMin) ITerm= outMin;  
      double dInput = (Input - lastInput);

   	  /*Compute P-Term*/ 
   	  if(pOnE) Output = kp * error; 
   	  else 
   	  { 
   	     PTerm -= kp * dInput; 
   	     Output = PTerm; 
   	  } 
   	  
   	  /*Compute Rest of PID Output*/ 
   	  Output += ITerm - kd * dInput; 
   
      if(Output > outMax) Output = outMax;
      else if(Output < outMin) Output = outMin;
 
      /*Remember some variables for next time*/
      lastInput = Input;
      lastTime = now;
   }
}
 
void SetTunings(double Kp, double Ki, double Kd, int pOn)
{
   if (Kp<0 || Ki<0|| Kd<0) return;
 
   pOnE = pOn == P_ON_E;
  
   double SampleTimeInSec = ((double)SampleTime)/1000;
   kp = Kp;
   ki = Ki * SampleTimeInSec;
   kd = Kd / SampleTimeInSec;
 
  if(controllerDirection ==REVERSE)
   {
      kp = (0 - kp);
      ki = (0 - ki);
      kd = (0 - kd);
   }
}
 
void SetSampleTime(int NewSampleTime)
{
   if (NewSampleTime > 0)
   {
      double ratio  = (double)NewSampleTime
                      / (double)SampleTime;
      ki *= ratio;
      kd /= ratio;
      SampleTime = (unsigned long)NewSampleTime;
   }
}
 
void SetOutputLimits(double Min, double Max)
{
   if(Min > Max) return;
   outMin = Min;
   outMax = Max;
 
   if(Output > outMax) Output = outMax;
   else if(Output < outMin) Output = outMin;
 
   if(ITerm > outMax) ITerm= outMax;
   else if(ITerm < outMin) ITerm= outMin;
}
 
void SetMode(int Mode)
{
    bool newAuto = (Mode == AUTOMATIC);
    if(newAuto == !inAuto)
    {  /*we just went from manual to auto*/
        Initialize();
    }
    inAuto = newAuto;
}
 
void Initialize()
{
   lastInput = Input;
   PTerm = 0;
   ITerm = Output;
   if(ITerm > outMax) ITerm= outMax;
   else if(ITerm < outMin) ITerm= outMin;
}
 
void SetControllerDirection(int Direction)
{
   controllerDirection = Direction;
}

Instead of multiplying the entirety of Input-initInput by Kp, we now keep a working sum, PTerm. At each step we multiply just the current input change by Kp and subtract it from PTerm (line 41.) Here we can see the impact of the change:

PonM-no-blip

PonM-no-blip-math

Because the old Kps are “in the bank”, the change in tuning parameters only affects us moving forward

Final Pass – Sum problems.

I won’t go into complete detail (fancy trends etc) as to what wrong with the above code. It’s pretty good, but there are still major issues with it. For example:

  1. Windup, sort of: While the final output is limited between outMin and outMax, it’s possible for PTerm to grow when it shouldn’t. It wouldn’t be as bad as integral windup, but it still wouldn’t be acceptable
  2. On-the-fly changes: If the user were to change from P_ON_M to P_ON_E while running, then after some time return back, PTerm wouldn’t be property initialized and it would cause an output bump

There are more, but just these are enough to see what the real issue is. We’ve dealt with all of these before, back when we created ITerm. Rather than go through and re-implement the same solutions for PTerm, I decided on a more aesthetically-pleasing solution.

By merging PTerm and ITerm into a single variable called “outputSum”, the P_ON_M code then benefits from the all the ITerm fixes that are already in place, and because there aren’t two sums in the code there isn’t needless redundancy.

/*working variables*/
unsigned long lastTime;
double Input, Output, Setpoint;
double outputSum, lastInput;
double kp, ki, kd;
int SampleTime = 1000; //1 sec
double outMin, outMax;
bool inAuto = false;
 
#define MANUAL 0
#define AUTOMATIC 1
 
#define DIRECT 0
#define REVERSE 1
int controllerDirection = DIRECT;
 
#define P_ON_M 0
#define P_ON_E 1
bool pOnE = true;


void Compute()
{
   if(!inAuto) return;
   unsigned long now = millis();
   int timeChange = (now - lastTime);
   if(timeChange>=SampleTime)
   {
   
      /*Compute all the working error variables*/	 	
      double error = Setpoint - Input;	 
      double dInput = (Input - lastInput);
      outputSum+= (ki * error);	 
	  
	  /*Add Proportional on Measurement, if P_ON_M is specified*/
	  if(!pOnE) outputSum-= kp * dInput
	  
      if(outputSum > outMax) outputSum= outMax;	 	
      else if(outputSum < outMin) outputSum= outMin;  
   	
   	  /*Add Proportional on Error, if P_ON_E is specified*/ 
	  if(pOnE) Output = kp * error; 
	  else Output = 0;
   	  
   	  /*Compute Rest of PID Output*/ 
   	  Output += outputSum - kd * dInput; 
   
      if(Output > outMax) Output = outMax;
      else if(Output < outMin) Output = outMin;
 
      /*Remember some variables for next time*/
      lastInput = Input;
      lastTime = now;
   }
}
 
void SetTunings(double Kp, double Ki, double Kd, int pOn)
{
   if (Kp<0 || Ki<0|| Kd<0) return;
 
   pOnE = pOn == P_ON_E;
  
   double SampleTimeInSec = ((double)SampleTime)/1000;
   kp = Kp;
   ki = Ki * SampleTimeInSec;
   kd = Kd / SampleTimeInSec;
 
  if(controllerDirection ==REVERSE)
   {
      kp = (0 - kp);
      ki = (0 - ki);
      kd = (0 - kd);
   }
}
 
void SetSampleTime(int NewSampleTime)
{
   if (NewSampleTime > 0)
   {
      double ratio  = (double)NewSampleTime
                      / (double)SampleTime;
      ki *= ratio;
      kd /= ratio;
      SampleTime = (unsigned long)NewSampleTime;
   }
}
 
void SetOutputLimits(double Min, double Max)
{
   if(Min > Max) return;
   outMin = Min;
   outMax = Max;
 
   if(Output > outMax) Output = outMax;
   else if(Output < outMin) Output = outMin;
 
   if(outputSum > outMax) outputSum= outMax;
   else if(outputSum < outMin) outputSum= outMin;
}
 
void SetMode(int Mode)
{
    bool newAuto = (Mode == AUTOMATIC);
    if(newAuto == !inAuto)
    {  /*we just went from manual to auto*/
        Initialize();
    }
    inAuto = newAuto;
}
 
void Initialize()
{
   lastInput = Input;
   
   outputSum = Output;
   if(outputSum > outMax) outputSum= outMax;
   else if(outputSum < outMin) outputSum= outMin;
}
 
void SetControllerDirection(int Direction)
{
   controllerDirection = Direction;
}

And there you have it. The above functionality is what is now present in v1.2.0 of the Arduino PID.

But wait, there’s more: Setpoint Weighting.

I didn’t add the following to the Arduino library code, but this is a feature that might be of some interest if you want to roll your own. Setpoint Weighting is, at it’s core, a way to have both PonE and PonM at the same time. By specifying a ratio between 0 and 1, you can have 100% PonM, 100% PonE (respectively) or some ratio in between. This can be helpful if you have a process that’s not perfectly integrating (like a reflow oven) and want to account for this.

Ultimately I decided not to add it to the library at this time, as it winds up being ANOTHER parameter to adjust/explain, and I didn’t think the resulting benefit was worth it. At any rate, here is the code if you’d like to modify the code to have Setpoint Weighting instead of just pure PonM/PonE selection:

/*working variables*/
unsigned long lastTime;
double Input, Output, Setpoint;
double outputSum, lastInput;
double kp, ki, kd;
int SampleTime = 1000; //1 sec
double outMin, outMax;
bool inAuto = false;
 
#define MANUAL 0
#define AUTOMATIC 1
 
#define DIRECT 0
#define REVERSE 1
int controllerDirection = DIRECT;
 
#define P_ON_M 0
#define P_ON_E 1
bool pOnE = true, pOnM = false;
double pOnEKp, pOnMKp;


void Compute()
{
   if(!inAuto) return;
   unsigned long now = millis();
   int timeChange = (now - lastTime);
   if(timeChange>=SampleTime)
   {
   
      /*Compute all the working error variables*/	 	
      double error = Setpoint - Input;	 
      double dInput = (Input - lastInput);
      outputSum+= (ki * error);	 
	  
	  /*Add Proportional on Measurement, if P_ON_M is specified*/
	  if(pOnM) outputSum-= pOnMKp * dInput
	  
      if(outputSum > outMax) outputSum= outMax;	 	
      else if(outputSum < outMin) outputSum= outMin;  
   	
   	  /*Add Proportional on Error, if P_ON_E is specified*/ 
	  if(pOnE) Output = pOnEKp * error; 
	  else Output = 0;
   	  
   	  /*Compute Rest of PID Output*/ 
   	  Output += outputSum - kd * dInput; 
   
      if(Output > outMax) Output = outMax;
      else if(Output < outMin) Output = outMin;
 
      /*Remember some variables for next time*/
      lastInput = Input;
      lastTime = now;
   }
}
 
void SetTunings(double Kp, double Ki, double Kd, double pOn)
{
   if (Kp<0 || Ki<0|| Kd<0 || pOn<0 || pOn>1) return;
 
   pOnE = pOn>0; //some p on error is desired;
   pOnM = pOn<1; //some p on measurement is desired;  
  
   double SampleTimeInSec = ((double)SampleTime)/1000;
   kp = Kp;
   ki = Ki * SampleTimeInSec;
   kd = Kd / SampleTimeInSec;
 
  if(controllerDirection ==REVERSE)
   {
      kp = (0 - kp);
      ki = (0 - ki);
      kd = (0 - kd);
   }
   
   pOnEKp = pOn * kp; 
   pOnMKp = (1 - pOn) * kp;
}
 
void SetSampleTime(int NewSampleTime)
{
   if (NewSampleTime > 0)
   {
      double ratio  = (double)NewSampleTime
                      / (double)SampleTime;
      ki *= ratio;
      kd /= ratio;
      SampleTime = (unsigned long)NewSampleTime;
   }
}
 
void SetOutputLimits(double Min, double Max)
{
   if(Min > Max) return;
   outMin = Min;
   outMax = Max;
 
   if(Output > outMax) Output = outMax;
   else if(Output < outMin) Output = outMin;
 
   if(outputSum > outMax) outputSum= outMax;
   else if(outputSum < outMin) outputSum= outMin;
}
 
void SetMode(int Mode)
{
    bool newAuto = (Mode == AUTOMATIC);
    if(newAuto == !inAuto)
    {  /*we just went from manual to auto*/
        Initialize();
    }
    inAuto = newAuto;
}
 
void Initialize()
{
   lastInput = Input;
   outputSum = Output;
   if(outputSum > outMax) outputSum= outMax;
   else if(outputSum < outMin) outputSum= outMin;
}
 
void SetControllerDirection(int Direction)
{
   controllerDirection = Direction;
}

Instead of setting pOn as an integer, it now comes in as a double which allows for a ratio (line 58.) In addition to some flags (lines 62&63) weighted Kp terms are computed on lines 77-78. Then on lines 37 and 43 the weighted PonM and PonE contributions are added to the overall PID output.

flattr this!

Introducing Proportional On Measurement

Tuesday, June 20th, 2017

It’s been quite a while, but I’ve finally updated the Arduino PID Library. What I’ve added is a nearly-unknown feature, but one which I think will be a boon to the hobby community. It’s called “Proportional on Measurement” (PonM for short).

Why you should care

process-types

There are processes out there that are known as “Integrating Processes.” These are processes for which the output from the pid controls the rate of change of the input. In industry these comprise a small percentage of all processes, but in the hobby world these guys are everywhere: Sous-vide, linear slide, and 3D printer extruder temperature are all examples of this type of process.

The frustrating thing about these processes is that with traditional PI or PID control they overshoot setpoint. Not sometimes, but always:

PonE-9

This can be maddening if you don’t know about it. You can adjust the tuning parameters forever and the overshoot will still be there; the underlying math makes it so. Proportional on Measurement changes the underlying math. As a result, it’s possible to find sets of tuning parameters where overshoot doesn’t occur:
PonM-9
Overshoot can still happen to be sure, but it’s not unavoidable. With PonM and the right tuning parameters, that sous vide or linear slide can coast right in to setpoint without going over.

So What is Proportional on Measurement?

Similar to Derivative on Measurement, PonM changes what the proportional term is looking at. Instead of error, the P-Term is fed the current value of the PID input.

Proportional on Error: PonE-eqn

Proportional on Measurement: PonM-eqn

Unlike Derivative on Measurement, the impact on performance is drastic. With DonM, the derivative term still has the same job: resist steep changes and thus dampen oscillations driven by P and I. Proportional on Measurement, on the other hand, fundamentally changes what the proportional term does. Instead of being a driving force like I, it becomes a resistive force like D. This means that with PonM, a bigger Kp will make your controller more conservative.

Great. But how does this eliminate overshoot?

To undestand the problem, and fix, it helps to look at the different terms and what they contribute to the overall PID output. Here’s a response to a setpoint change for an integrating process (a sous-vide) using a traditional PID:

PonE-Components

The two big things to notice are:

  • When we’re at setpoint, the I-Term is the only contributor to the overall output.
  • Even though the setpoint is different at the start and end, the output returns to the same value. This value is generally known as the “Balance Point”: the output which results in 0 Input slope. For a sous-vide, this would correspond to just enough heat to compensate for heat-losses to the surroundings.

Here we can see why overshoot happens, and will always happen. When the setpoint first changes, the error present causes the I-Term to grow. To keep the process stable at the new setpoint, the output will need to return to the balance point. The only way for that to happen is for the I-Term to shrink. The only way for THAT to happen is to have negative error, which only happens if you’re above setpoint.

PonM changes the game

Here is the same sous-vide controlled using Proportional on Measurement (and the same tuning parameters):

PonM-Components
Here you should notice that:

  • The P-Term is now providing a resistive force. The higher the Input goes, the more negative it becomes.
  • Where before the P-Term became zero at the new setpoint, it now continues to have a value.

The fact the the P-Term doesn’t return to 0 is the key. This means that the I-Term doesn’t have to return to the balance point on its own. P and I together can return the output to the balance point without the I-Term needing to shrink. Because it doesn’t need to shrink, overshoot isn’t required.

How to use it in the new PID Library

If you’re ready to try Proportional on Measurement, and you’ve installed the latest version of the PID library, setting it up is pretty easy. The primary way to use PonM is to specify it in the overloaded constructor:

Constructor

If you’d like to switch between PonM and PonE at runtime, the SetTunings function is also overloaded:

SetTunings

You only need to call the overloaded method when you want to switch. Otherwise you can use the regular SetTunings function and it will remember your choice.

flattr this!

“Beginner’s PID” Series Translated to Spanish

Sunday, July 29th, 2012


HUGE thanks to Jonathan Moyano who has gone through the trouble of translating the Beginner’s PID series into Spanish! You can see his announcement here and here.

Here’s a direct link to his file. In case that goes down I’ve also set up a local mirror here.

flattr this!

PID Library makes it into (another) successful kickstarter [PID Showcase]

Wednesday, July 4th, 2012

You may remember the espresso machine that was on kickstarter earlier this year. You know, where they asked for $20k and got $369k instead?

Well it looks like the PID Library has made it on to another wild Kickstarter ride. This time it’s with the Nomiku: an ingenious sous-vide solution. I’m a sucker for forehead-smack ideas, and this product is a great one. Rather than sell you an entire bulky unit, the Nomiku a small, easy to store, device that clips to a pot you already have.

The icing on the cake is that I know these guys. I met them at makerfaire last year and they’re great; very passionate about this stuff. Actually, it seemed like they were just passionate. about everything. I’m really glad that this is working out for them!

This is what I love about working on base-level components. I only have to do the work once, and then I get to see it crop up in tons of cool stuff. It makes me proud to know I helped those people along a little. I can only imagine how the Arduino team feels.

flattr this!

Arduino PID Autotune Library

Saturday, January 28th, 2012

At long last, I’ve released an Autotune Library to compliment the Arduino PID Library. When I released the current version of the PID Library, I did an insanely extensive series of posts to get people comfortable with what was going on inside.

While not nearly as in-depth, that’s the goal of this post. I’ll explain what the Autotune Library is trying to accomplish, and how it goes about its business.
(more…)

flattr this!

PID-Controlled Espresso Machine [PID Showcase]

Monday, January 16th, 2012

It is to my great delight that I can confirm that this Kick-ass, Kickstarting, Open Source Espresso Machine is using the Arduino PID Library.

They seem to be using a stand-alone commercial pid controller for at least some of the control. Perhaps this could be replaced with an osPID for a 100% open experience?

flattr this!

Introducing the osPID

Tuesday, January 3rd, 2012

About 5 months ago I sent an out-of-the-blue email to RocketScream electronics asking if they wanted to help me build an open source PID controller. It’s been far more challenging than I expected, but today I get to announce the release of my first open source hardware project: the osPID!

(pause for applause)

(more…)

flattr this!

PID Library moved to GitHub

Wednesday, December 14th, 2011

As per the request by the Arduino Team, I’ve moved the Arduino PID Library to GitHub. I’ve also used this opportunity to:

  • Make the Library Arduino 1.0 compliant
  • Release the Library under a GPLv3 License

The google code site will still be there, but there will no longer be updates.

flattr this!

A Little Teaser

Thursday, September 15th, 2011

I haven’t done many PID posts in the last couple months. Rest assured I haven’t been sleeping on that front. I’ve been working closely with RocketScream on an OSHW project that should be released soon. Here are a couple teaser pictures to (hopefully) get you drooling.

As I post this I’m on my way to Makerfaire NYC. I’ll have this with me if you’d like a closer look.

flattr this!

Sousvide-O-Mator [PID Showcase]

Wednesday, August 17th, 2011

Sous-vide BabyTop

There have been many sous vide projects floating around the tubes recently. The one I’m showcasing here was first brought to my attention by the Adafruit blog. I found out from the developer that he was in the midst of a full write-up, so I held off on posting. Now that the write-up is done, we can see that he’s put together an impressive little project.

Great documentation, excellent implementation of the PID Library, delicious, delicious meat? What’s not to love? I’m also looking forward to subsequent posts, where he promises to show “how to calibrate the beast :)”

flattr this!