Progress has slowed up considerably Marksy as I have been forced to turn my attention away to less important things (namely uni). I have have distracted myself with other silly projects such as aqauponics and cider. But I really like where the controller is going. I have a few more ideas on features to add but they are in the planning phase.
Here is my code so far:
//-------------------------------------------------------------------
//
// Brewing Kettle Controller adapated from Sous Vide Controller (Bill Earl - for Adafruit Industries)
// Michael Ah-Cann
//
// 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 LCD Sheild
#include <Wire.h> //is this required for project??
#include <LiquidCrystal.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 (PowerSwitch Tail)
#define RelayPin 3
// One-Wire Temperature Sensor
// (Use GPIO (General Purpose In Out) pins for power (and ground) to simplify the wiring)
#define ONE_WIRE_BUS 11
#define ONE_WIRE_PWR 12
//#define ONE_WIRE_GND 13 //COMMENTED OUT TO ACCOMONDATE SPEAKER
// ************************************************
// Timer Variables
// ************************************************
//Define the variables used for the timer
int sound_pin = 13;
int set_time = 0;
float time_hold;
int buttons;
int time_remain;
float elapsed = 0.00;
boolean alarm = false;
boolean sound_state = true;
boolean time_state = false;
// ************************************************
// PID Variables and constants
// ************************************************
//Define Variables we'll be connecting to
double Setpoint;
double Input;
double Output;
volatile long onTime = 0;
// pid tuning parameters
double Kp;
double Ki;
double Kd;
// 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
// ************************************************
LiquidCrystal lcd(8,9,4,5,6,7);
unsigned long lastInput = 0; // last button press
byte degree[8] = // define 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, TUNE_T, 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 address
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); // COMMENTED OUT TO ACCOMONDATE SPEAKER
//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.setCursor(0,0);
lcd.print("Welcome Michael");
lcd.setCursor(0, 1);
lcd.print("Initializing..");
delay(1000); //Splash Screen
// Start up the DS18B20 One Wire Temperature Sensor
sensors.begin();
if (!sensors.getAddress(tempSensor, 0))
{
lcd.setCursor(0, 1);
lcd.print("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(analogRead(0) < 1000) {}
lcd.clear();
switch (opState)
{
case OFF:
Off();
break;
case SETP:
Tune_Sp();
break;
case RUN:
Run();
break;
case TUNE_P:
TuneP();
break;
case TUNE_I:
TuneI();
break;
case TUNE_D:
TuneD();
break;
case TUNE_T:
Timer();
break;
}
}
// ************************************************
// Initial State - press RIGHT to enter setpoint
// ************************************************
void Off()
{
myPID.SetMode(MANUAL);
digitalWrite(RelayPin, LOW); // make sure it is off
lcd.print("Brew Kettle");
lcd.setCursor(0, 1);
lcd.print("Controller");
int buttons = analogRead(0);
while(buttons != 0)
{
buttons = analogRead(0);
}
// 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
}
// ************************************************
// Set Timer
// UP/DOWN to change timing length
// LEFT for OFF
// SELECT to BEGIN
// ************************************************
void Timer()
{
if (time_state == false)
{
lcd.clear();
lcd.print("Timer Set:");
lcd.setCursor(0,1);
lcd.print("Select to begin");
elapsed = 0.00;
time_hold = 0.00;
while (true)
{
buttons = analogRead(0);
if (buttons == 480) //left button
{
opState = RUN;
return;
}
if (buttons == 721 && set_time != 0) // begin timer
{
time_hold = millis()/60000.00; // Time stamp decimal minutes
time_state = true; // Enter in to timer state
opState = TUNE_T;
return;
}
if (buttons == 131) // Increase time
{
set_time += 1;
delay(200);
}
if (buttons == 306 && set_time != 0) // Decrease time but not below 0
{
set_time -= 1;
delay(200);
}
lcd.setCursor(10,0);
lcd.print(set_time);
lcd.print("min ");
DoControl();
}
}
else if (time_state == true)
{
if (alarm == true)
{
lcd.clear();
lcd.print("Timer Completed");
lcd.setCursor(0,1);
lcd.print("Select to Reset");
while (true)
{
buttons = analogRead(0);
if (buttons == 721)
{
time_state = false;
alarm = false;
opState = TUNE_T;
return;
}
DoControl();
delay(100);
}
}
else
{
lcd.clear();
lcd.print("Time Remaining:");
while (true)
{
buttons = analogRead(0);
if (buttons == 480) //left button
{
opState = RUN;
return;
}
if (buttons == 721) //Press Select to reset timer
{
time_state = false;
opState = TUNE_T;
return;
}
if (alarm == true)
{
opState = TUNE_T;
return;
}
time_remain = set_time - elapsed;
lcd.setCursor(0,1);
lcd.print(time_remain);
lcd.print("min ");
DoControl();
}
}
}
}
// ************************************************
// Setpoint Entry State
// UP/DOWN to change setpoint
// RIGHT for tuning parameters
// LEFT for OFF
// SELECT for toggling between 0.1 and 1 increments
// ************************************************
void Tune_Sp()
{
//Serial.print("Tuning Setpoint"); //debugging checkpoint
lcd.print("Set Temperature:");
int buttons = analogRead(0);
boolean increment_toggle = LOW;
float increment = 0.1;
while(true)
{
buttons = analogRead(0);
if (buttons == 721) //select button
{
if (increment_toggle == LOW)
{
increment = 1;
increment_toggle = HIGH;
delay(200);
}
else if (increment_toggle == HIGH)
{
increment = 0.1;
increment_toggle = LOW;
delay(200);
}
}
if (buttons == 480) //left button
{
opState = RUN;
return;
}
if (buttons == 0) //right button
{
opState = TUNE_P;
return;
}
if (buttons == 131)
{
Setpoint += increment;
delay(200);
}
if (buttons == 306)
{
Setpoint -= increment;
delay(200);
}
/* ************************************************************************************ Timer to be debugged
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.print("Set Kp");
int buttons = analogRead(0);
float increment = 1.0;
boolean increment_toggle = LOW;
while(true)
{
buttons = analogRead(0);
if (buttons == 721) //select button
{
if (increment_toggle == LOW)
{
increment = 10.0;
increment_toggle = HIGH;
delay(200);
}
else if (increment_toggle == HIGH)
{
increment = 1.0;
increment_toggle = LOW;
delay(200);
}
}
if (buttons == 480)
{
opState = SETP;
return;
}
if (buttons == 0)
{
opState = TUNE_I;
return;
}
if (buttons == 131)
{
Kp += increment;
delay(200);
}
if (buttons == 306)
{
Kp -= increment;
delay(200);
}
/* ************************************************************************************ Timer to be debugged
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.print("Set Ki");
int buttons = analogRead(0);
float increment = 0.01;
boolean increment_toggle = LOW;
while(true)
{
buttons = analogRead(0);
if (buttons == 721) //select button
{
if (increment_toggle == LOW)
{
increment = 0.10;
increment_toggle = HIGH;
delay(200);
}
else if (increment_toggle == HIGH)
{
increment = 0.01;
increment_toggle = LOW;
delay(200);
}
}
if (buttons == 480)
{
opState = TUNE_P;
return;
}
if (buttons == 0)
{
opState = TUNE_D;
return;
}
if (buttons == 131)
{
Ki += increment;
delay(200);
}
if (buttons ==306)
{
Ki -= increment;
delay(200);
}
/* ************************************************************************************ Timer to be debugged
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.print(F("Set Kd"));
int buttons = analogRead(0);
boolean increment_toggle = LOW;
float increment = 0.01;
while(true)
{
buttons = analogRead(0);
if (buttons == 721) //select button
{
if (increment_toggle == LOW)
{
increment = 0.10;
increment_toggle = HIGH;
delay(200);
}
else if (increment_toggle == HIGH)
{
increment = 0.01;
increment_toggle = LOW;
delay(200);
}
}
if (buttons == 480)
{
opState = TUNE_I;
return;
}
if (buttons == 0)
{
opState = RUN;
return;
}
if (buttons == 131)
{
Kd += increment;
delay(200);
}
if (buttons == 306)
{
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
// SELECT for autotune
// RIGHT - Setpoint
// LEFT - OFF
// ************************************************
void Run()
{
//Serial.print("Checkpoint Run"); //debugging checkpoint
// set up the LCD's number of rows and columns:
lcd.print("Sp: ");
lcd.print(Setpoint);
lcd.write(1);
lcd.print("C : ");
SaveParameters();
myPID.SetTunings(Kp,Ki,Kd);
int buttons = analogRead(0);
while(true)
{
buttons = analogRead(0);
if ((buttons == 721)
&& (abs(Input - Setpoint) < 0.5)) // Should be at steady-state
{
StartAutoTune();
}
else if (buttons == 0)
{
opState = SETP;
//Serial.println("Run Return to SETP"); //debugging checkpoint
return;
}
else if (buttons == 480)
{
opState = OFF;
//Serial.println("Run Return to Off"); //debugging checkpoint
return;
}
else if (buttons == 131)
{
opState = TUNE_T;
return;
}
DoControl();
lcd.setCursor(0,1);
lcd.print(Input);
lcd.write(1);
lcd.print("C : ");
float pct = map(Output, 0, WindowSize, 0, 1000);
lcd.setCursor(10,1);
lcd.print((" "));
lcd.setCursor(10,1);
lcd.print(pct/10);
//lcd.print(Output);
lcd.print("%");
lcd.setCursor(15,0);
if (tuning)
{
lcd.print("T");
//Serial.print("Run - Tuning Active"); debugging active
}
else
{
lcd.print(" ");
}
// periodically log to serial port in csv format
if (millis() - lastLogTime > logInterval)
{
Serial.print(Input);
Serial.print(",");
Serial.print(Output);
Serial.print(",");
Serial.println(millis());
}
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;
if (time_state == true)
elapsed = (millis()/60000.00) - time_hold;
{
if (elapsed > set_time)
{
alarm = true;
if (sound_state = true)
{
tone(sound_pin, 800, 200);
delay(200);
sound_state = false;
}
else
{
sound_state = true;
delay(200);
}
}
}
}
// ************************************************
// 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);
}
}
// ************************************************
// 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();
}
// ************************************************
// 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;
}
And thats it! Changes mostly related to the hardware (LCD, relay, etc) I had, and the changes to pin allocations. If youre still here I might just describe how it works just a little. So it begins with a splash screen in which leads to the the "off" screen. To go to the next screen you press the right button to go to the "on screen". This activates relay control, and temperature monitoring. From here you have three choices, left to go back to "off", up to set the timer, and right again to go into "tuning SP". Tuning SP allows you to set the set point temperature which the arduino will aim to get to and stablize at. You can press up or down to adjust set point up or down, and pressing select increases the up and down adjustment by a factor of ten. Once the setpoint is adjusted to your liking you may either go back by pressing left, or you can tune the PID control variables by pressing right. Pressing right once you go into a screen to tune Kd in the same way you adjusted setpoint, by pressing twice you can adjust Ti, and by pressing right a third time you may adjust Td. From this last screen, if you press right again you will end back up in the "run section". Other features that i wont bother to describe are the alarm and the autotuning function for the PID control variables. The timer works well but the autotuner is still requiring some adjustment.
If anyone was thinking of trying this here are some of the parts i used:
Arduino Uno (or similar rip-off on ebay): - i think its good to support the original though
http://www.australianrobotics.com.au/products/arduino-uno-r3
LCD Screen Shield with Pushbuttons:
http://www.dx.com/p/2-6-lcd-keypad-shield-for-arduino-green-black-161359#.UzGWhfmSyek
DS18B20 Temperature Sensor: - this one has a teflon coat is safer to use in food at higher temperatures
https://core-electronics.com.au/store/index.php/sensors-modules/temperature/high-temp-waterproof-ds18b20-digital-temperature-sensor-extras.html
The relay to control mains power to the element: (requires soldering and caution!!)
https://www.sparkfun.com/products/11042
Protoscrew Shield: - If using arduino I would highly recommend this, makes life a lot easier
https://www.sparkfun.com/products/9729
Junction box to hold the relay:
https://www.masters.com.au/product/900029131/tripac-adaptable-weatherproof-junction-box-abko332
My element is from Keg keg and I wired up the relay using an extension cable I had lying around. To add a buzzer, a piezo electric buzzer that runs on 5 V will do.
The prices can add up but over time the parts can be re-used (eg: my arduino is begin used to monitor ambient air temp for my aquaponics) but the main benefits is that putting it all together is fun, educational, and extremely satisfying.
Im always making revisions to the code, so if you see any changes that need to be made be sure to let me know! Sorry for the extreme post.. :super: