11,503,113 members (76,178 online)

# Industrial .NET - PID Controllers

, 29 Dec 2009 CPOL 45.7K 2.7K 46
 Rate this:
Implementing a robust PID controller in .NET.

## Introduction

Often times, .NET isn't realized as an industrial language, that is, one that can be trusted to control critical processes in near real-time performance. This article explains the design of a PID controller in .NET that can be used to control an industrial process.

## PID Controller Basics

A PID controller stands for Proportional, Integral, and Derivative, named after the three basic elements of a PID controller. There are a number of PID controller designs out there, each manufacturer taking a slightly different approach to the design.

First, a quick glossary:

1. Proportional - the "P" element of the PID controller (more on this later)
2. Integral - the "I" element of the PID controller (more later)
3. Derivative - the "D" element of the PID controller (more later)
4. Process Variable - the controllable variable that affects the output
5. Set Point - Desired output value

I will use the old "cruise control" example throughout the article to explain how this works, since a cruise control is the most observable type of PID controller out there.

The proportional term identifies what the PID controller's reaction to the error (difference between set point and output) will be. Think of this as how much gas the cruise controller gives the car for some amount of error.

The integral term is how the PID controller reacts to prolonged periods of error. If we just had a P controller, the car would not accelerate going up hills or into the wind. This is an amount to add to the output per period of error.

Derivative is how much reaction the controller has versus the rate of change of the error. If we just used a PI controller, then the P term would make the speed shoot past the target, the I term would accumulate and pull it back, but it wouldn't "anticipate" approaching the set point, and would shoot past it again.

There is an entire theory behind PID controllers despite their simplicity, so I would suggest popping open Google and searching for PID controllers. You can find different implementations of them for particular situations, and learn about fun things like anti-windup reset.

## Goals and Technologies

The goal of this article is to develop an easy to use PID controller. We will look at a couple different technologies implemented in this article, including delegates, threading, timing, and automation. We will look at how to use delegates so the implementing class doesn't need to worry about "feeding" the PID controller with data, and the PID controller can automatically update the output function.

## Using the Code

First, let's start off looking at the delegate setup. If you don't know what delegates are, they are function pointers that can be passed around and stored, and you use them just like any other function. Our PID controller uses two types of delegates:

```namespace PIDLibrary
{
public delegate double GetDouble();
public delegate void SetDouble(double value);
}```

Here, we identify two delegates, or function pointers, that our application will use. The first one, `GetDouble`, is a function that takes no arguments and returns a `double`. The second one, `SetDouble`, takes a single `double` argument and doesn't return anything.

Now, let's look at the `PID` class:

```public class PID
{
#region Fields

//Gains
private double kp;
private double ki;
private double kd;

//Running Values
private DateTime lastUpdate;
private double lastPV;
private double errSum;

private SetDouble writeOV;

//Max/Min Calculation
private double pvMax;
private double pvMin;
private double outMax;
private double outMin;

private double computeHz = 1.0f;

#endregion

#region Properties

public double PGain
{
get { return kp; }
set { kp = value; }
}

public double IGain
{
get { return ki; }
set { ki = value; }
}

public double DGain
{
get { return kd; }
set { kd = value; }
}

public double PVMin
{
get { return pvMin; }
set { pvMin = value; }
}

public double PVMax
{
get { return pvMax; }
set { pvMax = value; }
}

public double OutMin
{
get { return outMin; }
set { outMin = value; }
}

public double OutMax
{
get { return outMax; }
set { outMax = value; }
}

public bool PIDOK
{
get { return runThread != null; }
}

#endregion

#region Construction / Deconstruction

public PID(double pG, double iG, double dG,
double pMax, double pMin, double oMax, double oMin,
GetDouble pvFunc, GetDouble spFunc, SetDouble outFunc)
{
kp = pG;
ki = iG;
kd = dG;
pvMax = pMax;
pvMin = pMin;
outMax = oMax;
outMin = oMin;
writeOV = outFunc;
}

~PID()
{
Disable();
writeOV = null;
}

#endregion

#region Public Methods

public void Enable()
{
return;

Reset();

}

public void Disable()
{
return;

}

public void Reset()
{
errSum = 0.0f;
lastUpdate = DateTime.Now;
}

#endregion

#region Private Methods

private double ScaleValue(double value, double valuemin,
double valuemax, double scalemin, double scalemax)
{
double vPerc = (value - valuemin) / (valuemax - valuemin);
double bigSpan = vPerc * (scalemax - scalemin);

double retVal = scalemin + bigSpan;

return retVal;
}

private double Clamp(double value, double min, double max)
{
if (value > max)
return max;
if (value < min)
return min;
return value;
}

private void Compute()
{
if (readPV == null || readSP == null || writeOV == null)
return;

//We need to scale the pv to +/- 100%, but first clamp it
pv = Clamp(pv, pvMin, pvMax);
pv = ScaleValue(pv, pvMin, pvMax, -1.0f, 1.0f);

//We also need to scale the setpoint
sp = Clamp(sp, pvMin, pvMax);
sp = ScaleValue(sp, pvMin, pvMax, -1.0f, 1.0f);

//Now the error is in percent...
double err = sp - pv;

double pTerm = err * kp;
double iTerm = 0.0f;
double dTerm = 0.0f;

double partialSum = 0.0f;
DateTime nowTime = DateTime.Now;

if (lastUpdate != null)
{
double dT = (nowTime - lastUpdate).TotalSeconds;

//Compute the integral if we have to...
if (pv >= pvMin && pv <= pvMax)
{
partialSum = errSum + dT * err;
iTerm = ki * partialSum;
}

if (dT != 0.0f)
dTerm = kd * (pv - lastPV) / dT;
}

lastUpdate = nowTime;
errSum = partialSum;
lastPV = pv;

//Now we have to scale the output value to match the requested scale
double outReal = pTerm + iTerm + dTerm;

outReal = Clamp(outReal, -1.0f, 1.0f);
outReal = ScaleValue(outReal, -1.0f, 1.0f, outMin, outMax);

//Write it out to the world
writeOV(outReal);
}

#endregion

private void Run()
{
while (true)
{
try
{
int sleepTime = (int)(1000 / computeHz);
Compute();
}
catch (Exception e)
{

}
}
}

#endregion
}```

You can see that the implementation is rather short and sweet, but we'll take a closer look at how the PID works...

First off, the constructor:

```public PID(double pG, double iG, double dG,
double pMax, double pMin, double oMax, double oMin,
GetDouble pvFunc, GetDouble spFunc, SetDouble outFunc)
{
kp = pG;
ki = iG;
kd = dG;
pvMax = pMax;
pvMin = pMin;
outMax = oMax;
outMin = oMin;
writeOV = outFunc;
}```

It takes quite a few arguments, which I'll explain in detail. The first argument, `pG`, is the proportional gain, which identifies how much output to apply versus the percentage error. The second argument `iG` and the third argument `dG` do the same for the integral and derivative terms, respectively. The next two arguments, `pMax` and `pMin`, identify the process variable maximum and process variable minimum. This isn't to say that the process variable can't go above these values, but it will be clipped in the computation function to be within those extremes. The `oMax` and `oMin` arguments perform a similar action for the output variable.

The next three arguments are the delegates that tell the `PID` controller where it can find the data it needs to be able to process it. `pvFunc` is a function that returns the value of the process variable (thing being measured). `spFunc` returns the value of the set point (what we want the process variable to equal), and `outFunc` tells the PID controller what to call to set the output value.

I'll skip most of the basics of the implementation, like the destructor, properties, public functions, etc.

Let's take a look at the threading loop:

```private void Run()
{
while (true)
{
try
{
int sleepTime = (int)(1000 / computeHz);
Compute();
}
catch (Exception e)
{

}
}
}```

We can see that the loop is very simple; it makes a rough approximation of the amount of time that it needs to sleep (this isn't true time, because we would need to take in the amount of time it takes to run the calculation, but it's close enough, and the `Compute` routine compensates by using the actual time between measurements). All the function does is loop, sleep, and call `Compute`.

```private void Compute()
{
if (readPV == null || readSP == null || writeOV == null)
return;

//We need to scale the pv to +/- 100%, but first clamp it
pv = Clamp(pv, pvMin, pvMax);
pv = ScaleValue(pv, pvMin, pvMax, -1.0f, 1.0f);

//We also need to scale the setpoint
sp = Clamp(sp, pvMin, pvMax);
sp = ScaleValue(sp, pvMin, pvMax, -1.0f, 1.0f);

//Now the error is in percent...
double err = sp - pv;

double pTerm = err * kp;
double iTerm = 0.0f;
double dTerm = 0.0f;

double partialSum = 0.0f;
DateTime nowTime = DateTime.Now;

if (lastUpdate != null)
{
double dT = (nowTime - lastUpdate).TotalSeconds;

//Compute the integral if we have to...
if (pv >= pvMin && pv <= pvMax)
{
partialSum = errSum + dT * err;
iTerm = ki * partialSum;
}

if (dT != 0.0f)
dTerm = kd * (pv - lastPV) / dT;
}

lastUpdate = nowTime;
errSum = partialSum;
lastPV = pv;

//Now we have to scale the output
//value to match the requested scale
double outReal = pTerm + iTerm + dTerm;

outReal = Clamp(outReal, -1.0f, 1.0f);
outReal = ScaleValue(outReal, -1.0f, 1.0f, outMin, outMax);

//Write it out to the world
writeOV(outReal);
}```

The `Compute` routine is where the meat of the algorithm lies. Basically, it starts out reading the process variable (`pv`) and set point (`sp`). It then `Clamp`'s them to `pvMin` and `pvMax`, then scales them so they are a percentage between -100% and 100% of the scale. It then figures out the error percentage and starts running the PID calculation.

The calculation is pretty simple; it starts out finding the `pTerm`, which is the error times the gain (`kp`). Then, inside the `if` statement, we do what is called anti-windup reset, where we only calculate the `iTerm` if the process variable isn't pegged at or above the process variable range. This helps to limit the output of the system, and keeps the error from blowing up when the process variable gets out of range.

The last thing it does is simply sum the three terms to obtain the output value, clamp it to +/-100% of the output range, then scale it to come up with a real output number. It then uses the `SetDouble` delegate called `writeOV` (write output variable) to set the output value.

And there you have it. If we have a more real-time or critical process, we can set the `runThread` priority to something higher, but I wouldn't recommend going above "High" since it will cause other things to become preempted too often.

This is a very versatile class; setting the I gain term to zero will give you a PD controller, and if you wanted, you could have a strictly P controller by setting both the I and D gains to zero.

## Tuning a PID

Tuning a PID controller is beyond the scope of this article; again, the best place to learn about tuning PIDs is Google, just pop open your browser and search for "PID Tuning" or similar terms. There are a lot of interesting properties about PID controllers, and they can be used to perform some pretty amazing and almost intelligent control applications.

## Points of Interest

This PID controller works great for implementing processes that can be modeled linear or near linear, but processes that are a lot more complicated and need a multi-parameter PID. I've implemented PID controllers that use up to 18 terms, with great results, using the same simple framework. Tuning is the hard part...

## History

• 1.0: Initial version.

## Share

President 6D Systems LLC
United States
I studied Software Engineering at Milwaukee School of Engineering for 2 years before switching to Management of Information Systems for a more business oriented approach. I've been developing software since the age of 14, and have waded through languages such as QBasic, TrueBasic, C, C++, Java, VB6, VB.NET, C#, etc. I've been developing professionally since 2002 in .NET.

 First Prev Next
 SetDouble outFunc Member 1109343722-Dec-14 13:42 Member 11093437 22-Dec-14 13:42
 Modify/Use your Code with a project and OpenSource the project under GNU GPL v3.0 License Umer javaid21-Oct-14 2:36 Umer javaid 21-Oct-14 2:36
 Re: Modify/Use your Code with a project and OpenSource the project under GNU GPL v3.0 License Ron Beyer21-Oct-14 4:02 Ron Beyer 21-Oct-14 4:02
 Re: Modify/Use your Code with a project and OpenSource the project under GNU GPL v3.0 License Umer javaid21-Oct-14 5:12 Umer javaid 21-Oct-14 5:12
 An error is hidden in the code winnypolo12-Mar-13 0:55 winnypolo 12-Mar-13 0:55
 Re: An error is hidden in the code JudeC27-Oct-13 23:33 JudeC 27-Oct-13 23:33
 Re: An error is hidden in the code Pera Zdera19-Jun-14 0:18 Pera Zdera 19-Jun-14 0:18
 Re: An error is hidden in the code Member 1122384511-Jan-15 9:26 Member 11223845 11-Jan-15 9:26
 This is wrong GLMnet12-Jun-12 0:06 GLMnet 12-Jun-12 0:06
 ```//Compute the integral if we have to... if (pv >= pvMin && pv <= pvMax)``` pv is scaled and you are comparing it to non scaled??
 Can we include PV Tracking into this code ? bhargav30-May-12 18:20 bhargav 30-May-12 18:20
 My vote of 4 Mohammad Said Hefny10-Mar-12 4:42 Mohammad Said Hefny 10-Mar-12 4:42
 Thanks for easy understandable code :) LastMandg418-Oct-11 10:26 LastMandg4 18-Oct-11 10:26
 Re: Thanks for easy understandable code :) bigbrother2511-May-12 21:54 bigbrother25 11-May-12 21:54
 Nice sam.hill29-Dec-09 15:40 sam.hill 29-Dec-09 15:40
 Intersting article, but Rob Graham29-Dec-09 14:40 Rob Graham 29-Dec-09 14:40
 Re: Intersting article, but Silic0re0929-Dec-09 14:51 Silic0re09 29-Dec-09 14:51
 Re: Intersting article, but Tim Craig29-Dec-09 18:00 Tim Craig 29-Dec-09 18:00
 Re: Intersting article, but supercat930-Dec-09 6:34 supercat9 30-Dec-09 6:34
 Re: Intersting article, but Tim Craig30-Dec-09 8:42 Tim Craig 30-Dec-09 8:42
 Re: Intersting article, but supercat930-Dec-09 10:10 supercat9 30-Dec-09 10:10
 Re: Intersting article, but Tim Craig31-Dec-09 10:13 Tim Craig 31-Dec-09 10:13
 Re: Intersting article, but Atanas Palavrov11-Jan-10 21:35 Atanas Palavrov 11-Jan-10 21:35
 Re: Intersting article, but TonyJ30-Dec-09 11:41 TonyJ 30-Dec-09 11:41
 Finally someone mentions the analytical way to tune PID loops MicroImaging31-Dec-09 18:05 MicroImaging 31-Dec-09 18:05
 Last Visit: 31-Dec-99 18:00     Last Update: 2-Jun-15 18:06 Refresh 1