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:
- a consumer grade product that I purchased - the SousVide Supreme
- For those of you unfamiliar with Sous Vide, have a look at my previously-published posts:The science behind Sous Vide cooking and More on Sous Vide cooking
- Here's an excellent overall site for Sous Vide, and also an excellent book: Douglas Baldwin's site. He also has some excellent Youtube videos.
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.
SOFTWARE LISTING
//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
//
// HARDWARE:
// - This program makes use of the Thermocouple breakout board from www.adafruit.com 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: // www.ladyada.net/learn/sensors/thermocouple.html
// 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
const unsigned long SENSOR_READ_DELAY_TIME = QUARTER_SECOND;
//heater controlled by temperature
#include
//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 LOWEST_TEMPERATURE_SETPOINT = 130.0;
const float LOWEST_TEMPERATURE_DISPLAY = 100.0;
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
//**** http://www.douglasbaldwin.com/index.html 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:
//**** https://www.adafruit.com/products/268 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
const int TEMPERATURE_AVERAGING_ARRAY_SIZE = 10 * (ONE_SECOND/SENSOR_READ_DELAY_TIME); //Samples to be averaged. 10 SECONDS WORTH OF AVERAGES
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() {
Serial.begin(9600);
// initialize the digital pin as an output.
// Pin 13 has an LED connected on most Arduino boards:
pinMode(UNO_LED_PIN, OUTPUT);
pinMode(STATE_LED_PIN, OUTPUT);
pinMode(OFF_LED_PIN, OUTPUT);
pinMode(HEATER_OUTPUT_PIN, OUTPUT);
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
lowest_temperature_setpoint_ratio = (LOWEST_TEMPERATURE_SETPOINT - LOWEST_TEMPERATURE_DISPLAY) / MAX_CONTROL_RANGE_dF;
// wait for MAX chip to stabilize
delay(500);
Serial.println("setup()...Entry & Exit");
}
void loop() {
Serial.println("loop()...Entry");
//digitalWrite(ONBOARD_LED_PIN, HIGH); // set the LED on
turnHeaterOff();
turnHeaterOn();
Serial.println("loop()...Exit");
}
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();
Serial.println("turnHeaterOn()...temperature controlled...entry.......................");
do {
doCommonCalcs(&temperature, &previousTemperature, &setpointRatio, &setpointValue);
heaterOn();
delay(SENSOR_READ_DELAY_TIME);
} while(temperature < (setpointValue+DEADBAND_dF));
Serial.println("turnHeaterOn()...exit");
}
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();
Serial.println("turnHeaterOff()...temperature controlled...entry......................");
do {
doCommonCalcs(&temperature, &previousTemperature, &setpointRatio, &setpointValue);
heaterOff();
delay(SENSOR_READ_DELAY_TIME);
} while(temperature > (setpointValue-DEADBAND_dF));
Serial.println("turnHeaterOff()...exit");
}
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);
element++;
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!");
//TURN OFF!
digitalWrite(HEATER_OUTPUT_PIN, HEATER_OFF);
digitalWrite(OFF_LED_PIN, LOW);
Serial.print("alarm()....sensor low. sensorValueCounts: "); Serial.println(sensorValueCounts);
while(1) {
//keep turning heater off!
digitalWrite(HEATER_OUTPUT_PIN, HEATER_OFF);
//show it by alternating blinking both LEDs
digitalWrite(UNO_LED_PIN, HIGH);
digitalWrite(STATE_LED_PIN, HIGH);
digitalWrite(OFF_LED_PIN, LOW);
delay(ALARM_COUNTS);
digitalWrite(UNO_LED_PIN,LOW);
digitalWrite(STATE_LED_PIN, LOW);
digitalWrite(OFF_LED_PIN, HIGH);
delay(ALARM_COUNTS);
}
}
Hi Banjo,
ReplyDeleteJust 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!
Jon