/**********************************
* Annealing Circuit *
* By SMO 07/25/2018 *
**********************************/
#include <Button.h> //note that this is a different version than the standard button help, it can be found at https://github.com/slavianp/improc/tree/master/Arduino/src/Library
#include <TicksPerSecond.h> // this can be found at https://github.com/slavianp/improc/tree/master/Arduino/src/Library
#include <RotaryEncoderAcelleration.h> // this can be found at https://github.com/slavianp/improc/tree/master/Arduino/src/Library
#include <Metro.h>
#include <elapsedMillis.h>
#include <LiquidCrystal_I2C.h>
//#include <Servo.h>
#include <EEPROM.h>
#include <PinChangeInterrupt.h>
#include <PID_v1.h>
#include <PWM.h>
// Assign I/O pins (Traffic Cop)
#define Encoder_A_Pin 2
#define Encoder_B_Pin 3
#define Encoder_Button_Pin 4
#define Encoder_LED_BLU_Pin 11
#define Encoder_LED_GRN_Pin 12
#define Encoder_LED_RED_Pin 13 // Note that LED is also on pin 13, avoid using as input except UNO board with buffer to LED
#define Case_Sensor_Pin 5
#define Relay_Output_Pin 6
#define Solenoid_Pin 7
//#define Drop_Servo_Pin 7
#define Flow_Sensor_Pin 8
#define Fan_PWM_Pin 9 // analogwrite pins on ATmega328p can only be on 3,5,6,9,10&11
#define Fan_RPM_Pin 10
#define Analog_Amps_Pin A0
#define Analog_Volt_Pin A1
#define Analog_Temp1_Pin A2
#define Analog_Temp2_Pin A3
// Thermistor Values
#define Thermistor_Nom 10000 //Resistance at 25 DegC
#define Thermistor_Temp 25 //Nominal temperature in DegC
#define Thermistor_Beta 3435 //Beta coefficient for thermistor
#define Thermistor_Res 10000 //Value of resistor in series with thermistor
// Initialize I/O values
boolean Relay_State = LOW;
boolean Pushbutton_State = LOW;
boolean Case_Sensor_State = LOW;
boolean Solenoid_State = LOW;
float Analog_Pot_Val = 0;
float Analog_Volt_Val = 0;
float Analog_Amps_Val = 0;
float Max_Amps_Val = 0;
float Analog_Temp1_Val = 0;
float Temp1_Steinhart_Val = 0;
float Analog_Temp2_Val = 0;
float Temp2_Steinhart_Val = 0;
// Initialize other values
int Seq_Step_Num = 1;
int Timer_Setpt = 0;
int Timer_Display = 0;
int Load_Delay_Setpt = 0500; //Delay time in mSEC, gives time for a brass to get into position for annealing
int Drop_Delay_Setpt = 0010; //Delay time in mSEC, gives time for the servo to get into drop position
int Case_Delay_Setpt = 0500; //Delay time in mSEC, gives time for brass to drop and clear the servo
int Home_Delay_Setpt = 0010; //Delay time in mSEC, gives time for the servo to get into home position
int Drop_Pos_Setpt = 90; //in degrees
int Home_Pos_Setpt = 0; //in degrees
volatile long Flow_Total = 0;
volatile long Flow_Interlock_Total = 0;
int Flow_Interlock_Val = 0;
float Flow_Rate = 0;
volatile boolean Button_Toggle_State = 0;
float Joules_Total = 0;
volatile long Fan_RPM_Total = 0;
int Fan_RPM_Rate = 0;
boolean Encoder_Inc = LOW;
// Set Memory location for last time setpoint, setpoint will be saved when power is down
int eeAddress = 0;
float eeData = 0;
// Setup PID for Fan Control
double PID_SP=1000, PID_PV, PID_OP, PID_Kp=2, PID_Ki=0.1, PID_Kd=.5; //Setpoint is multiplied by 10, example 950=95.0 degF
PID FanPID(&PID_PV, &PID_OP, &PID_SP, PID_Kp, PID_Ki, PID_Kd, REVERSE);
// Sequence Descriptions
char *Seq_Step_Desc[] = {
// 12345678901234567892 20 characters max
"Step 0; not used ",
"Step 1; press start ",
"Step 2; load case ",
"Step 3; load delay ",
"Step 4; start anneal",
"Step 5; time anneal ",
"Step 6; stop anneal ",
"Step 7; drop pos ",
"Step 8; drop delay ",
"Step 9; case clear? ",
"Step10; clear delay ",
"Step11; home pos ",
"Step12; home delay ",
"Step13; Return ",
"Step14; Check Flow ",
"Step15; HIGH AMPS "
};
// Set the LCD address and 20x4 display also define extra characters
LiquidCrystal_I2C lcd(0x20, 20, 4);
char Print_Buffer[40] = "";
#if defined(ARDUINO) && ARDUINO >= 100
#define printByte(args) write(args);
#else
#define printByte(args) print(args,BYTE);
#endif
uint8_t Up_Arrow[8] = {0x04, 0x0E, 0x15, 0x04, 0x04, 0x04, 0x04, 0x00}; //Define Up Arrow for LCD
uint8_t Dn_Arrow[8] = {0x00, 0x04, 0x04, 0x04, 0x04, 0x15, 0x0E, 0x04}; //Define Down Arrow for LCD
//Configure Metro Objects
Metro LCD_Print_Data = Metro(10);
Metro Analog_Inputs_Read = Metro(10);
Metro Flow_Calculation = Metro(10000);
Metro Flow_Interlock = Metro(1000);
Metro Joules_Calculation = Metro(100);
Metro Fan_RPM_Calculation = Metro(6000);
//Configure Timers
elapsedMillis Timer_Actual;
elapsedMillis Button_Debounce;
//Configure Servos
//Servo Drop_Servo;
//Configure Encoder
RotaryEncoderAcelleration Encoder_Knob;
void UpdateEncoder(){
Encoder_Knob.update();
}
/***************************
* Setup Program Begin *
***************************/
void setup() {
//Assign pin modes
pinMode(Relay_Output_Pin, OUTPUT);
pinMode(Solenoid_Pin, OUTPUT);
pinMode(Fan_PWM_Pin, OUTPUT);
pinMode(Fan_RPM_Pin, INPUT_PULLUP);
pinMode(Case_Sensor_Pin, INPUT_PULLUP);
pinMode(Flow_Sensor_Pin, INPUT_PULLUP);
pinMode(Encoder_Button_Pin, INPUT);
pinMode(Encoder_LED_BLU_Pin, OUTPUT);
pinMode(Encoder_LED_GRN_Pin, OUTPUT);
pinMode(Encoder_LED_RED_Pin, OUTPUT);
//Get last setpoint before power down to set initial encoder setpoint
EEPROM.get(eeAddress, eeData);
//Initialize Encoder
Encoder_Knob.initialize(Encoder_A_Pin, Encoder_B_Pin);
Encoder_Knob.setMinMax(50, 2000);
Encoder_Knob.setPosition(eeData); //Initialize Encoder Position with saved data
attachInterrupt(0, UpdateEncoder, CHANGE);
//Initialize LCD
lcd.begin();
lcd.backlight();
lcd.createChar(0, Up_Arrow);
lcd.createChar(1, Dn_Arrow);
//Set initial postion for Drop Servo
/*
Drop_Servo.attach(Drop_Servo_Pin);
Drop_Servo.write(Drop_Pos_Setpt);
delay(Drop_Delay_Setpt);
Drop_Servo.write(Home_Pos_Setpt);
delay(Drop_Delay_Setpt);
*/
//Setup PID
FanPID.SetOutputLimits(100, 255);
FanPID.SetMode(AUTOMATIC);
//Setup PWM frequency
InitTimersSafe(); //Initialize all timers except for 0, to save time keeping functions
SetPinFrequencySafe(Fan_PWM_Pin, 25000); //Set frequency to 25khz
//Set the flow meter pin change interrupt
attachPinChangeInterrupt(digitalPinToPinChangeInterrupt(Flow_Sensor_Pin), Flow_Rising, RISING);
//Sense positive transition of button interrupt
attachPinChangeInterrupt(digitalPinToPinChangeInterrupt(Encoder_Button_Pin), Push_Button_Toggle, RISING);
//Set the fan RPM pin change interrupt
attachPinChangeInterrupt(digitalPinToPinChangeInterrupt(Fan_RPM_Pin), Fan_RPM_Rising, RISING);
}
/***************************
* Main Program Loop Begin *
***************************/
void loop() {
//Write Digital Outputs
digitalWrite(Relay_Output_Pin, Relay_State);
digitalWrite(Solenoid_Pin, Solenoid_State);
// Read Digital Inputs
// Pushbutton_State = !digitalRead(Pushbutton_Pin); //this button is normally closed
Pushbutton_State = digitalRead(Encoder_Button_Pin); //this button is normally open
Case_Sensor_State = digitalRead(Case_Sensor_Pin); //This sensor is normally open
// Read Analog & Do PID's
if(Analog_Inputs_Read.check() == 1){
//To smooth analog signals use a single Pole Digital Filter [out=wt*out+(1-wt)*new]
Analog_Amps_Val = 0.50 * Analog_Amps_Val + 0.50 * ((analogRead(Analog_Amps_Pin) / 1023.0 * 5000.0 - 5000.0/2.0)/100.0);
Analog_Volt_Val = 0.75 * Analog_Volt_Val + 0.25 * (analogRead(Analog_Volt_Pin) * 0.079101); // span is 80 volts or 80/1023=0.078201 adjust as needed for accuracy
Analog_Temp1_Val = 0.95 * Analog_Temp1_Val + 0.05 * (10000.0 / ((1023.0 / analogRead(Analog_Temp1_Pin)) - 1));
Temp1_Steinhart_Val = Analog_Temp1_Val / Thermistor_Nom;
Temp1_Steinhart_Val = log(Temp1_Steinhart_Val);
Temp1_Steinhart_Val /= Thermistor_Beta;
Temp1_Steinhart_Val += 1.0 / (Thermistor_Temp + 273.15);
Temp1_Steinhart_Val = 1.0 / Temp1_Steinhart_Val;
Temp1_Steinhart_Val -= 273.15;
Temp1_Steinhart_Val = Temp1_Steinhart_Val * 1.8 + 32.0;
PID_PV = Temp1_Steinhart_Val * 10.0;
FanPID.Compute();
pwmWrite(Fan_PWM_Pin, PID_OP);
Analog_Temp2_Val = 0.95 * Analog_Temp2_Val + 0.05 * (10000.0 / ((1023.0 / analogRead(Analog_Temp2_Pin)) - 1));
Temp2_Steinhart_Val = Analog_Temp2_Val / Thermistor_Nom;
Temp2_Steinhart_Val = log(Temp2_Steinhart_Val);
Temp2_Steinhart_Val /= Thermistor_Beta;
Temp2_Steinhart_Val += 1.0 / (Thermistor_Temp + 273.15);
Temp2_Steinhart_Val = 1.0 / Temp2_Steinhart_Val;
Temp2_Steinhart_Val -= 273.15;
Temp2_Steinhart_Val = Temp2_Steinhart_Val * 1.8 + 32.0;
}
// Calculate Flow Rate
if(Flow_Calculation.check() == 1){
Flow_Rate = (Flow_Total / 10.0) * 0.68913; //Flow meter; 1380 pulses/Liter or 5224 pulses/gal, gph = pps * 3600/5224 or .68913
Flow_Total = 0;
}
// Calculate Flow Interlock
if(Flow_Interlock.check() == 1){
Flow_Interlock_Val = Flow_Interlock_Total;
Flow_Interlock_Total = 0;
}
// Calculate Fan RPM's
if(Fan_RPM_Calculation.check() == 1){
Fan_RPM_Rate = Fan_RPM_Total * 10 / 2;
Fan_RPM_Total = 0;
}
// Read Encoder
long Encoder_Val = Encoder_Knob.getPosition();
if(Seq_Step_Num == 1){
Encoder_Inc = Encoder_Knob.isIncrementing();
}
// Calculate Timer Setpoint
Timer_Setpt = float(Encoder_Val) * 10.0;
// Calculate Joules (not accurate because of analog reading can only occur so fast)
if(Joules_Calculation.check() == 1 & Seq_Step_Num == 5){
Joules_Total = Joules_Total + Analog_Amps_Val * Analog_Volt_Val * 0.100;
}
// Set Debounce timer to max value to avoid rollover of value
if(Button_Debounce >= 10000){
Button_Debounce = 10000;
}
// Begin Sequence Logic
switch(Seq_Step_Num){
// Step 1 Wait for pushbutton toggle ON
case(1):
//Encoder LED's are reverse logic, Zero is ON
digitalWrite(Encoder_LED_BLU_Pin, 1);
digitalWrite(Encoder_LED_GRN_Pin, 1);
digitalWrite(Encoder_LED_RED_Pin, 1);
if(Button_Toggle_State == HIGH){
//Encoder LED's are reverse logic, Zero is ON
digitalWrite(Encoder_LED_BLU_Pin, 1);
digitalWrite(Encoder_LED_GRN_Pin, 0);
digitalWrite(Encoder_LED_RED_Pin, 1);
Seq_Step_Num++;
break;
}
break;
// Step 2 Wait for Case, also store setpoint if changed
case(2):
//Only store new setpoint if it has changed
if(eeData != Timer_Setpt){
eeData = Timer_Setpt / 10.0;
EEPROM.put(eeAddress, eeData);
}
if(Case_Sensor_State == LOW){
Timer_Actual = 0;
Seq_Step_Num++;
break;
}
if(Button_Toggle_State == LOW){
Seq_Step_Num = 1;
break;
}
break;
// Step 3 Wait for Load Case Delay
case(3):
if(Case_Sensor_State == LOW & Timer_Actual >= Load_Delay_Setpt){
Seq_Step_Num++;
break;
}
if(Case_Sensor_State == HIGH){
Seq_Step_Num = 2;
break;
}
break;
// Step 4 Initialize annealing timer and Energize Relay
case(4):
Timer_Actual = 0;
Max_Amps_Val = Analog_Amps_Val;
Relay_State = HIGH;
Joules_Total = 0;
Joules_Calculation.reset();
//Encoder LED's are reverse logic, Zero is ON
digitalWrite(Encoder_LED_BLU_Pin, 1);
digitalWrite(Encoder_LED_GRN_Pin, 1);
digitalWrite(Encoder_LED_RED_Pin, 0);
Seq_Step_Num++;
break;
// Step 5 Wait for Timer at Setpoint
case(5):
Timer_Display = Timer_Actual;
Max_Amps_Val = Analog_Amps_Val;
if(Timer_Actual >= Timer_Setpt || Button_Toggle_State == LOW){
Timer_Display = Timer_Actual;
Seq_Step_Num++;
break;
}
// if the amps go high then goto step 15
if(Analog_Amps_Val > 14.0){
Seq_Step_Num = 15;
break;
}
// if low flow then goto step 14
if(Flow_Interlock_Val <= 4){
Seq_Step_Num = 14;
break;
}
break;
// Step 6 Denergize Relay
case(6):
Relay_State = LOW;
//Encoder LED's are reverse logic, Zero is ON
digitalWrite(Encoder_LED_BLU_Pin, 0);
digitalWrite(Encoder_LED_GRN_Pin, 1);
digitalWrite(Encoder_LED_RED_Pin, 1);
Seq_Step_Num++;
break;
// Step 7 Set Drop Servo to drop position & Reset Timer
case(7):
Timer_Actual = 0;
//Drop_Servo.write(Drop_Pos_Setpt);
Solenoid_State = HIGH;
Seq_Step_Num++;
break;
// Step 8 Wait for drop position
case(8):
if(Timer_Actual >= Drop_Delay_Setpt){
Seq_Step_Num++;
break;
}
break;
// Step 9 Check for case clear & Reset Timer
case(9):
Timer_Actual = 0;
if(Case_Sensor_State == HIGH){
Seq_Step_Num++;
break;
}
break;
// Step 10 Wait for case to clear
case(10):
if(Timer_Actual >= Case_Delay_Setpt){
Seq_Step_Num++;
break;
}
break;
// Step 11 Set Servo to Home Position & Reset Timer
case(11):
Timer_Actual = 0;
//Drop_Servo.write(Home_Pos_Setpt);
Solenoid_State = LOW;
Seq_Step_Num++;
break;
// Step 12 Wait for servo home
case(12):
if(Timer_Actual >= Home_Delay_Setpt){
Seq_Step_Num++;
break;
}
break;
// Step 13 Return to Step 1
case(13):
if(Button_Toggle_State == HIGH){
digitalWrite(Encoder_LED_BLU_Pin, 1);
digitalWrite(Encoder_LED_GRN_Pin, 0);
digitalWrite(Encoder_LED_RED_Pin, 1);
Seq_Step_Num = 2;
break;
}
else{
Seq_Step_Num = 1;
break;
}
break;
// Step 14 Check for flow
case(14):
Relay_State = LOW;
//Encoder LED's are reverse logic, Zero is ON
digitalWrite(Encoder_LED_BLU_Pin, 0);
digitalWrite(Encoder_LED_GRN_Pin, 1);
digitalWrite(Encoder_LED_RED_Pin, 1);
if(Button_Toggle_State == LOW){
Seq_Step_Num = 7;
break;
}
break;
// Step 15 HIGH AMPS
case(15):
Relay_State = LOW;
//Encoder LED's are reverse logic, Zero is ON
digitalWrite(Encoder_LED_BLU_Pin, 0);
digitalWrite(Encoder_LED_GRN_Pin, 1);
digitalWrite(Encoder_LED_RED_Pin, 1);
if(Button_Toggle_State == LOW){
Seq_Step_Num = 7;
break;
}
break;
}
// Write to LCD Display except for last 150mSec of timer to prevent overshoot from writing to I2C, didn't want the complexity of a timer interrupt
if(LCD_Print_Data.check() == 1 && (Timer_Actual <= Timer_Setpt - 150 || Seq_Step_Num != 5)){
//1st LCD Row
lcd.home();
lcd.print("Set;");
lcd.setCursor(4,0);
dtostrf(float(Timer_Setpt)/1000, 5, 2, Print_Buffer);
lcd.print(Print_Buffer);
lcd.print(" Act ");
dtostrf(float(Timer_Display)/1000, 6, 3, Print_Buffer);
lcd.print(Print_Buffer);
//2nd LCD Row
lcd.setCursor(0,1);
dtostrf(Analog_Volt_Val, 4, 1, Print_Buffer);
lcd.print(Print_Buffer);
lcd.print("V");
lcd.setCursor(6,1);
if(Encoder_Inc){
dtostrf(Fan_RPM_Rate, 4, 0, Print_Buffer);
lcd.print(Print_Buffer);
lcd.print("RPM");
}
else{
dtostrf(Joules_Total, 5, 0, Print_Buffer);
lcd.print(Print_Buffer);
lcd.print("J ");
}
lcd.setCursor(13,1);
if(Seq_Step_Num == 5){
dtostrf(Analog_Amps_Val, 5, 1, Print_Buffer);
lcd.print(Print_Buffer);
lcd.print(" A");
}
else{
dtostrf(Max_Amps_Val, 5, 1, Print_Buffer);
lcd.print(Print_Buffer);
lcd.printByte(0); //Print Up Arrow
lcd.print("A");
}
//3rd LCD Row
lcd.setCursor(0,2);
if(Encoder_Inc){
lcd.print("T1;");
dtostrf(Temp1_Steinhart_Val, 5, 1, Print_Buffer);
lcd.print(Print_Buffer);
lcd.print("F ");
lcd.setCursor(11,2);
lcd.print("T2;");
dtostrf(Temp2_Steinhart_Val, 5, 1, Print_Buffer);
lcd.print(Print_Buffer);
lcd.print("F");
}
else{
lcd.print("F; ");
dtostrf(Flow_Rate, 4, 1, Print_Buffer);
lcd.print(Print_Buffer);
lcd.print(" gph ");
lcd.setCursor(13,2);
if(Case_Sensor_State == LOW){
lcd.print("Case; Y");
}
else{
lcd.print("Case; N");
}
}
//4th LCD Row
lcd.setCursor(0,3);
lcd.print(Seq_Step_Desc[Seq_Step_Num]);
}
}
/***************************
* Flow Meter Interrupt *
***************************/
void Flow_Rising(void){
Flow_Total++;
Flow_Interlock_Total++;
}
/***************************
* Fan RPM Interrupt *
***************************/
void Fan_RPM_Rising(void){
Fan_RPM_Total++;
}
/*********************************
* Positive Transition Sense *
*********************************/
void Push_Button_Toggle(void){
if(Button_Debounce >= 1000){
if(Button_Toggle_State == HIGH && Button_Debounce >= 100){
Button_Toggle_State = LOW;
}
else{
Button_Toggle_State = HIGH;
}
Button_Debounce = 0;
}
}