Google Analytics

Monday, February 13, 2012

Banjo's Sous Vide Controller - Arduino Thermocouple Controller

Arduino Sous Vide Controller!

This is part 3 of a 3 part project
             (Part 1 is a Timer Controller)

Note: the software limits you to a minimum food-safety temperatuer setpoint of 130 dF.  

This project makes use of a previous project, the Arduino Thermocouple Controlled Thermostat for the ability to precisely detect and control the temperature of the water-bath, along with a device - power switch tail - from Adafruit.  This power switch tail needs 5 vdc to drive it.

So I've changed the wiring from the pure NO contacts on the timer and thermocouple controller projects (see previous posts), to have a switched 5 vdc across the contacts.  So, as wired, the 5 vdc comes from the Arduino 5 vdc pin, then through the NO contacts, then out to the power switch tail, and then back to the Arduino gnd pin.  Per the power switch tail, it needs 5 vdc at 40 ma, so that's the reason I'm still using the relay I used in the thermocouple controller.

This version of the project does not incorporate a display, in order to keep the project costs and simplicity down.  However, the temperature setpoint will be able to be established using a DVM, or analog panel voltmeter.  I used a (currently listed at $5.00) 5vdc panel meter from SparkFun.   This works great, as I assigned it 0 - 5 volts to correspond to 100 - 150 dF, which the project can generate using a PWM pin (I used pin 11 in my software).  That's 10 degrees F per volt, or 1 dF per 1/10th volt, which is easy to read on the little meter.  I mounted this on my cigar box (see picture).  Note: with a simple software change, I can use this same device for the gas heater thermocouple temperature controller, and assign it a range of 0 - 5 vdc for 70 - 75 dF, then each volt equals 1 dF.

Some useful links:

Software Changes
Note the software changes from that posted for the thermocouple controller.  Not significant, but I added:

  • PWM output on pin 11, in order to be able to view the setpoint without having an expensive LCD display or a laptop connected.  You can use a DVM, or as I did, mount a 5 vdc panel meter on the face of your project box.
  • Software generate the analog voltage values.
  • Changed software to reflect use of 'const'.
  • Gave predefined values for Sous Vide vs thermostat values.
I'm going to go ahead and post this, along with the software.  I'll update a Fritzing layout later and add back.


//Author: Banjo 1/29/12
// Rev 1.0 added ability to use dvm to monitor setpoint via pwm output
//     1.1 refactored some duplications in turnHeaterOn() and turnHeaterOff()
//     1.2 change setpoint so it can't be less than 130 dF for food safety reasons.  Added LOWEST_TEMPETURE_DISPLAY vs LOWEST_TEMPERATURE_SETPOINT
//  - 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.
//       5VDC volt meter.  This corresponds to temperature, with zero volts equal to your base temperature, and 5 volts equal to your base temperature plus range.
//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
//  - wire dvm across gnd and pin 11 for PWM setpoint value in volts
//  - remember to change back!
//some handy time constants
const unsigned long ONE_SECOND = 1000;
const unsigned long HALF_SECOND = 500;
const unsigned long QUARTER_SECOND = 250;
const unsigned long ONE_MINUTE = ONE_SECOND * 60;
const int MAX_ADC_COUNTS = 1024; //ADC

//heater controlled by temperature
//pin assignments 
const int HEATER_OUTPUT_PIN = 8;              //This controls the onboard relay that controls the heater gas valve
const int STATE_LED_PIN = 9;                  //LED if using an external state LED
const int OFF_LED_PIN = 10;                   //LED heater off 
const int REF_VOLTAGE_SETPOINT = 11;          //pwm output pin.  Connecting DVM here will give you corresponding temperature setpoint, added to LOWEST_TEMPERATURE_DISPLAY.  
const int UNO_LED_PIN = 13;                   //LED on the uno board

const int HEATER_RATIO_INPUT_PIN = A0;        //this is the onboard potetiometer that controls the ratio of on to off
const int HEATER_INPUT_PERMISSIVE_PIN = A1;   // Read this pin for input permissive from thermocouple pile.  

//     E.g., if LOWEST_TEMPERATURE_DISPLAY = 70, then 1 vdc output would be 71 deg; 2 volts = 72, ...5 volts = 75, which would be maximum value.
//for adafruit thermocouple amp
const int thermoDO = 4;  //corresponds to adafruit thermocouple amp shield
const int thermoCS = 5;  //corresponds to adafruit thermocouple amp shield
const int thermoCLK = 6; //corresponds to adafruit thermocouple amp shield
const int vccPin = 3;  //corresponds to adafruit thermocouple amp shield used via set high
const int gndPin = 2;  //corresponds to adafruit thermocouple amp sheild coupled to ground via set low
MAX6675 thermocouple(thermoCLK, thermoCS, thermoDO);  //function declaration
void docommonCalcs(double *pTemperature, double *pPreviousTemperature, float *pSetpointRatio, float *pSetpointValue);

//stuff for calcs
//**** normal usage ****
//const float LOWEST_TEMPERATURE_SETPOINT = 70.0;  //This corresponds to 0% on temperature setpoint adjustment knob.  It is the lowest temperature setpoint.
//const float LOWEST_TEMPERATURE_DISPLAY = 70.0;   //When setpoint minimum (food safety) and display are the same, set both to be same
//const float MAX_CONTROL_RANGE_dF = 5.0;          //useful to stay with 5, as it is also the pwm output voltage capability for use with dvm for setpoint.  see showSetpoint() for insight 
//**** end of normal usage ****
//**** for Sous Vide cooking replace with following values ****
const float MAX_CONTROL_RANGE_dF = 50.0;
//****  these values will give you a base temperature of 100.0dF, and with a range of 50.0 dF, you will have a total control temperature range of 100.0 dF to 150.0 dF.
//****  at these values, the 5 volt meter will represent 50 dF.  So each 1/10 volt will equal 1 dF.
//****  e.g., a value of 1 volt  would equate to 10 dF, plus the base of 100.0 dF, for a total of 110 dF.
//****        a value of 2 volts would equate to 20 dF plus base of 100.0 for a total of 120.0 dF
//****        a value of 3.1 volts would equate to 31dF pluse base of 100.0 for a total of 131 dF.
//****  Note for Sous vide cooking: I would not trust this device totally for setting by a volt meter.  I would 
//****       set the temperature, then verify the water bath after it has had time to stabalize by using an accurate thermometer.
//****       I would also extend considerably the amont of time needed for pasteurazation - I would not use minimum time values
//****       so I could be assured that the food has been pasteurized.  Please consult 
//****       Doublas E. Baldwin's excellant book "Sous Vide for the home cook" for information, and his web page at
//**** for additional safety information.
//****  Note for Souse Vide cooking equipment:  I would suggest using a crock-pot, placing it on low temperature if it is capable of 
//****       water up to 160 dF when full, or on high otherwise.  The lower setting is desirable, if usable with your cooker, in order to
//****       lesson the amount of temperature swings from heat on to heat off.  Also, use a device to control the crock pot that is capable 
//****       of safely handling the current and voltages involved.  I use something like the following device from Adafruit:
//**** power switch tail, which is rated at 15 amps at 120 vac.  
//**** end of Sous vide cooking values

//const float 
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 temperatureReadings[TEMPERATURE_AVERAGING_ARRAY_SIZE];
boolean firstTime = true;

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

float lowest_temperature_setpoint_ratio = 0.0; //global

void setup() {                
  // initialize the digital pin as an output.
  // Pin 13 has an LED connected on most Arduino boards:
  pinMode(REF_VOLTAGE_SETPOINT, OUTPUT);  //Will provide PWM output voltage

  pinMode(vccPin, OUTPUT); digitalWrite(vccPin, HIGH); //voltage source
  pinMode(gndPin, OUTPUT); digitalWrite(gndPin, LOW); //current sink

  //This calculation needs to be done only once
  // 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 doCommonCalcs(double *pTemperature, double *pPreviousTemperature, float *pSetpointRatio, float *pSetpointValue) {
  //note this uses pass-by-reference not pass-by-value in order to be able to change these values
  //warning - this method will change values on the passed references
    *pPreviousTemperature = *pTemperature; //first time will be 0.0;
    *pTemperature = thermocouple.readFarenheit();
    *pTemperature += CALIBRATION; //Determine this by testing with boiling water for your probe.  
    *pTemperature = getTemperatureAverage(*pTemperature);
    *pSetpointRatio = getSetpointRatio();
    if(*pSetpointRatio < lowest_temperature_setpoint_ratio) {
      *pSetpointRatio = lowest_temperature_setpoint_ratio;
      Serial.print("doCommonCalcs() ***!!!*** setpoint override to minimum food-safe value of: "); Serial.println(LOWEST_TEMPERATURE_SETPOINT);       
    showSetpoint(*pSetpointRatio);  //pwm
    *pSetpointValue = LOWEST_TEMPERATURE_DISPLAY + (MAX_CONTROL_RANGE_dF * *pSetpointRatio);
    Serial.print("doCommonCalcs()...setpointValue: "); Serial.println(*pSetpointValue);
    Serial.print("doCommonCalcs()...Temperature avg: "); Serial.println(*pTemperature);
    Serial.print("doCommonCalcs()...previousTemperature: ");Serial.print(*pPreviousTemperature); //useful to see how noise the data is
      Serial.print(", difference: "); Serial.println(*pTemperature - *pPreviousTemperature);

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 {  
    doCommonCalcs(&temperature, &previousTemperature, &setpointRatio, &setpointValue);
  }  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 {
    doCommonCalcs(&temperature, &previousTemperature, &setpointRatio, &setpointValue);
  }  while(temperature > (setpointValue-DEADBAND_dF));

void showSetpoint(float setpointRatio) {
  //write the voltage to pwm pin.
  //voltage out is defined as 0 - 5 vdc.  0 volts = LOWEST_TEMPERATURE_DISPLAY, and 5 volts = LOWEST_TEMPERATURE_DISPLAY + MAX_CONTROL_RANGE_dF
  //so, if LOWEST_TEMPERATURE_DISPLAY is defined as 70 dF, and MAX_CONTROL_RANGE_dF is defined as 5 dF, then
  // 70 dF would equal 0 Volt, 71 dF would equal 1 Volt, 72 dF would equal 2 volts, 73 would equal 3 volts, 74 would equal 4 volts, and 75 would equal 5 volts
  float voltageOut = 0.0;
  int pwmValue = 0;
//  setpointRatio = 1.0 - setpointRatio; //depending on which way you wire your pot, you may have to have 1.0 - setpointRatio or just setpointRatio.
  pwmValue = 255.0 * setpointRatio;
  voltageOut = 5.0 * setpointRatio;  //only valid if you keep the MAX_CONTROL_RANGE_dF to 5!
  analogWrite(REF_VOLTAGE_SETPOINT, pwmValue);
  Serial.print("showSetpoint()...pwmValue: "); Serial.println(pwmValue);
  Serial.print("showSetpoint()...voltage: "); Serial.println(voltageOut);

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
    return oldAverageValue;  //bailout shortcut on first time
  //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 comment:

  1. Hi Banjo,

    Just wanted to say THANK YOU! I've been looking for a way to control a 150L Sous Vide bath (for pasteurisation) with 9kW buzzing through it. I just have to find a way to add data log all the temps and send them over wifi. Now I have to learn to programme :-)

    If you have any thoughts on if your code could be tweaked then please let us know!