Browsing the internet I found another very interesting project, because through it it is possible to hack an old rice cooker and transform it into a robot that produces a series of delicious dishes in addition to rice using the sous vide technique.
Want to make delicious, perfectly cooked food using a robot? Who doesn't? This project will show you how to build your own "Sous viduino," an automated cooker that makes perfect eggs, juicy steaks and tender fish, without all the "slaving over a stove." All of this is possible due to a recent advance in cooking technology, instead of using a frying pan or pan, a "sous vide" (pronounced suu veed) is used, a machine that heats the food in a kind of cross between a jacuzzi and a clay pot.
Sous vide is quickly becoming an important cooking technique in many of the world's top restaurants, as it combines principles of molecular gastronomy with industrial temperature controls to precisely manage chemical cooking reactions.
We love good food as much as we love science and technology, so of course we would have to build our own sous vide controller. This project turns an inexpensive rice cooker into a precision cooking instrument capable of maintaining the cooking temperature within +/- 0.1 C. By precisely controlling the temperature you can ensure that food is cooked exactly to the desired level of cooking and no-more. This makes the sous vide cooking method perfect for everything from a soft-boiled egg to a steak cooked perfectly from edge to edge which is medium rare.
Materials for the project The Controller
You don't need a powerful microcomputer to conduct this setup, we selected Arduino for this project because of the excellent (well documented) PID and autotune libraries available for it. The Adafruit RGB / LCD shield with integrated buttons also has a perfect user interface, making it easier to use.
To build this controller, you will need:
rice machine
Arduino UNO
Proto Shield or Wing Shield
RGB LCD Shield (positive or negative display)
power switch
DS18B20 Temperature Sensor for high temperatures, you can also use the standard DS18B20 Waterproof type if you are not planning on heating it above 90C, which is very rare for a Sous Vide.
Food Grade Heat Pipes
Servo Extension Cable
JST In-Line Power Wire Connector: Male and Female
You will also need some basic tools and soldering materials:
Wire Cutters
Wire strippers
Soldering Iron
Welding
Wrap Tubes
Small Phillips screwdriver
Pan Selection
Type
Slow Cooker rice cookers and appliances have the basic elements we're looking for:
The vessel for putting water
An electric heating element
Capacity
You need a container substantially larger than the food you intend to cook in it. It needs to have a large enough volume of water with plenty of space for circulation while maintaining a uniform temperature. Look for a rice cooker with a capacity of at least 10 cups (20 cups) or a pot with at least a 4-quart capacity.
Controls
Since we are preparing our own control, choose the simplest manual control pan you can find. One with a simple switch or control button would be best. We just want to turn it on high and connect it to our controller.
(Here at Adafruit we use a "Black and Decker RC880" which has a capacity of 24 cups which is a very large size.
Pans with digital controls are not suitable unless you want to rip out the electronics, as the controller works by pulsing the pan's electrical supply. If you do this with a digitally controlled pan, it will just turn off!
Build the controller
Prepare the Sensor
Most sous vide dishes are made with sealed plastic bags, an exception to this is eggs which are cooked in their shells. Other recipes (such as lobster tails cooked in butter) cook the food by submerging it directly in the cooking liquid. To ensure that the temperature sensor is in safe contact with the food, cut a piece of high-temperature plastic tape and place it around the sensor.
Prepare the sensor
Our waterproof DS18B20 sensors are great for immersion in liquid and measuring temperature, but they are not food safe. If you are only going to cook food in a plastic bag it doesn't matter as much, but if you cook food directly in water like eggs it really matters. We strongly suggest using tape to seal the sensor, use a heat gun to shrink the waterproof plastic sleeve over the sensor.
Install the Sensor
Drill or enlarge the steam vent opening so that the sensor cable can be routed through the lid. Position the sensor so that it extends approximately halfway into the sewing container when the lid is closed.
If there is a gap, place a cable tie around the sensor wire and lid outlet to prevent them from sliding deeper into the pan.
Add additional cable ties every 4-6 inches to secure the sensor wire to the power cord.
Terminating the sensor wires
Let's use an extension cable as a way to make a detachable sensor. It is not mandatory but makes it easier to remove for cleaning.
Peel and prepare a few lengths of heat-shrink.
Cut a servo extension cable in half.
Solder the male end to the sensor wires.
Shrink Heat-shrink to insulate.
The color coding sensor wires (right) are a little strange, so we'll wire them together to use a standard color coding for the servo connector (right).
Black <- White (earth)
Red <- Orange Stripe (+5v)
White <- blue stripe (Signal)
Add the Resistor
With the female half of the servo extension:
Strip and tin at the ends
Solder the 47K ohm resistor that comes with it between the 5v (red) and signal (white) wires.
Shrink the heat-shrink to insulate.
Connect the wing shield to the Arduino and the RGB shield LCD. Fix the battery on top of the Electrical box and add some safety clips.
Connect the cables
Connect the JST male power cable to pin 7 and ground on the wing shield.
Connect the female JST cable to the terminal screws in the electrical box. Make sure the polarity is the same as that on the other end.
Connect the male half of the cable to the servo extension:
White -> Pin 2 (Signal)
Red -> Pin 3 (5v)
Black -> Pin 4 (Ground)
Put it all together:
Connect the male and female ends of the servo extension cable.
Connect the male and female JST cables together.
Connect the pan to the electricity connector
Plug the Electrical box into a wall socket.
Now you're ready to load some software!
PID
Before we start cooking we have to solve a simple problem: how to maintain the temperature of the water in the pan at a certain temperature for a long period of time. It's really easy to get water boiling (just heat it until it starts to boil) or freezing (cool it until it solidifies), but maintaining a stable water temperature is a little more difficult. Water cools as it approaches the food and heats it, but how quickly it cools depends on the amount of water, the temperature of the pan, the temperature of the food, etc. It is difficult to do this control manually so we will automate it using PID feedback control
What is a PID?
We need our microcontroller to control the heater to keep the temperature stable. You can heat the water by turning on the rice cooker (control), and then measure the temperature with our waterproof sensor (feedback). These components are in the middle of an algorithm that links them together. The PID algorithm is a type of feedback control, in this application the temperature measurement is compared with the nominal value and the difference between them is called error. The error is used to calculate a setting for the output, which controls the heating element.
The name comes from the PID of the three terms in the equation used to calculate the output:
P - The proportional term looks at the current state of the process. Its value is proportional to the current error.
I - The integral term looks at the history of the process. Its value is the integral of past errors.
D - Derivative tries to predict the future of the process. Its value is the derivative or the rate of change in the error..
These three terms are weights known as assigned tuning parameters. Kp, Ki and Kd The three terms are summed to produce the control output.
Automatic Adjustment
You may have heard of "Auto Tuning" as a way to filter your voice. Auto-tuning a PID controller isn't quite the same thing - instead of improving your singing voice, it can help you set the initial tuning parameters for your controller. Each "system" has different parameters inherent to the physical world. For example, the sous vide controller has to explain how many Watts of power the rice cooker uses, how fast it takes to start, the specific heat of the water, etc.
The autotune function attempts to characterize the performance of your sous vide system by interrupting the controller's output to see how it responds.
Based on the rupture response, the algorithm calculates the adjustment parameters Kp, Ki and Kd. Autotune isn't perfect and it can't find the ideal tuning for your system, but it should get you on your way.
User interface
The sous vide user interface allows you to set the cooking temperature and make PID tuning parameter adjustments. It is implemented as a simple state machine, each state implements the user interface 'screen'. State machines are often used in semi-complex microcontroller projects where you want to do a lot of configurations and activities in the correct order.
The shield buttons are used to navigate between the different screens. After a period of inactivity, this returns the system to the "Run" screen, to display the current set-point (the desired temperature) and current bath temperature.
Each state is implemented as a function that is called by the main loop based on the opState variable. To change states, a state function sets opState to the new state and returns it to the main loop.
If you're working on a project that has a lot of things going on, drawing a state machine can be really helpful to keep your head on track!
/ / ************************************************
// Main control loop
//
// All state changes go through here
/ / ************************************************
void loop()
{
// Wait for button release before changing state
while (ReadButtons (!) = 0) {}
lcd.clear();
Serial.println(opState);
switch (opState)
{
case OFF:
Off ();
to break;
SETP case:
Tune_Sp();
to break;
RUN case:
Run();
to break;
TUNE_P case:
TUNEP ();
to break;
TUNE_I case:
TuneI();
to break;
TUNE_D case:
Tuned();
to break;
}
}
Each State function is responsible for updating the display and monitoring, button presses in addition to navigating between screens are also used to modify control parameters. For example, use the up and down keys to change the set point from the nominal tuning value state, as indicated in the figure and code below.
Button 5 on the shield is used as a "shift" switch. When pressed simultaneously with the up or down keys, it increases or decreases by 10 instead of just 1. The other tuning screens work the same way.
/ / ************************************************
// Nominal state input value
//UP/DOWN to change setpoint
// RIGHT for tuning parameters
// Left to OFF
// Change to 10x setting
/ / ************************************************
empty Tune_Sp()
{
lcd.setBacklight(TEAL);
lcd.print (F ("Set Temperature:"));
buttons uint8_t = 0;
while (true)
{
buttons ReadButtons = ();
float increment = 0.1;
if (buttons and BUTTON_SHIFT)
{
increment * = 10;
}
if (buttons and BUTTON_LEFT)
{
opState = RUN;
to go back;
}
if (buttons and BUTTON_RIGHT)
{
opState = TUNE_P;
to go back;
}
if (buttons and BUTTON_UP)
{
Setpoint + = increment;
delay (200);
}
if (buttons and BUTTON_DOWN)
{
Setpoint - = increment;
delay (200);
}
if ((millis() - lastInput) > 3000) // returns to RUN after 3 seconds of idle
{
opState = RUN;
to go back;
}
lcd.setCursor(0,1);
lcd.print (nominal value);
DoControl();
}
}
Persistent data
You won't have to hard-code tuning parameters or type them in every time you use the controller, they will be saved in the Arduino's EEPROM. This makes the code a little more elegant, programming the controller just once, whenever you turn on your controller, it will remember the settings from the last time you used it and if you change the settings, they will be saved.
EEPROM can only be written a finite number of times (typically 100,000), we compare the contents before writing and will only write if something has changed. This functionality is implemented in the following helper functions:
// ************************************************
// Save any parameter changes to EEPROM
// ************************************************
void SaveParameters()
{
if (Setpoint != EEPROM_readDouble(SpAddress)) { EEPROM_writeDouble(SpAddress, Setpoint); } if (Kp != EEPROM_readDouble(KpAddress)) { EEPROM_writeDouble(KpAddress, Kp); } if (Ki != EEPROM_readDouble(KiAddress)) { EEPROM_writeDouble(KiAddress, Ki); } if (Kd != EEPROM_readDouble(KdAddress)) { EEPROM_writeDouble(KdAddress, Kd); }
}
// ************************************************
// Load parameters from EEPROM
// ************************************************
void LoadParameters()
{
// Load from EEPROM
Setpoint = EEPROM_readDouble(SpAddress); Kp = EEPROM_readDouble(KpAddress); Ki = EEPROM_readDouble(KiAddress); Kd = EEPROM_readDouble(KdAddress); // Use defaults if EEPROM values are invalid if (isnan(Setpoint)) { Setpoint = 60; } if (isnan(Kp)) { Kp = 500; } if (isnan(Ki)) { Ki = 0.5; } if (isnan(Kd)) { Kd = 0.1; }
}
// ************************************************
// Write floating point values to EEPROM
// ************************************************
void EEPROM_writeDouble(int address, double value)
{
byte* p = (byte*)(void*)&value; for (int i = 0; i < sizeof(value); i++) { EEPROM.write(address++, *p++); }
}
// ************************************************
// Read floating point values from EEPROM
// ************************************************
double EEPROM_readDouble(int address)
{
double value = 0.0; byte* p = (byte*)(void*)&value; for (int i = 0; i < sizeof(value); i++) { *p++ = EEPROM.read(address++); } return value;
}
Proportional output time
While pan heating is controlled by a relay, we cannot use a standard PWM output to control it. PWM is a very easy and accurate way to control heating, but it requires a more expensive SSR. There are some PID feedback systems that benefit from PWM control. Fortunately, our system is a large bathtub of water and the water heats and cools very slowly, due to this "thermal mass" of the system, the response time is relatively slow, so a very slow form of PWM known as "proportional output time". In this case, the frequency of the pulses is 0.1 Hz or once very, 10 seconds, it is really a very, very slow PWM scale.
We need to control the pulse time precisely, so that they are not affected by any delays that may exist in the main circuit, so we will use a timer to generate a periodic interruption. The timer is initialized in setup():
//Run timer2 interrupt every 15 ms
TCCR2A = 0;
TCCR2B = 1 << CS22 | 1 << CS21 | 1 << CS20;
//Timer2 Overflow Interrupt Enable
TIMSK2 | TOIE2 << = 1;
The interrupt service routine is called once every 15 milliseconds to update the relay output.
/ / ************************************************
// Interrupt Timer Handler
/ / ************************************************
SIGNAL (TIMER2_OVF_vect)
{
if (opState == OFF)
{
digitalWrite(RelayPin, LOW) // make sure relay is out
}
other
{
DriveOutput();
}
}
The DriveOutput() function that implements proportional output timing.
/ / ************************************************
// Called by the ISR every 15ms to trigger the output
/ / ************************************************
empty DriveOutput()
{
time now = millis();
// Set the output
// "In time" is proportional to the PID output
if (now - windowStartTime> WindowSize)
{/ / Time to change the Relay window
windowStartTime + = WindowSize;
}
if ((ONTIME> 100) && (ONTIME> (now - windowStartTime)))
{
digitalWrite(RelayPin, HIGH);
}
other
{
digitalWrite(RelayPin, LOW);
}
}
Putting it all together!
Here is the complete sketch for the Adafruit Sous Vide Controller
You can also get the latest code (which may have updates or improvements) from Github at https://github.com/adafruit/Sous_Viduino
//------------------------------------------------ -------------------
//
// Sous Vide Controller
// Bill Earl - for Adafruit Industries
//
// Based on the Arduino PID and PID AutoTune Libraries
// by Brett Beauregard
//------------------------------------------------ ------------------
// PID Library
#include <PID_v1.h>
#include <PID_AutoTune_v0.h>
// Libraries for the Adafruit RGB/LCD Shield
#include <Wire.h>
#include <Adafruit_MCP23017.h>
#include <Adafruit_RGBLCDShield.h>
// Libraries for the DS18B20 Temperature Sensor
#include <OneWire.h>
#include <DallasTemperature.h>
// So we can save and retrieve settings
#include <EEPROM.h>
// ************************************************
// Pin definitions
// ************************************************
// Output Relay
#define RelayPin 7
// One-Wire Temperature Sensor
// (Use GPIO pins for power/ground to simplify the wiring)
#define ONE_WIRE_BUS 2
#define ONE_WIRE_PWR 3
#define ONE_WIRE_GND 4
// ************************************************
// PID Variables and constants
// ************************************************
//Define Variables we'll be connecting to
double Setpoint;
doubleInput;
double Output;
volatile long onTime = 0;
// pid tuning parameters
doubleKp;
doubleKi;
doubleKd;
// EEPROM addresses for persisted data
const int SpAddress = 0;
const int KpAddress = 8;
const int KiAddress = 16;
const int KdAddress = 24;
//Specify the links and initial tuning parameters
PID myPID(&Input, &Output, &Setpoint, Kp, Ki, Kd, DIRECT);
// 10 second Time Proportional Output window
int WindowSize = 10000;
unsigned long windowStartTime;
// ************************************************
// Auto Tune Variables and constants
// ************************************************
byte ATuneModeRemember=2;
double aTuneStep=500;
double aTuneNoise=1;
unsigned int aTuneLookBack=20;
boolean tuning = false;
PID_ATune aTune(&Input, &Output);
// ************************************************
// DiSplay Variables and constants
// ************************************************
Adafruit_RGBLCDShield lcd = Adafruit_RGBLCDShield();
// These #defines make it easy to set the backlight color
#define RED 0x1
#define YELLOW 0x3
#define GREEN 0x2
#define TEAL 0x6
#define BLUE 0x4
#define VIOLET 0x5
#define WHITE 0x7
#define BUTTON_SHIFT BUTTON_SELECT
unsigned long lastInput = 0; // last button press
byte degree[8] = // defines the degree symbol
{
B00110,
B01001,
B01001,
B00110,
B00000,
B00000,
B00000,
B00000
};
const int logInterval = 10000; // log every 10 seconds
long lastLogTime = 0;
// ************************************************
// States for state machine
// ************************************************
enum operatingState { OFF = 0, SETP, RUN, TUNE_P, TUNE_I, TUNE_D, AUTO};
operatingState opState = OFF;
// ************************************************
// Sensor Variables and constants
// Data wire is plugged into port 2 on the Arduino
// Setup a oneWire instance to communicate with any OneWire devices (not just Maxim/Dallas temperature ICs)
OneWire oneWire(ONE_WIRE_BUS);
// Pass our oneWire reference to Dallas Temperature.
DallasTemperature sensors(&oneWire);
// arrays to hold device addresses
DeviceAddress tempSensor;
// ************************************************
// Setup and display initial screen
// ************************************************
void setup()
{
Serial.begin(9600);
// Initialize Relay Control:
pinMode(RelayPin, OUTPUT); // Output mode to drive relay
digitalWrite(RelayPin, LOW); // make sure it is off to start
// Set up Ground & Power for the sensor from GPIO pins
pinMode(ONE_WIRE_GND, OUTPUT);
digitalWrite(ONE_WIRE_GND, LOW);
pinMode(ONE_WIRE_PWR, OUTPUT);
digitalWrite(ONE_WIRE_PWR, HIGH);
// Initialize LCD DiSplay
lcd.begin(16, 2);
lcd.createChar(1, degree); // create degree symbol from the binary
lcd.setBacklight(VIOLET);
lcd.print(F(" Adafruit"));
lcd.setCursor(0, 1);
lcd.print(F(" Sous Vide!"));
// Start up the DS18B20 One Wire Temperature Sensor
sensors.begin();
if (!sensors.getAddress(tempSensor, 0))
{
lcd.setCursor(0, 1);
lcd.print(F("Sensor Error"));
}
sensors.setResolution(tempSensor, 12);
sensors.setWaitForConversion(false);
delay(3000); // splash screen
// Initialize the PID and related variables
LoadParameters();
myPID.SetTunings(Kp,Ki,Kd);
myPID.SetSampleTime(1000);
myPID.SetOutputLimits(0, WindowSize);
// Run timer2 interrupt every 15 ms
TCCR2A = 0;
TCCR2B = 1<<CS22 | 1<<CS21 | 1<<CS20;
//Timer2 Overflow Interrupt Enable
TIMSK2 |= 1<<TOIE2;
}
// ************************************************
// Timer Interrupt Handler
// ************************************************
SIGNAL(TIMER2_OVF_vect)
{
if (opState == OFF)
{
digitalWrite(RelayPin, LOW); // make sure relay is off
}
else
{
DriveOutput();
}
}
// ************************************************
// Main Control Loop
//
// All state changes pass through here
// ************************************************
void loop()
{
// wait for button release before changing state
while(ReadButtons() != 0) {}
lcd.clear();
switch (opState)
{
case OFF:
Off();
break;
SETP case:
Tune_Sp();
break;
case RUN:
Run();
break;
case TUNE_P:
TuneP();
break;
case TUNE_I:
TuneI();
break;
case TUNE_D:
TuneD();
break;
}
}
// ************************************************
// Initial State - press RIGHT to enter setpoint
// ************************************************
void Off()
{
myPID.SetMode(MANUAL);
lcd.setBacklight(0);
digitalWrite(RelayPin, LOW); // make sure it is off
lcd.print(F(" Adafruit"));
lcd.setCursor(0, 1);
lcd.print(F(" Sous Vide!"));
uint8_t buttons = 0;
while(!(buttons & (BUTTON_RIGHT)))
{
buttons = ReadButtons();
}
// Prepare to transition to the RUN state
sensors.requestTemperatures(); // Start an asynchronous temperature reading
//turn the PID on
myPID.SetMode(AUTOMATIC);
windowStartTime = millis();
opState = RUN; // start control
}
// ************************************************
// Setpoint Entry State
// UP/DOWN to change setpoint
// RIGHT for tuning parameters
// LEFT for OFF
// SHIFT for 10x tuning
// ************************************************
void Tune_Sp()
{
lcd.setBacklight(TEAL);
lcd.print(F("Set Temperature:"));
uint8_t buttons = 0;
while(true)
{
buttons = ReadButtons();
float increment = 0.1;
if (buttons & BUTTON_SHIFT)
{
increment *= 10;
}
if (buttons & BUTTON_LEFT)
{
opState = RUN;
return;
}
if (buttons & BUTTON_RIGHT)
{
opState = TUNE_P;
return;
}
if (buttons & BUTTON_UP)
{
Setpoint += increment;
delay(200);
}
if (buttons & BUTTON_DOWN)
{
Setpoint -= increment;
delay(200);
}
if ((millis() - lastInput) > 3000) // return to RUN after 3 seconds idle
{
opState = RUN;
return;
}
lcd.setCursor(0,1);
lcd.print(Setpoint);
lcd.print(" ");
DoControl();
}
}
// ************************************************
// Proportional Tuning State
// UP/DOWN to change Kp
// RIGHT for Ki
// LEFT for setpoint
// SHIFT for 10x tuning
// ************************************************
void TuneP()
{
lcd.setBacklight(TEAL);
lcd.print(F("Set Kp"));
uint8_t buttons = 0;
while(true)
{
buttons = ReadButtons();
float increment = 1.0;
if (buttons & BUTTON_SHIFT)
{
increment *= 10;
}
if (buttons & BUTTON_LEFT)
{
opState = SETP;
return;
}
if (buttons & BUTTON_RIGHT)
{
opState = TUNE_I;
return;
}
if (buttons & BUTTON_UP)
{
Kp += increment;
delay(200);
}
if (buttons & BUTTON_DOWN)
{
Kp -= increment;
delay(200);
}
if ((millis() - lastInput) > 3000) // return to RUN after 3 seconds idle
{
opState = RUN;
return;
}
lcd.setCursor(0,1);
lcd.print(Kp);
lcd.print(" ");
DoControl();
}
}
// ************************************************
// Integral Tuning State
// UP/DOWN to change Ki
// RIGHT for Kd
// LEFT for Kp
// SHIFT for 10x tuning
// ************************************************
void TuneI()
{
lcd.setBacklight(TEAL);
lcd.print(F("Set Ki"));
uint8_t buttons = 0;
while(true)
{
buttons = ReadButtons();
float increment = 0.01;
if (buttons & BUTTON_SHIFT)
{
increment *= 10;
}
if (buttons & BUTTON_LEFT)
{
opState = TUNE_P;
return;
}
if (buttons & BUTTON_RIGHT)
{
opState = TUNE_D;
return;
}
if (buttons & BUTTON_UP)
{
Ki += increment;
delay(200);
}
if (buttons & BUTTON_DOWN)
{
Ki -= increment;
delay(200);
}
if ((millis() - lastInput) > 3000) // return to RUN after 3 seconds idle
{
opState = RUN;
return;
}
lcd.setCursor(0,1);
lcd.print(Ki);
lcd.print(" ");
DoControl();
}
}
// ************************************************
// Derivative Tuning State
// UP/DOWN to change Kd
// RIGHT for setpoint
// LEFT for Ki
// SHIFT for 10x tuning
// ************************************************
void TuneD()
{
lcd.setBacklight(TEAL);
lcd.print(F("Set Kd"));
uint8_t buttons = 0;
while(true)
{
buttons = ReadButtons();
float increment = 0.01;
if (buttons & BUTTON_SHIFT)
{
increment *= 10;
}
if (buttons & BUTTON_LEFT)
{
opState = TUNE_I;
return;
}
if (buttons & BUTTON_RIGHT)
{
opState = RUN;
return;
}
if (buttons & BUTTON_UP)
{
Kd += increment;
delay(200);
}
if (buttons & BUTTON_DOWN)
{
Kd -= increment;
delay(200);
}
if ((millis() - lastInput) > 3000) // return to RUN after 3 seconds idle
{
opState = RUN;
return;
}
lcd.setCursor(0,1);
lcd.print(Kd);
lcd.print(" ");
DoControl();
}
}
// ************************************************
// PID CONTROL STATE
// SHIFT and RIGHT for autotune
// RIGHT - Setpoint
// LEFT - OFF
// ************************************************
void Run()
{
// set up the LCD's number of rows and columns:
lcd.print(F("Sp: "));
lcd.print(Setpoint);
lcd.write(1);
lcd.print(F("C : "));
SaveParameters();
myPID.SetTunings(Kp,Ki,Kd);
uint8_t buttons = 0;
while(true)
{
setBacklight(); // set backlight based on state
buttons = ReadButtons();
if ((buttons & BUTTON_SHIFT)
&& (buttons & BUTTON_RIGHT)
&& (abs(Input - Setpoint) < 0.5)) // Should be at steady-state
{
StartAutoTune();
}
else if (buttons & BUTTON_RIGHT)
{
opState = SETP;
return;
}
else if (buttons & BUTTON_LEFT)
{
opState = OFF;
return;
}
DoControl();
lcd.setCursor(0,1);
lcd.print(Input);
lcd.write(1);
lcd.print(F("C : "));
float pct = map(Output, 0, WindowSize, 0, 1000);
lcd.setCursor(10,1);
lcd.print(F(" "));
lcd.setCursor(10,1);
lcd.print(pct/10);
//lcd.print(Output);
lcd.print("%");
lcd.setCursor(15,0);
if (tuning)
{
lcd.print("T");
}
else
{
lcd.print(" ");
}
// periodically log to serial port in csv format
if (millis() - lastLogTime > logInterval)
{
Serial.print(Input);
Serial.print(",");
Serial.println(Output);
}
delay(100);
}
}
// ************************************************
// Execute the control loop
// ************************************************
void DoControl()
{
// Read the input:
if (sensors.isConversionAvailable(0))
{
Input = sensors.getTempC(tempSensor);
sensors.requestTemperatures(); // prime the pump for the next one - but don't wait
}
if (tuning) // run the auto-tuner
{
if (aTune.Runtime()) // returns 'true' when done
{
FinishAutoTune();
}
}
else // Execute control algorithm
{
myPID.Compute();
}
// Time Proportional relay state is updated regularly via timer interrupt.
onTime = Output;
}
// ************************************************
// Called by ISR every 15ms to drive the output
// ************************************************
void DriveOutput()
{
long now = millis();
// Set the output
// "on time" is proportional to the PID output
if(now - windowStartTime>WindowSize)
{ //time to shift the Relay Window
windowStartTime += WindowSize;
}
if((onTime > 100) && (onTime > (now - windowStartTime)))
{
digitalWrite(RelayPin,HIGH);
}
else
{
digitalWrite(RelayPin,LOW);
}
}
// ************************************************
// Set Backlight based on the state of control
// ************************************************
void setBacklight()
{
if (tuning)
{
lcd.setBacklight(VIOLET); // TuningMode
}
else if (abs(Input - Setpoint) > 1.0)
{
lcd.setBacklight(RED); // High Alarm - off by more than 1 degree
}
else if (abs(Input - Setpoint) > 0.2)
{
lcd.setBacklight(YELLOW); // Low Alarm - off by more than 0.2 degrees
}
else
{
lcd.setBacklight(WHITE); // We're on target!
}
}
// ************************************************
// Start the Auto-Tuning cycle
// ************************************************
void StartAutoTune()
{
// REMEMBER the mode we were in
ATuneModeRemember = myPID.GetMode();
// set up the auto-tune parameters
aTune.SetNoiseBand(aTuneNoise);
aTune.SetOutputStep(aTuneStep);
aTune.SetLookbackSec((int)aTuneLookBack);
tuning = true;
}
// ************************************************
// Return to normal control
// ************************************************
void FinishAutoTune()
{
tuning = false;
// Extract the auto-tune calculated parameters
Kp = aTune.GetKp();
Ki = aTune.GetKi();
Kd = aTune.GetKd();
// Re-tune the PID and revert to normal control mode
myPID.SetTunings(Kp,Ki,Kd);
myPID.SetMode(ATuneModeRemember);
// Persist any changed parameters to EEPROM
SaveParameters();
}
// ************************************************
// Check buttons and time-stamp the last press
// ************************************************
uint8_t ReadButtons()
{
uint8_t buttons = lcd.readButtons();
if (buttons != 0)
{
lastInput = millis();
}
return buttons;
}
// ************************************************
// Save any parameter changes to EEPROM
// ************************************************
void SaveParameters()
{
if (Setpoint != EEPROM_readDouble(SpAddress))
{
EEPROM_writeDouble(SpAddress, Setpoint);
}
if (Kp != EEPROM_readDouble(KpAddress))
{
EEPROM_writeDouble(KpAddress, Kp);
}
if (Ki != EEPROM_readDouble(KiAddress))
{
EEPROM_writeDouble(KiAddress, Ki);
}
if (Kd != EEPROM_readDouble(KdAddress))
{
EEPROM_writeDouble(KdAddress, Kd);
}
}
// ************************************************
// Load parameters from EEPROM
// ************************************************
void LoadParameters()
{
// Load from EEPROM
Setpoint = EEPROM_readDouble(SpAddress);
Kp = EEPROM_readDouble(KpAddress);
Ki = EEPROM_readDouble(KiAddress);
Kd = EEPROM_readDouble(KdAddress);
// Use defaults if EEPROM values are invalid
if (isnan(Setpoint))
{
Setpoint = 60;
}
if (isnan(Kp))
{
Kp = 850;
}
if (isnan(Ki))
{
Ki = 0.5;
}
if (isnan(Kd))
{
Kd = 0.1;
}
}
// ************************************************
// Write floating point values to EEPROM
// ************************************************
void EEPROM_writeDouble(int address, double value)
{
byte* p = (byte*)(void*)&value;
for (int i = 0; i < sizeof(value); i++)
{
EEPROM.write(address++, p++);
}
}
// ************************************************
// Read floating point values from EEPROM
// ************************************************
double EEPROM_readDouble(int address)
{
double value = 0.0;
byte p = (byte*)(void*)&value;
for (int i = 0; i < sizeof(value); i++)
{
*p++ = EEPROM.read(address++);
}
return value;
}
Adjusting
Standard fit
The default tuning parameters in the sketch are average values of a few different rice cooker models. But there will be variations even between two pans of the same model and the same manufacturer.
Auto Tuning
The automatic sketch adjustment function can determine "approximate" parameters for your pan. You can refine the tuning from there. To use the Autotune function, first let the pan preheat and reach the set temperature. Self-tuning works best if the system is already in a state of equilibrium. The "Run" state will not allow you to invoke the auto-tuner unless you are within 0.5 degrees of the set temperature. Once the pan has stabilized at or near the set point, press the RIGHT and SELECT buttons simultaneously. The backlight will turn violet to indicate you are in auto adjustment mode.
Be patient as the system tunes itself. It will typically take an hour or more for the auto-tune algorithm to complete. When self-adjustment is complete, the backlight will return to normal color mode. The auto-tuned parameters will be saved in EEPROM memory, so they will be ready for the next time you use it.
Manual tuning
As mentioned earlier, auto-tuning is not perfect. With a little practice, you can probably get closer to the ideal setting for your stove. There are many good resources on the web that explain PID tuning.
Manual Tuning Tip
One thing to be aware of is that temperature control in a rice cooker is non-linear and asymmetrical. You can apply heat, but there is no active cooling. As a result, most rice cookers take a long time to recover from a temperature overshoot. For this reason, it is generally best to look for an overdamped response to avoid overflow.
Cook with it!
The best part about making your own sous vide setup is the testing portion, yum! Sous vide uses the lowest of normal cooking temperatures. If not done carefully, this can create conditions that promote the growth of harmful bacteria. For an excellent guide to sous vide safe cooking temperatures and food handling practices, as well as time and temperature charts
Cook a "perfect" egg!
A good first test for your new stove is a "perfect egg." Eggs do not require special preparation for sous vide. And, since they are very sensitive to small variations in cooking temperature, they are a good test of the accuracy of your temperature controller. Different chefs have different ideas about what exactly constitutes a perfect egg. But your sous vide cooker's precise temperature control will let you achieve your perfect egg every time.