Google Analytics

Wednesday, February 8, 2012

Banjo's Arduino Thermocouple Controlled Thermostat

Banjo's Arduino Thermocouple Controlled Thermostat
(Note: scroll down for circuit diagram and software listing)
Part 2 of 3 parts

Thermostat controlled by Arduino using

Interior view

Fritzing view of Arduino Thermostat Controller using Thermocouple 

In a previous post, I presented an Arduino Timer Circuit, which was my original attempt at controlling a gas heater on my porch.  I originally thought this might work pretty well, as my porch is somewhat drafty, and I thought a time-based control might work better than a temperature-based control.  That didn't work out in practice, although I am seeing some truth to that assumption when the wind picks up!

Hardware Change
In this circuit, I have changed some of the hardware by adding a thermocouple breakout-board from AdaFruit.  This device makes use of a Maxim MAX6675, which provides you with a lot benefits, such as cold-junction thermocouple adjustments.  It also interfaces well from a software standpoint, making your the job of determining temperature much easier.

Changing from a timer-based controller to a temperature-based controller meant I would have to make a hardware change, as well as software changes.  I mentioned the hardware change above; now for insight into the software change.

Software Change
Since this is temperature-based, I no longer need to calculate time and time-changes introduced through manipulation of the front knob (potentiometer).  Instead, the front knob (potentiometer) will now change the desired temperature, or setpoint, of a thermostat.

So the first software change will be to remove the time-based calculations, and substitute in the temperature-based changes.  The first choice is what temperature span to give to the knob, from full counterclockwise to full clockwise - how many degrees of change are under the control of the knob?  I elected to give the full range a change of 4 degrees for my porch - your needs may differ.  The smaller the total change, the larger the movement of the knob.  You could in fact, make the full range be as small as 1 dF, (or as small as you desire), but the smaller you make it, the more susceptible to circuit noise, and temperature changes due to wind-drafts.  After some experimentation for my porch, I elected for a 4 dF full range change.

Knowing that I have provided a 4 dF full range temperature change, the next choice is - what is the bottom temperature?  I decided that I wouldn't want to have a setpoint below 70 dF, so I made this value my base, or bottom, value.  In my case, therefore, I have a bottom temperature setpoint capability of 70 dF, and with a 4 dF full range, my maximum setpoint will be 74 dF.

A final decision regards the deadband.  Deadband represents the range, in temperature, where no state-change is made by a thermostat.  In other words, if the thermostat is in the heating-state, then it will continue to demand heat until it reaches the setpoint plus the deadband value.  If the thermostat is in the cooling-state (or for a heater, coasting or het-not-on), then it will continue to demand no-heat until it reaches the setpoint minus the deadband value.

My first thoughts on deadband would be that I would make it a small as possible.  Well, I realized this wasn't a good idea.   Let's assume I have a setpoint of 72 dF, and the thermostat is in a heating-state.  Let's further assume I have a setpoint of 0.1 dF.  This means that the thermostat will continue to heat until the setpoint (72 dF) is reached, plus the deadband (0.1), or until 72.1 dF is reached.  Once that 72.1 dF is reached, the thermostat will change states to no-heat.  At that point, the temperature in the porch will fall, until it reaches the setpoint value (72 dF) minus the deadband value (0.1), or 69.9 dF.  If you were running a compressor style heatpump, then you may burn up the compressor or motor, as almost every gust of wind could cause the thermostat to change states.  So having a realistic value for the deadband, instead of the smallest value you can sense, is your goal.  What my goal is, for my porch, is to not feel hot when the thermostat is in the heating-state and reaches the turn-off point (setpoint plus deadband), and not feel cold when it reaches the turn-on point (setpoint minus deadband).  Also, note that the temperature, when falling, will continue to fall for a while after the thermostat changes state from no-heat to on-heat, before the heated air has enough time to circulate; the same idea applies to turning changing to no-heat too.  After some experimentation, I found that, for my porch, the deadband needs to be 0.25 dF.  This will vary for you, based on the size of the room, the size of your heater, the amount of draft, the heat loss rate of the room, your willingness to tolerate temperature swings......all of which you can adjust by making a simple software change in the program (below).

So, these are really the fine points of turning the timer-based circuit into a temperature-based thermostat.

  • change to the hardware to add a thermocouple breakout board.
  • change to the software to read the thermocouple temperature value
  • change to the software to add a deadband
  • change to the software to convert the knob adjustment from time to temperature 
  • change to the software to add a base temperature

Software Listing

//Author: Banjo 1/29/12
//  - This program makes use of the Thermocouple breakout board from MAX6675, inserted into 
//    Arduino UNO Digital pin positions 2 - 6.  Correctly inserted, this sheild will lay over the board, not project outside of it.
//  SOFTWARE: This program makes use of a thermocouple library from: //
//    you will also need to install the library and rename it, from that same page.
//  - displays 
//       RED - heater is on
//       GREEN - heater is off
//       Alternating RED and GREEN - pilot doesn't appear to be lit.
//For use:
//  - Wire manual heater switch to NO contacts on relay.  This relay is controlled by this Arduino.
//    These contacts are not polarized, so it doesn't matter which contact goes to which
//    leg of the switch.
//  - Wire Type K Thermocouple to screws on Thermocouple breakout board, observing correct polarity.
//      (Note: if you get the thermocouple poloratiy wrong, it won't hurt anything, it just won't work right.)
//for testing:
//  - If you want LED for HEATER ON, then add LED at digital pin 9, then through resistor to ground
//  - if you want LED for HEATER OFF, then add LED at digital pin 10, then through resistor to ground 
//  - add jumper from 5 vdc to pin A1 - this is the pilot light permissive
//  - add jumper from pin A0 to 3.3 vdc ref; this will supply about 66% ratio
//  - change MAX_CYCLE_TIME to something short, like 10 ONE_SECOND
//  - change IGNORE_MINIMUM to something short, like 2 ONE_SECOND
//  - remember to change back!
//some handy time constants
unsigned long ONE_SECOND = 1000;
unsigned long ONE_MINUTE = ONE_SECOND * 60;
int MAX_ADC_COUNTS = 1024; //ADC

//heater controlled by temperature
#include max6675.h
//stuff for thermocouple
int thermoDO = 4;
int thermoCS = 5;
int thermoCLK = 6;
MAX6675 thermocouple(thermoCLK, thermoCS, thermoDO);
int vccPin = 3;
int gndPin = 2;
//stuff for calcs
float MAX_CONTROL_RANGE_dF = 5.0;
float DEADBAND_dF = 0.5;
float CALIBRATION = -3.25; //Testing boiling water adjusted for altitude (-1 deg for every 500 ft).  If you don't test, then set to 0.0 
float LOWEST_TEMPERATURE_SETPOINT = 70.0;  //This corresponds to 0% on temperature setpoint adjustment knob
const int TEMPERATURE_AVERAGING_ARRAY_SIZE = 10;  //Samples to be averaged.  Eg., this value set to 10 will average 10 samples
float temperatureReadings[TEMPERATURE_AVERAGING_ARRAY_SIZE];
boolean firstTime = true;

//pin assignments common to all heater contoller types
int UNO_LED_PIN = 13;  //LED on the uno board
int STATE_LED_PIN = 9;  //LED if using an external state LED
int OFF_LED_PIN = 10;   //LED heater off 
int HEATER_OUTPUT_PIN = 8;        //This controls the onboard relay that controls the heater gas valve
int HEATER_RATIO_INPUT_PIN = A0;  //this is the onboard potetiometer that controls the ratio of on to off
int HEATER_INPUT_PERMISSIVE_PIN = A1;  // Read this pin for input permissive from thermocouple pile.  

int HEATER_ON = HIGH;  //used because may use sink current at some point
int HEATER_OFF = LOW;  //used because may use sink current at some point
int ALARM_COUNTS = 250;  //blink rate

void setup() {                
  // initialize the digital pin as an output.
  // Pin 13 has an LED connected on most Arduino boards:

  pinMode(vccPin, OUTPUT); digitalWrite(vccPin, HIGH);
  pinMode(gndPin, OUTPUT); digitalWrite(gndPin, LOW);
  // wait for MAX chip to stabilize

  Serial.println("setup()...Entry & Exit"); 


void loop() {
  //digitalWrite(ONBOARD_LED_PIN, HIGH);   // set the LED on

float getSetpointRatio() {
  //rather than counts of 0 - 1023 (range of the onboard ADC), return the 0% - 100% of range
  float ratio = 0.0;
  int sensorValue = 0;

  sensorValue = analogRead(HEATER_RATIO_INPUT_PIN);
  ratio = float(sensorValue) / float(MAX_ADC_COUNTS);

//  Serial.print("getSetpointRatio()....sensorValue: "); Serial.println(sensorValue); 
//  Serial.print("getSetpointRatio()....ratio: "); Serial.println(ratio);
  return ratio;


void turnHeaterOn () {  //needs to be refactored with turnHeaterOff
  double temperature = 0.0;
  double previousTemperature = 0.0;
  float setpointRatio = 0.0;
  float setpointValue = 0.0;  //dF
  static boolean initNeeded = true;  //needed first time we start up
  Serial.println("turnHeaterOn()...temperature controlled...entry.......................");
  do {  
    previousTemperature = temperature; //first time will be 0.0;
    temperature = thermocouple.readFarenheit();
    temperature += CALIBRATION; //Determine this by testing with boiling water for your probe.  
    temperature = getTemperatureAverage(temperature);
    setpointRatio = getSetpointRatio();
    setpointValue = LOWEST_TEMPERATURE_SETPOINT + (MAX_CONTROL_RANGE_dF * setpointRatio);
    Serial.print("turnHeaterOn()...setpointValue: "); Serial.println(setpointValue);
    Serial.print("turnHeaterOn()...Temperature avg: "); Serial.println(temperature);
    Serial.print("turnHeaterOn()...previousTemperature: ");Serial.print(previousTemperature); //useful to see how noise the data is
      Serial.print(", difference: "); Serial.println(temperature - previousTemperature);
  }  while(temperature < setpointValue+DEADBAND_dF);

void turnHeaterOff() {  //needs to be refactored with turnHeaterOn
  double temperature = 0.0;
  double previousTemperature = 0.0;
  float setpointRatio = 0.0;
  float setpointValue = 0.0;  //dF

  Serial.println("turnHeaterOff()...temperature controlled...entry......................");

  do {
    previousTemperature = temperature; //will be 0 for first time
    temperature = thermocouple.readFarenheit();
    temperature += CALIBRATION; //Determine this by testing with boiling water for your probe.  
    temperature = getTemperatureAverage(temperature);
    setpointRatio = getSetpointRatio();
    setpointValue = LOWEST_TEMPERATURE_SETPOINT + (MAX_CONTROL_RANGE_dF * setpointRatio);
    Serial.print("turnHeaterOff()...setpointValue: "); Serial.println(setpointValue);
    Serial.print("turnHeaterOff()...Temperature avg: "); Serial.println(temperature);
    Serial.print("turnHeaterOff()...previousTemperature: ");Serial.print(previousTemperature); //useful to see how noise the data is
      Serial.print(", difference: "); Serial.println(temperature - previousTemperature);
  }  while(temperature > setpointValue-DEADBAND_dF);

float getTemperatureAverage(float readValue) {
  static int element = 0;
  float oldReadingValue = 0.0;
  float newAverageValue = 0.0;
  static float oldAverageValue = 0.0;
//  Serial.println("getTemperatureAverage()...entry ");

  if(firstTime) {  //first time, there are no previous readings, so populate the whole thing with current reading
//    Serial.println("getTemperatureAverage()...First Time.");
    firstTime = false;
    for(int i=0;i<TEMPERATURE_AVERAGING_ARRAY_SIZE; i++) {
      temperatureReadings[i] = readValue;  //do the population with current reading
    oldAverageValue = readValue;  //on first time, all are same, so this is also the average
  //doing it this way avoids having to loop through whole array and then taking the average
  //replace the oldest reading with the newest reading, then offset the average value
  //this is a circular queue
  oldReadingValue = temperatureReadings[element];
  temperatureReadings[element] = readValue;  //replace old value with new value
  newAverageValue = (readValue - oldReadingValue)/ float(TEMPERATURE_AVERAGING_ARRAY_SIZE);  //get averaged change between old value and new value
  oldAverageValue += newAverageValue; //change average by average increase or decrease
//  Serial.print("getTemperatureAverage()...average temperature: "); Serial.println(averageTemperature);
  if(element==TEMPERATURE_AVERAGING_ARRAY_SIZE)  //this makes it a circular queue
    element = 0;
//  Serial.println("getTemperatureAverage()...exit ");

  return oldAverageValue;  


void heaterOn() {
  Serial.println("heaterOn() ... entry.");
  digitalWrite(HEATER_OUTPUT_PIN, HEATER_ON);   // sink current
  digitalWrite(OFF_LED_PIN, LOW);
  digitalWrite(UNO_LED_PIN, HIGH);   // set the LED on
  digitalWrite(STATE_LED_PIN, HIGH);
  Serial.println("heaterOn() ... exit.");

void heaterOff() {
  Serial.println("heaterOff() ... entry.");
  digitalWrite(HEATER_OUTPUT_PIN, HEATER_OFF);     
  digitalWrite(UNO_LED_PIN, LOW);   // set the LED on
  digitalWrite(STATE_LED_PIN, LOW);
  digitalWrite(OFF_LED_PIN, HIGH);
  Serial.println("heaterOff() ... exit.");

void alarm(int sensorValueCounts) {  //needs to be refactored to use heaterOff()
  //blink both on and off LEDs
  Serial.println("alarm()....Entry...will not exit!");

  digitalWrite(OFF_LED_PIN, LOW);
  Serial.print("alarm()....sensor low. sensorValueCounts: "); Serial.println(sensorValueCounts);
  while(1) {
    //keep turning heater off!
    //show it by alternating blinking both LEDs
    digitalWrite(UNO_LED_PIN, HIGH);
    digitalWrite(STATE_LED_PIN, HIGH);
    digitalWrite(OFF_LED_PIN, LOW);

    digitalWrite(STATE_LED_PIN, LOW);
    digitalWrite(OFF_LED_PIN, HIGH);


  1. Hi, I try to compile and it is a problem with subroutine with read temperature.. is line incomplete at IF:
    firstTime = false;
    for(int i=0;i
    temperatureReadings[i] = readValue; //do the population with current reading

    1. Nicu, I don't know where the code went. I have corrected it and updated the post. Thanks, BBQ!