Arduino PID Temperature Controller

I recently implemented a PID routine for controlling temperature settings.  The goal was to maintain a fixed temperature rate for a specified ramp up period, hold the temperature for a specified soak time, and then cool down at a specified rate.  This could be useful for solder reflow cycles, food baking, beer brewing, or anything that needs a controlled timing scheme for temperature control.

 

PID stands for proportional-integral-derivative and is a very popular algorithm in control systems.  In very simple terms, the temperature controller takes readings of the current state of the system and uses that data to determine whether to keep the heating element on.  I won’t get into the details of the equation or the coefficients in this post; I am merely showing an Arduino implementation.  My PID controller library on Github is here.  I based my Arduino code off of this library on Github.  This library is very good with handling the PID equation, but I felt like it could use some more description and clarification in applying the equation to ordinary Arduino peripherals, which is why I’m writing this post.

 

My system includes:

Arduino Uno

MAX31855 Thermocouple Reader

Power Switch for switching heating element, activated by digital pin.  Solid State Relays can work too.

PID_schematic

The MAX31855 is controlled through a SPI interface.  I wrote a library for communicating with this chip on Github.  The MISO and SCLK pin positions are dictated by the type of Arduino board you are using (see this).  Any digital pin can be used for CSB.  I don’t normally use the built-in SPI CSB pin (pin 10 on Uno) just because I normally am using multiple SPI devices at the same time that can’t all use pin 10.

I also just randomly picked a digital pin to control the SSR.

I have posted the full code at the bottom of the page, but I will break down some of the main functions now.

First, the function to run the PID equation.  As parameters, this function takes in the 3 PID coefficient values (Kp, Ki, Kd); WindowSize, which is the maximum value the response could potentially be; and time_interval, the amount of time before the PID equation is re-evaluated.  After setting the properties of the myPID object (part of the borrowed PID class), the output response is calculated based on the current temperature reading input and the desired temperature set point.  I had issues getting the routine to start if the set point was too low, so the init_read section kick-starts the routine for the first time_interval.  I calculated a ratio of the output response divided by the maximum response to determine how long the heating element should be on and how long it should be off in a given time_interval.

 

void run_PID(double Kp, double Ki, double Kd, uint16_t WindowSize, uint32_t time_interval)
{
   double ratio;
   uint32_t windowStartTime;
   uint8_t buttons;

   //Specify the links and initial tuning parameters
   myPID.SetOutputLimits(0, WindowSize);
   myPID.SetTunings(Kp, Ki, Kd);
   myPID.SetMode(AUTOMATIC);

   //This prevents the system from initially hanging up
   if(init_read){init_read = 0;Setpoint = Setpoint + 1;}
   else{read_temps();}

   windowStartTime = millis();

   Input = CtoF(thermo.thermocouple_temp);
   myPID.Compute();

   ratio = Output / WindowSize;

   digitalWrite(SSR_PIN, 1);
   while(millis() - windowStartTime < time_interval * ratio);

   digitalWrite(SSR_PIN, 0);
   while(millis() - windowStartTime < time_interval);
}

 

Next is the section that loops through the multiple time intervals.  Depending on the system state (ramp up, soak, cool down), the function first determines how long that state will run based on the beginning and ending temperatures, calculates the number of time intervals are in that state time, and then loops through those iterations.  In ramp up, the function determines the initial temperature reading and subtracts from the soak temperature to get the temperature delta, which along with the ramp rate can be used to find the total ramp time.  In the soak state, the length of time is already specified.  In cool down, the reverse of the ramp up is used.

void run_cycle_time(void)
{
   uint32_t initial_time = millis();
   uint32_t elapsed_time;
   double diff_time_min;
   double cycle_time;

   //This set of statements calculates time of cycle phase
   if(state == 0) //rising ramp, increasing temperature
   {
      //Calculate time remaining in rise phase based on temperature and rate
      read_temps();
      init_temp = CtoF(thermo.thermocouple_temp);
      cycle_time = (max_temp - init_temp)/ramp_rate;
   }
   else if(state == 1) //soak time
   {
      //Soak time is already determined
      cycle_time = soak_time;
   }
   else if(state == 2) //falling ramp, decreasing temperature
   {
      //Calculate time remaining in fall phase based on temperature and rate each cycle
      read_temps();
      cycle_time = (CtoF(thermo.thermocouple_temp) - init_temp)/cool_down;
   }

   //Determine time left in current phase
   elapsed_time = millis();
   diff_time_min = float(elapsed_time - initial_time) / 60000;

   while(diff_time_min < cycle_time)
   {
      if(state == 0) //rising ramp, increasing temperature
      {
          //While increasing, Setpoint increases based on elapsed time
          Setpoint = (diff_time_min * ramp_rate) + init_temp;
      }
      else if(state == 1) //soak time
      {
          Setpoint = max_temp;
      }
      else if(state == 2) //falling ramp, decreasing temperature
      {
          //While decreasing, Setpoint increases based on elapsed time
          Setpoint = max_temp - (diff_time_min * cool_down);
      }

      //Determine current temp
      read_temps();

      //Determine PID response based on current temp
      run_PID(2, 5, 1, 500, PID_interval);

      //Determine time left in current phase
      elapsed_time = millis();
      diff_time_min = float(elapsed_time - initial_time) / 60000;
   }
}

The main loop simply steps through the various states in sequence.

void loop()
{
   switch (state) {
      case 0: //Ramp Up
         run_cycle_time();
         state = state + 1;
         break;
      case 1: //Soak Time
         run_cycle_time();
         state = state + 1;
         break;
      case 2: //Cool Down
         run_cycle_time();
         state = state + 1;
         break;
      case 3: //Finished
         while(1);
    }
}

 

UPDATE:  I’ve started a new post on Controlling Multiple PID Loops with One Arduino.  Please check it out!
The full source code for PID controller

#include <PID_v1.h>
#include "MAX31855.h"

#define SPI_transfer 1

#if defined(SPI_transfer)
#include "SPI.h"
#endif

//Arduino Pins
uint8_t DHTPIN = 2;
uint8_t MAX31855_DATA = 12;
uint8_t MAX31855_CLK = 13;
uint8_t MAX31855_LAT0 = 7;
uint8_t SSR_PIN = 4;

//Temperature Cycle Settings
uint8_t state = 0;
uint16_t max_temp = 95; //in degrees F
uint8_t soak_time = 10; //in minutes
uint8_t ramp_rate = 10; //in degrees F/min
uint8_t cool_down = 10; //in degrees F/min

//PID parameters
double init_temp;
double Input, Output, Setpoint;
uint8_t init_read = 1; //flag to prevent 2 temp reads on first PID pass
uint32_t PID_interval = 5000; //time in ms to run PID interval

//Object Instantiation
MAX31855 thermo;
PID myPID(&Input, &Output, &Setpoint, 2, 5, 1, DIRECT);

void setup()
{
   pinMode(SSR_PIN, OUTPUT);

   thermo.setup(MAX31855_LAT0);
   read_temps();
}

void loop()
{
   switch (state) {
      case 0: //Ramp Up
         run_cycle_time();
         state = state + 1;
         break;
      case 1: //Soak Time
         run_cycle_time();
         state = state + 1;
         break;
      case 2: //Cool Down
         run_cycle_time();
         state = state + 1;
         break;
      case 3: //Finished
         while(1);
    }
}

void run_PID(double Kp, double Ki, double Kd, uint16_t WindowSize, uint32_t time_interval)
{
   double ratio;
   uint32_t windowStartTime;
   uint8_t buttons;

   //Specify the links and initial tuning parameters
   myPID.SetOutputLimits(0, WindowSize);
   myPID.SetTunings(Kp, Ki, Kd);
   myPID.SetMode(AUTOMATIC);

   //This prevents the system from initially hanging up
   if(init_read){init_read = 0;Setpoint = Setpoint + 1;}
   else{read_temps();}

   windowStartTime = millis();

   Input = CtoF(thermo.thermocouple_temp);
   myPID.Compute();

   ratio = Output / WindowSize;

   digitalWrite(SSR_PIN, 1);
   while(millis() - windowStartTime < time_interval * ratio);

   digitalWrite(SSR_PIN, 0);
   while(millis() - windowStartTime < time_interval);
}

void run_cycle_time(void)
{
   uint32_t initial_time = millis();
   uint32_t elapsed_time;
   double diff_time_min;
   double cycle_time;

   //This set of statements calculates time of cycle phase
   if(state == 0) //rising ramp, increasing temperature
   {
      //Calculate time remaining in rise phase based on temperature and rate
      read_temps();
      init_temp = CtoF(thermo.thermocouple_temp);
      cycle_time = (max_temp - init_temp)/ramp_rate;
   }
   else if(state == 1) //soak time
   {
      //Soak time is already determined
      cycle_time = soak_time;
   }
   else if(state == 2) //falling ramp, decreasing temperature
   {
      //Calculate time remaining in fall phase based on temperature and rate each cycle
      read_temps();
      cycle_time = (CtoF(thermo.thermocouple_temp) - init_temp)/cool_down;
   }

   //Determine time left in current phase
   elapsed_time = millis();
   diff_time_min = float(elapsed_time - initial_time) / 60000;

   while(diff_time_min &lt; cycle_time)
   {
      if(state == 0) //rising ramp, increasing temperature
      {
          //While increasing, Setpoint increases based on elapsed time
          Setpoint = (diff_time_min * ramp_rate) + init_temp;
      }
      else if(state == 1) //soak time
      {
          Setpoint = max_temp;
      }
      else if(state == 2) //falling ramp, decreasing temperature
      {
          //While decreasing, Setpoint increases based on elapsed time
          Setpoint = max_temp - (diff_time_min * cool_down);
      }

      //Determine current temp
      read_temps();

      //Determine PID response based on current temp
      run_PID(2, 5, 1, 500, PID_interval);

      //Determine time left in current phase
      elapsed_time = millis();
      diff_time_min = float(elapsed_time - initial_time) / 60000;
   }
}

double CtoF(double temp_C)
{
   double temp_F = (temp_C * 1.8) + 32;
   return temp_F;
}

void read_temps(void)
{
   thermo.read_temp();
}

 

 

Advertisements

22 thoughts on “Arduino PID Temperature Controller

  1. I tried the full source code and I got the following errors. Could you tell me what I am missing?

    Arduino: 1.6.4 (Windows 7), Board: “Arduino Pro or Pro Mini, ATmega328 (5V, 16 MHz)”

    Build options changed, rebuilding all pid-Ktype.ino: In function ‘void run_PID(double, double, double, uint16_t, uint32_t)’:
    pid-Ktype:74: error: no match for ‘operator[]’ (operand types are ‘MAX31855’ and ‘int’)
    pid-Ktype.ino: In function ‘void run_ramp_time()’:
    pid-Ktype:129: error: too many arguments to function ‘void run_PID(double, double, double, uint16_t, uint32_t)’
    pid-Ktype.ino:58:6: note: declared here
    no match for ‘operator[]’ (operand types are ‘MAX31855’ and ‘int’)

    • Thanks for checking out my blog. I’ve written an update that should simplify my code and make it easier to run the example. For your exact issue, I just noticed that on line 74, it should be “thermo.thermocouple_temp” instead of “thermo[0].thermocouple_temp”.

  2. Pingback: Arduino PID Update | Joe's Electronics Adventures

  3. I now see your new post. The reason I was interested in this one was because of the controlled ramp feature. I want to implement a ramp up/soak/ramp down controller.

    • Sorry for all the issues. I updated the code in the original post and added several more comments. Looking back at it now, I can see how that code would have very hard to follow.

      • Hey. It happens. It isn’t easy to make things simple and clear right off the bat. When we are developing something we are concerned about how to get it done and not how it will be read by someone later.

        I had once written a postscript program for a programmer. It was prison admission form that was to be computerized. He was doing the computer interface and he was going to feed the parameters and the postscript programming to the printer.

        I was writing routines for spacing and I had started in points, done some in tenths of an inch, then sixteenths. I think I had four different units of measurements by the time I was done. As I got further a different unit became more appropriate. In the end it all worked perfectly but if someone had to look at the code they would thing I was schizophrenic.

        Anyway. I appreciate the work you have done here. I just have to get some of the language rules to sink in.

  4. I still couldn’t get it to work so I had to start troubleshooting. I added LEDs to unused pins to indicate how far the program got. But it never got very far. The problem seemed to be in a library.

    I eventually tracked it down after some digging around on the internet.

    MAX31855.cpp

    uint8_t MAX31855::spi_shift(void)
    {
    //SPI.begin();
    uint8_t temp_data = 0;
    temp_data = SPI.transfer(255);
    //SPI.end();
    return temp_data;
    }

    The problem was there was no SPI.begin(); executed. It is commented out in the above piece of code. It got hung up and I don’t know what it was doing. I put it into my initialization (setup) and then I was getting all my indicators. (LED & serial out)

    I had put in some serial print routines in the run_PID routine to show me the time, setpoint, temp, error, and output. It runs pretty slick. I’m playing around with different PID settings now. I have a thermistor taped to a power resistor. My ramp rate is 5 degrees per minute. My soak temp is 250 degrees.

    Once again, thanks Joe for putting this up. I wish I would have found it earlier. It is the starting point I was looking for. I didn’t want to start with code with a display or keyboard. I plan to use serial to talk to the controller. But first I want to add in the SD card logging.

    This has been interesting. Prior to this I played with the Blink program then a Logger test program. I’m surprised how much overhead some of the libraries consume. I thought there was plenty of memory but I now see this is a lot different than assembly language programming.

  5. Pingback: PID control implemented on DC motor with Arduino | Enhancing knowledge in control systems theories and its applications in the real world.

  6. Hello,
    Thanks for all informations, I want to make a pid control of temperature by a pic16F877.
    I saw someone use a resistance 10 ohm 5 watt for Heating with 12volte command it by PWM signal of pic, but i don’t know how he links betewen the PWM and temperature.
    I wish you help me with any info and thank you so much.

  7. Hello Joe,

    Thank you so much for this great code. I am running it on some heating elements stuck inside of parts and it does a great job of ramping and soaking with PID values I give it. As of now I am just controlling one relay that looks at just one temp in a part.

    I was wondering if there would be a way to control multiple relays with this code? This means multiple PID loops responding to multiple temperature readings from different areas of a part. I want to control different areas of a tool because controlling based off just one temperature in one area is not efficient for my application. I was struggling to figure this out on my own because the timing that goes into turning on and off a relay would mess up the timing for another relay.

    Thank you so much,
    Tarik

    • Hi Tarik,
      I really appreciate your comment. Running multiple PID loops should be very possible on an Arduino, but you are right that the timing would be messed up the way the code is now because I use the millis() function. If the code used a software counter as the timer instead of the “hardware” counter of the millis() function, this should work. Let me see what I can do. If I write you some code, do you mind trying it out and letting me know how it goes?

      • Thanks for the quick reply Joe!

        Great to hear it is possible! I was going to try and test the code on two different areas using the while loops for on and off right after each other for each area, though I assumed I would run into problems with one being completely off while the other was running through its loop. I basically just repeated the functions you made for one relay such as creating run_PID2 for the second relay.
        Yes, if you have a better idea for the code that would be greatly appreciated and I will test it out! And if there is a certain counter chip needed to overcome the issue as well I would get that.

        Thank you so much,

        Tarik

      • I’m concerned about the while loop that effectively waits for the time to expire before moving one. As you mentioned, the second relay won’t do anything while the first relay is waiting. Instead, I’m thinking about passing a time variable around so that timers for all relays could be evaluated almost concurrently, rather than serially (doing one and then the next).

        I haven’t had a chance to acutally put code together, but I’ll get to it in the next couple of days. Thanks for the idea. I’ll write a post about it when it’s done.

      • I actually didn’t repeat functions I repeated variables for each particular relay in the functions such as this:

        if(state == 0) //rising ramp, increasing temperature
        {
        // Serial.println(“Current state is Ramp up.”);
        //Calculate time remaining in rise phase based on temperature and rate

        init_temp2 = thermocouple2.readFarenheit();
        cycle_time2 = (max_temp – init_temp2)/ramp_rate;
        init_temp = thermocouple.readFarenheit();//the first temperature
        cycle_time = (max_temp – init_temp)/ramp_rate;//cycle_time will be how long it takes to ramp up from initial_temp to max_temp
        }

        Tarik

  8. Hello,

    Thank you very much for this great write up. I am implementing your project for my popcorn popper – coffee bean roaster. My setup is almost exactly the same, as I need to control the heat ramp rate on the roaster (via SSR) for a specified amount of time. However, the thermocouple that I am using is the MAX31856. My question is, how much of the source code would I need to modify in order to run it? So far, I have tried replacing the 31855 library with the 31856 files, as well as referencing the 31856 within the source code, but I keep getting errors. I am fairly new to Arduino, so any help would be much appreciated. Again, thank you very much for sharing this project, this is exactly what I’ve been searching for, just a little lost.

    Thanks,

    Alex Z.

  9. Pingback: Controlling Multiple PID Loops with One Arduino | Joe's Electronics Adventures

  10. Hello,

    I would like to ask you. Please, could you explain Temperature Cycle Settings? What does it mean soak time, ramp rate, cool down?

    Secodn question. Is it possible using libraries Adafruit MAX31855? I changed it so:

    Adafruit_MAX31855 thermo(MAX31855_CLK, MAX31855_LAT0, MAX31855_DATA);

    void read_temps(void)
    {
    thermo.readCelsius();
    }

    Input = thermo.readCelsius();

    Thank you in advance.

    • Hi Hans,
      Most controlled temperature chambers (ovens, kilns, etc.) have a cycle in which the unit ramps up from room temperature to the desired set temperature, then holds at the set temperature for a certain amount of time, and then cools back down to room temperature. I see that I could be more clear about that in my post. I’ll try to add a diagram for that too.
      Yes, I tried to make the code as modular as possible so that you could use any temp sensor that you want, just swap in new code.
      Thanks for reading!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s