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:

Proportional - the "P" element of the PID controller (more on this later)

Integral - the "I" element of the PID controller (more later)

Derivative - the "D" element of the PID controller (more later)

Process Variable - the controllable variable that affects the output

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:

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.

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.

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.

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...

Share

About the Author

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.

it would become even better if you would mention some of the issues with using .NET for process control (lack of determinism, for instance), impact of garbage collection (possible interruption at a critical juncture), etc.

You'd be amazed at what kinds of applications are written in interpreted languages, I wouldn't do this for an application like an aircraft auto-pilot, but the lack of determinism really doesn't weight heavily when the computer is put in the right situation. For example, using a dedicated server and adjusting the GC properties to run in foreground mode (this is done through the control panel), and selecting only critical windows components to run (you don't even need the UI), you can get an application written in .NET that rivals those written on embedded hardware in both speed and reliability. You can even adjust kernel pre-empting or run RTLinux and Mono on something like a GumStix and you have a real embedded control system.

I didn't mention determinism, or get too deep into things like tuning the GC because I wanted the article to focus on the implementation of a PID controller, the surrounding application architecture is up to the implementor.

As an introduction to PID, the article is fine. I personally think C# is a crappy language, especially applied to embedded systems. In many cases, PID will be put on an embedded controller way down at the device level and you don't want to be squandering resources for the .NET crud. Of course, Java was originally created for embedded systems and was going nowhere until Sun got the bright idea to promote it as the "internet language".

You measure democracy by the freedom it gives its dissidents, not the freedom it gives its assimilated conformists.

In many cases, PID will be put on an embedded controller way down at the device level and you don't want to be squandering resources for the .NET crud.

The shorter the time between the PID loop making a decision and seeing the effects thereof, the better the loop will run. In cases where the physical response time is minimal, pushing the PID loop as close to hardware as possible will often be helpful. In one project, when my main loop got so bogged down I couldn't run it 100x/second, I made the mistake of dropping to 50Hz rather than working to optimize code so I could keep it at 100Hz. I was eventually able to re-tune the application so it still worked acceptably, but it never worked nearly as well as it had at 100Hz, and the if I'd spent on optimization as much time I as spent re-tuning for 50Hz I probably could have met the 10ms execution target.

That having been said, with things like temperature control, physical actions take long enough that even if the PID loop were to run 1,000,000x/second there's still a substantial lag between action and response. The bigger issue with slower loops is knowing when actions and measurements take place. If readings are normally taken 10x/second but garbage-collection delays a reading by 200ms, the apparent delta from that reading will be three times as large as it should; if this isn't accounted for in code, the system may emit a wacky action request.

BTW, it seems to me that a PID loop is just a particular case of a process loop with a number of state variables where, on each step, state 0 is loaded with the current input, and each other state variable is bumped by a configurable fraction of the difference between its present variable and the next lower-order state variable. The output function is the sum of the products of of each state variable multiplied by a configurable factor.

For a PID loop, the fraction for state 0->1 is near unity, and for 1->2 is very small. The difference between states 0 and 1 is the 'D' term, and the value of state 2 is the I term. State 0 or 1 may be used as the P term.

Using more terms would seem to provide a better representation of the system's current state, and thus allow a better computation of what needs to be done about it.

I sort of look at PID as being similar to the predictor-corrector methods in numerical solutions to differential equations. The primary predictor is the proportional term and the correctors are the derivative and integral. The derivative tries to correct for overshoot and the integral tidies up the steady state.

I guess part of it depends on which formulation you saw or saw first. One keeps each of the three terms with their constants separate. The other, called "standard" I think, has the derivative and integral constants and the proportional constant is unity. You sum the three terms that way and then apply the proportional constant as an overall constant on the while thing. Mostly algebraic manipulation but I guess it can affect one's perspective on the process.

You measure democracy by the freedom it gives its dissidents, not the freedom it gives its assimilated conformists.

I think the article is useful for showing how PID controllers work but I agree that I would never use .NET to execute the loop on a PC.

I've written several .NET applications that control manufacturing hardware (e.g., SW to control DNA microarray wash stations ... controls up to 4 stations simultaneously along with one common temperature controller, SW to calibrate and test microarray laser scanner systems, etc). The .NET SW controlled non-real time systems by communicating with micro-controllers that implemented the PID loops (my SW simply issued the commands to the controllers). I think this is more common in practice for .NET developers of non-real time PC host systems but it is always good to understand PID.

I've seen a few PID loops implemented by engineers with just the P-term set (I and D set to zero) and them wondering why the output never equals the command. Proportional only control will always give steady-state offset error (unless you had infinite gain). Setting the PID gains is an art and some commonly used guidelines are the "Ziegler-Nichols Rules" for processes where accurate transfer function models cannot be found.

Ziegler-Nichols is always the first place to start. If you can not apply a step function or an impulse to the input of the system, then your system is probably unstable. Thank you for writing an article that I can point my bosses to, and making it simple enough for them to actually comprehend.