Consider a PID controller that needs to regulate the temperature of a small object. This will be implemented in software in a microcontroller.

A thermistor is closely bonded to a heater (a small 3 watt power resistor driven by an 8-bit PWM through a suitable MOSFET.) The system is able to correctly read the temperature and is able to modulate the heating output of the resistor. All the features work in isolation but now I need to bring it all together using a PID controller.

The ultimate output needs to be an integer in the range 0 - 255 for use with the PWM generator, where 0 is the heater turned off and 255 is the heater at maximum output.

It is not clear to me how to scale the values so they fit into 0 - 255. Is that something that the PID math is able to do if you choose the right P, I and D coefficient values, or should I have the PID system output values such as -1 to +1 and then just scale that linearly so it maps to 0 - 255?

Basically the point where the clever math ends and the tangible hardware begins. How do people usually set this up?

I'm after a "best practice" type of answer, or perhaps a rule-of-thumb.

Further information:

The ADC that reads the thermistor is of 10-bit resolution, but I'm taking an average of 25 samples to mitigate noise effects (this sampling is interrupt driven and at a fixed frequency). The averaged temperature is updated at about 4.3 Hz. The heater PWM frequency is about 136 Hz.

It works!

With the accepted answer below I was able to implement a full PID control system. I'm using a few simple checks to ensure that the integral term doesn't become unmanageable: setting the integral to zero when the error is very small and also an upper limit on its absolute value. I'm simply clamping the final PWM value between 0 and 255.

The preliminary PID constants that produced the image below are P=50, I=3 and D=5. My set point is 60 Celsius and the system seems to stabilise at about 59.4 Celsius. It's still a bit sensitive and there is a lot of latency between the heater and the thermistor, but on the whole I'm pretty happy with the outcome. I'll continue to tweak the variables and see how good it can get.

enter image description here


2 Answers 2


A PID controller is linear. As a result, it doesn't make any difference whether you scale the PID coefficients or the output of the PID controller (or even the input for that matter).

For example, if you want the proportional (P) part of the PID controller to have a gain of 100%/10°C (that is, a 100% increase in output for a 10°C decrease in temperature), the proportional gain has to be 25.5/°C so that a change of 10°C causes the output to go from 0 to 255.

The same applies to the integral and differential parts. Note, though, that the sampling rate of your system also influences the gain for these, as they are time-dependent. If you sample more often while keeping the coefficients the same, the integral gain goes up, while the differential gain goes down.

Additionally, for the PID controller to work properly, the sensor must have a surprisingly large resolution. If the resolution is too low, the discrete differentiation becomes noisy and unstable. For example, if you have an 8-bit input value range (0-255) for temperature, and your differential gain is 50, this would mean that a single-digit change in the input temperature will cause the output to swing by 50 units (which is about 20% of your 0-255 range). As a result, the output will constantly jump up and down by 20%, or more if the input is slightly noisy, making the PID controller potentially unusable. It'll be even worse if the resolution of your sensor is even lower (i.e. only whole degrees Celsius).

Ideally, for the differential part of the PID controller to work properly, the input value range should bigger than the output value range by a factor of the differential gain. So, if you have a differential gain of 50 and an output value range of 0-255 (8 bit), the input value range should be 0-12749, or about 14 bits. In practice, you can go a little lower, but not by much before you introduce excessive noise and instability into the system. 12 bits is probably fine in this case, which is a typical resolution of microcontroller ADCs. In practice, you'll have to tune the PID parameters by hand, so the best you can do is to make an educated guess or just use the highest resolution available to you.

Your 10-bit ADC with 25x oversampling gives you a bit more than 2 extra effective bits of resolution, bringing you to that 12 bit figure I mentioned earlier. As a result, you can use differential gains of up to 64 without suffering any ill effects from quantization noise in your discrete differentiator.

The proportional and integral gains are not affected by this effect.

If you want to calculate the integral and differential gains instead of just adjusting them by guessing, you will need to know the time constants of your system (which you can measure by turning the heater on and plotting the resulting values from the temperature sensor). Using this curve, you can estimate the system's frequency response (via a Fourier transform), and then you can calculate a viable frequency response for your PID controller or just tune it in a simulation (with LTSpice, for example).

  • \$\begingroup\$ I've added a bit more information to the Question about my sampling rates and resolution. \$\endgroup\$
    – Wossname
    Commented Mar 18, 2023 at 17:03
  • \$\begingroup\$ Thank you for this detailed answer. There is a lot to mentally process here. I'm going to try to work this all out over the next day or two. Cheers. \$\endgroup\$
    – Wossname
    Commented Mar 18, 2023 at 18:06

I had a look at the source code for the Arduino AutoPID library. I picked it because I've used it before.

They seem to simply clip the output values to the limits given on initialization:

    unsigned long _dT = millis() - _lastStep;   //calculate time since last update
    if (_dT >= _timeStep) {                     //if long enough, do PID calculations
      _lastStep = millis();
      double _error = *_setpoint - *_input;
      _integral += (_error + _previousError) / 2 * _dT / 1000.0;   //Riemann sum integral
      //_integral = constrain(_integral, _outputMin/_Ki, _outputMax/_Ki);
      double _dError = (_error - _previousError) / _dT / 1000.0;   //derivative
      _previousError = _error;
      double PID = (_Kp * _error) + (_Ki * _integral) + (_Kd * _dError);
      //*_output = _outputMin + (constrain(PID, 0, 1) * (_outputMax - _outputMin));
      *_output = constrain(PID, _outputMin, _outputMax);

*_output is the output of the PID algorithm. _outputMin and _outputMax are limits given to the library at initialization. constrain just looks at the value and clips it to the given limits.

It makes sense that way. If you implemented an analog PID controller, it would have to "clip" at the limits of the output voltage source.

  • \$\begingroup\$ Before you use a controller with an integrator whose states are not limited, look up "integrator windup" to see what awaits you. "Pitfall" is a mild term for it. "Pitfall with poisoned stakes at the bottom" is more representative, but does not trip easily off the tongue. \$\endgroup\$
    – TimWescott
    Commented Mar 18, 2023 at 18:45
  • \$\begingroup\$ @TimWescott, I have read about this effect in George Gillard's PDF (c. 2012) tutorial, I'll be wary. \$\endgroup\$
    – Wossname
    Commented Mar 18, 2023 at 20:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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