Archive for March 7th, 2012

Peltier PWM Temperature Control: Better PI Loop

As I feared, P control can’t push the platform into the deadband all by itself at high temperatures, so I rewrote the loop the way it should have been all along:

  • PWM=1 beyond a limit well beyond the deadband, set integral=0 to avoid windup
  • Proportional + integral control inside that limit
  • Not worrying about relay chatter

Holding PWM=1 until the PI loop kicks in ensures that the P control won’t lose traction along the way, but full throttle must give way to PI control outside the deadband to avoid a massive overshoot. Relay chatter could be a problem around room temperature where the heating/cooling threshold falls within the deadband, but that won’t shouldn’t be a problem in this application.

Without much tuning, the results looked like this:



Each temperature plateau lasts 3 minutes, the steps are 10 °C, starting at 30 °C and going upward to 50 °C, then downward to 0 °C, and upward to 20 °C. These are screenshots from OpenOffice Calc, so the resolution isn’t all that great.

Two internal variables show what’s going on:



The blue trace is the temperature error (actual – setpoint: negative = too cold = more heat needed), the purple trace is the signed PWM drive (-1.0 = full heat, +1.0 = full cool) summed from the P and I terms.

Overlaying all the plateaus with their starting edges aligned on the left, then zooming in on the interesting part, shows the detailed timing:



These X axis units are in samples = calls to the PI function, which happened about every 100 ms, which is roughly what the main loop will require for the MOSFET measurements.

The Peltier module just barely reaches 0 °C with a 14 °C ambient: the drive exceeds +1.0 (output PWM = 255) as the temperature gradually stabilized at 0 °C with the module at full throttle; it’s dissipating 15 W to pump the temperature down. The heatsink reached 20 °C, with a simple foam hat surrounding the Peltier module and aluminum MOSFET mount. Any power dissipation from a MOSFET would add heat inside the insulation, but a bit more attention to detail should make 0 °C workable.

On the high end, it looks like the module might barely reach 60 °C.

Increasing the power supply voltage to increase the Peltier current would extend the temperature range, although a concerted stack probe didn’t produce anything like an 8 V 5A supply in the Basement Laboratory Parts Warehouse. If one turns up I’ll give it a go.

There’s a bit of overshoot that might get tuned away by fiddling with the P gain or squelching the integral windup beyond the deadband. The temperature changes will be the most time-consuming part of the MOSFET measurement routine no matter what, so it probably doesn’t make much difference: just stall 45 s to get past most of the transient overshoot, then sample the temperature until it enters the deadband if it hasn’t already gotten there. Reducing the initial overshoot wouldn’t improve the overall time by much, anyway, as it’d just increase the time to enter the deadband. Given that the initial change takes maybe 30 seconds at full throttle, what’s the point?

The PI loop Arduino source code, with some cruft left over from the last attempt, and some tweaks left to do:

#define T_LIMIT         3.0                 // delta for full PWM=1 action
#define T_ACCEPT        1.5                 // delta for good data (must be > deadband)
#define T_DEADBAND      1.0                 // delta for integral-only control
#define T_PGAIN         (1.0 / T_LIMIT)     // proportional control gain: PWM/degree
#define T_IGAIN         0.001               // integral control gain: PWM/degree*sample

#define sign(x) ((x>0.0)-(x<0.0))           // adapted from old Utility.h library

//-- Temperature control
//      returns true for temperature within deadband

int SetPeltier(float TNow, float TSet) {

float TErr, TErrMag;
int TSign;
float PelDrive;

int EnableHeat,OldEnableHeat;
static float Integral;
int TZone;
int PWM;
int PWMSigned;

    TErr = TNow - TSet;                  // what is the temperature error
    TErrMag = abs(TErr);                 //  ... magnitude
    TSign = sign(TErr);                  //  ... direction

    if (TErrMag >= T_LIMIT)                 // beyond outer limit
      TZone = 3;
    else if (TErrMag >= T_DEADBAND)         // beyond deadband
      TZone = 2;
    else if (TErrMag >= T_DEADBAND/2)       // within deadband
      TZone = 1;
    else                                    // pretty close to spot on
      TZone = 0;

    switch (TZone) {
      case 3:                                   // beyond outer limit
        PelDrive = TSign;                       //  drive hard: -1 heat +1 cool
        Integral = 0.0;                         //  no integration this far out
      case 2:                                   // beyond deadband
      case 1:                                   // within deadband
      case 0:                                   // inner deadband
        PelDrive = T_PGAIN*TErr + T_IGAIN*Integral;             // use PI control
        Integral += TErr;                                       // integrate the offset
      default:                                  // huh? should not happen...
        PelDrive = 0.0;

    EnableHeat = (PelDrive > 0.0) ? LOW : HIGH;             // need cooling or heating?
    OldEnableHeat = digitalRead(PIN_ENABLE_HEAT);           // where is the relay now?

    if (OldEnableHeat != EnableHeat) {          // change from heating to cooling?
      analogWrite(PIN_SET_IPELTIER,0);          // disable PWM to flip relay
      delay(15);                                // relay operation + bounce

    PWM = constrain(((abs(PelDrive) * AO_PEL_SCALE) + AO_PEL_OFFSET),0.0,255.0);

    if (true) {
      PWMSigned = (EnableHeat == HIGH) ? -PWM : PWM;

      Serial.print(NowTime - StartTime);

    return (TZone <= 1);