MicroHTR (Part 6)

Part 1, 2, 3, 4, 5, 6, 7, 8

The heart of MicroHTR is a PID controller which is coded within the PlainPID library. Some refinements have been brought to this library in order to make it easier to use and as versatile as possible. It now features a pretty robust anti-windup function which prevents large overshoots while heating (or actively cooling). This new version of PlainPID features an “advanced” function for computing the derivative component of the PID.

Most PID algorithms compute the signal difference between the actual measurement and the previous one. Although this method is simple it is very sensitive to noisy signal. A better approach consists in computing the slope over a set of points. PlainPID contains the components necessary for computing slope: dynamically set vectors of data (Time and abundance), circular buffers and a a slope calculator. Using 8 data points proved to be optimal in most cases (temperature control) , but you can decide to change this constant directly from the code.

Next are a couple of pictures of the test material that I used while developing this new version of MicroHEATER.

microheater_2

The heating element is made of two power resistors in series, 10 Ohm each, thermally bond to a heat sink by means of thermal grease. The resistors are firmly maintained in contact with the heat sink by means of a twisted piece of wire which applies a constant force.

microheater_1

At the back of the heat sink seats the thermal sensor. I used a TMP04, as in many of my applications. The TO92 package makes it easy to wire and install nearby the area to be measured. I like the ratio-metric output, which is easy to interpret and which is not disturb neither by lengthy cables nor by noisy environmental conditions. On the other hand, it is not cheap and some other sensors offer better performances. The TMP04 is maintained by a metal retainer from my scrap box and coated with thermal grease in order to improve the thermal transfer.

microheater_3

The electronics are pretty simple and mainly consist in a N-Channel MOSFET transistor which acts as a switch. MOSFET feature low to very low ON resistances, thus little power dissipation while driving the load attached to their drains. MOSFET is used in a very simplistic configuration, good enough for the PWM base frequency that we will use. Other components are just ancillaries which objective is to provide real time information about the controller.

microheater_4

Once we have the hardware ready, let’s care about the firmware. Although PlainPID is the heart of the system, we need code for reading data and code for driving the load, respectively PlainTMP0x and PlainPWM. ultimately, the only line of code required for running the whole combination looks like:

PWM.DutyCycle(PID.PIDOutput(TMP.Temperature()));

Well, some initialization is required prior to running this line ! Here are the required lines of code in the setup routine.

/* Temperature sensor settings */
TMP.InitializeTMP(TMP_MOD_TMP04, &PORTB, 0, TMP_UNI_CELSIUS, 8);
/* PWM settings */
PWM.InitializePWM(&PORTB, 1, 4);
/* PID settings */
PID.InitializePID(0.0, 256.0, _Kp, _Ki, _Kd, _smoothingFactor);

And this it ! It might also be interesting to get access to the operating conditions of PlainPID, wouldn’t it ? PlainPID features a structure which contains the key information. There’s a function which gives access to this information an easy way:

/* Get variables */
stats_t stats;
PID.Stats(&stats);
/* Print variables */
Serial.print(stats.processVariable, 2);
Serial.print(";");
Serial.print(stats.setpoint, 2);
Serial.print(";");
Serial.print(stats.error, 2);
Serial.print(";");
Serial.print(stats.kFactor, 2);
Serial.print(";");
Serial.print(stats.iFactor, 2);
Serial.print(";");
Serial.print(stats.dFactor, 2);
Serial.print(";");
Serial.print(stats.outputValue, 2);
Serial.println();

So, is that all, really ? Almost. And this little lack is every thing. What me miss so far are the kP, kI and kD coefficients, aka Proportional, Integral and Derivative coefficients.There are many papers around dealing with the  best way to start with these parameters. A simple Google search will give plenty information. May be too much actually ! Here is my way:

  • Turn kI and kD to null. Adjust Kd until the process variable (measured temperature in our case) oscillates in a stable manner, no matter if the mean value is not reching the target value.
  • Then increase kD in order to reduce the amplitude of the oscillation.
  • And at last, increase kI in order to have the process variable oscillating around the target value.

Next is the plot of the temperature (blue trace)  profile after running PlainPID for various temperature settings (red trace). The gray trace corresponds to the power output (normalized for 100%).

plainpid_a

What is very clear from this plot is that the higher the set-point the more the power required and applied to the resistors. Also, it is difficult to control the temperature for low set-points as no active cooling is applied (an additional would help, but that’s an other story). At temperatures between 40 and 50 °C, the ripple is kept pretty low, down to approx. +/- 0.1 °C.

An advance analysis of data requires a careful look at the other traces and gives some indications about the way PID works. Here is the cross-reference between colors and data type: blue = processVariable, red = set-point, green = error, yellow = kFactor, pink = iFactor, gold = dFactor, gray = outputValue. Data is available from here for those who are interested in having a closer look.

And here is the code which helped generating this data:

#include <PlainTMP0x.h> /* TMP0x sensors library */
#include <PlainPWM.h> /* PWM library */
#include <PlainPID.h> /* PID library */
PlainTMP0x TMP;
PlainPWM PWM;
PlainPID PID;
/* 
Use _Kp to decrease the rise time.
Use _Kd to reduce the overshoot and settling time.
Use _Ki to eliminate the steady-state error
*/
const float _Kp = 1.5; 
const float _Ki = 0.02; 
const float _Kd = 12.00; 
const uint8_t _smoothingFactor = 16;
/* Regulation intervals */
const uint32_t _interval = 1000;
int32_t _now, _lastTime;
uint32_t _startTime = millis();
float _vSetPoints[6] = {30.0, 40.0, 50.0, 45.0, 35.0, 25.0};
uint8_t _counter = 0;

void setup(void) 
{
	/* Application settings */
	Serial.begin(115200);
	/* Temperature sensor settings */
	TMP.InitializeTMP(TMP_MOD_TMP04, &PORTB, 0, TMP_UNI_CELSIUS, 8);
	/* PWM settings */
	PWM.InitializePWM(&PORTB, 1, 4);
	PWM.DutyCycle(0);
	/* PID settings */
	PID.InitializePID(0.0, 256.0, _Kp, _Ki, _Kd, _smoothingFactor);;
	PID.SetPoint(25.0);
}

void loop(void)
{
	do {
		_now = millis();
	} while ((_now - _lastTime) < _interval);
	_lastTime = _now;
	if ((_now % 600000) == 0) {
		_counter += 1;
		_counter %= 6;
		PID.SetPoint(_vSetPoints[_counter]);
	}
	PWM.DutyCycle(PID.PIDOutput(TMP.Temperature()));
	GetAndPrintStats();
}

void GetAndPrintStats(void)
{
	stats_t stats;
	PID.Stats(&stats);
	/* Print variables */
	Serial.print((_now / 1000.0), 3);
	Serial.print(";");
	Serial.print(stats.processVariable, 2);
	Serial.print(";");
	Serial.print(stats.setpoint, 2);
	Serial.print(";");
	Serial.print(stats.error, 2);
	Serial.print(";");
	Serial.print(stats.kFactor, 2);
	Serial.print(";");
	Serial.print(stats.iFactor, 2);
	Serial.print(";");
	Serial.print(stats.dFactor, 2);
	Serial.print(";");
	Serial.print(stats.outputValue / 256.0 * 100.0, 2);
	Serial.println();
}

 

 

Leave a Reply

You must be logged in to post a comment.