Initial commit.
commit
de400a11fb
|
@ -0,0 +1,725 @@
|
|||
/*******************************************************************************
|
||||
* ControLeo Reflow Oven Controller (v2)
|
||||
* Author: Keith Rome
|
||||
* Website: www.wintellect.com/blogs/krome
|
||||
*
|
||||
* Based on the original ControLeo Reflow Oven Controller sample by Peter Easton @ whizoo.com
|
||||
*
|
||||
* This version is a near-complete rewrite that uses a phase-based profile system with
|
||||
* minimum/maximum times per phase. Also supports additional runtime data on the display
|
||||
* (time elapsed in current phase), triggering the buzzer alarm at various phase transitions,
|
||||
* and sends data to the Serial console to help with fine-tuning the reflow profile for
|
||||
* your individual oven hardware. Also uses EEPROM nonvolatile memory on ControLeo to
|
||||
* remember the last profile that was selected when the oven is powered back up again.
|
||||
*
|
||||
* Thanks to Rocketscream for the original code using the PID library. The code has
|
||||
* been heavily modified (removing PID) to give finer control over individual heating
|
||||
* elements.
|
||||
*
|
||||
* This is an example of a reflow oven controller. The reflow curve below is for a
|
||||
* lead-free profile, but this code supports both leaded and lead-free profiles.
|
||||
*
|
||||
* Temperature (Degree Celcius) Magic Happens Here!
|
||||
* 245-| x x
|
||||
* | x x
|
||||
* | x x
|
||||
* | x x
|
||||
* 200-| x x
|
||||
* | x | | x
|
||||
* | x | | x
|
||||
* | x | |
|
||||
* 150-| x | |
|
||||
* | x | | |
|
||||
* | x | | |
|
||||
* | x | | |
|
||||
* | x | | |
|
||||
* | x | | |
|
||||
* | x | | |
|
||||
* 30 -| x | | |
|
||||
* |< 60 - 90 s >|< 90 - 120 s >|< 90 - 120 s >|
|
||||
* | Preheat Stage | Soaking Stage | Reflow Stage | Cool
|
||||
* 0 |_ _ _ _ _ _ _ _|_ _ _ _ _ _ _ _ _ _|_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
|
||||
* Time (Seconds)
|
||||
*
|
||||
* Here is an example serial log generated by running a complete cycle using
|
||||
* the default lead-bearing solder profile:
|
||||
*
|
||||
* ControLeo Reflow Oven v2 firmware startup
|
||||
* Last selected profile loading from eeprom: 1
|
||||
* Advancing to next profile
|
||||
* Advancing to next profile
|
||||
* Button Pressed: CONTROLEO_BUTTON_BOTTOM
|
||||
* Starting Profile: Leaded solder
|
||||
* Leaving Phase: Idle (0), Elapsed: 8s
|
||||
* Entering Phase: Pre-heat (1), Exit Temp:145C, Min Time: 0s, Max: 0s, Ideal: 90s
|
||||
* **Exit Temperature Reached, Time in phase: 173s, Temperature: 145.00C
|
||||
* Leaving Phase: Pre-heat (1), Elapsed: 173s
|
||||
* Entering Phase: Soak (2), Exit Temp:180C, Min Time: 30s, Max: 120s, Ideal: 30s
|
||||
* **Duration/Temperature Reached, Time in phase: 55s, Temperature: 180.00C
|
||||
* Leaving Phase: Soak (2), Elapsed: 55s
|
||||
* Entering Phase: Liquidus (3), Exit Temp:210C, Min Time: 30s, Max: 90s, Ideal: 60s, alarm sounds on exit
|
||||
* **Duration/Temperature Reached, Time in phase: 51s, Temperature: 210.00C
|
||||
* Leaving Phase: Liquidus (3), Elapsed: 51s
|
||||
* Entering Phase: Reflow (4), Exit Temp:180C (falling), Min Time: 30s, Max: 90s, Ideal: 60s
|
||||
* *** Alarm ***
|
||||
* **Duration/Temperature Reached, Time in phase: 69s, Temperature: 180.00C
|
||||
* Leaving Phase: Reflow (4), Elapsed: 69s
|
||||
* Entering Phase: Cooling (5), Exit Temp:50C (falling), Min Time: 0s, Max: 0s, Ideal: 0s
|
||||
* **Exit Temperature Reached, Time in phase: 679s, Temperature: 50.00C
|
||||
* Profile Finished, Total Elapsed Time: 1028s, Peak Temperature Observed: 212.25C
|
||||
* Leaving Phase: Cooling (5), Elapsed: 679s
|
||||
* Entering Phase: Idle (0), Exit Temp:0C, Min Time: 0s, Max: 0s, Ideal: 0s
|
||||
*
|
||||
*
|
||||
*
|
||||
* This firmware builds on the work of other talented individuals:
|
||||
* ==========================================
|
||||
* Rocketscream (www.rocketscream.com)
|
||||
* Produced the Arduino reflow oven shield ands code that inspired this project.
|
||||
*
|
||||
* ==========================================
|
||||
* Limor Fried of Adafruit (www.adafruit.com)
|
||||
* Author of Arduino MAX6675 library. Adafruit has been the source of tonnes of
|
||||
* tutorials, examples, and libraries for everyone to learn.
|
||||
*
|
||||
* Disclaimer
|
||||
* ==========
|
||||
* Dealing with high voltage is a very dangerous act! Please make sure you know
|
||||
* what you are dealing with and have proper knowledge before hand. Your use of
|
||||
* any information or materials on this reflow oven controller is entirely at
|
||||
* your own risk, for which we shall not be liable.
|
||||
*
|
||||
* Released under WTFPL license.
|
||||
*
|
||||
* Revision Description
|
||||
* ======== ===========
|
||||
* 1.00 Initial public release.
|
||||
* 2.00 Public release.
|
||||
*******************************************************************************/
|
||||
|
||||
// ***** INCLUDES *****
|
||||
#include <Wire.h>
|
||||
//#include <ControLeo2.h>
|
||||
#include "ControLeo2_MAX31855.h"
|
||||
#include <EEPROM.h>
|
||||
#include "max6675.h"
|
||||
#include <LiquidCrystal.h>
|
||||
|
||||
// Defines for the 2 buttons
|
||||
#define CONTROLEO_BUTTON_TOP_PIN 3 // Top button is on D11
|
||||
#define CONTROLEO_BUTTON_BOTTOM_PIN 2 // Bottom button is on D2
|
||||
#define CONTROLEO_BUTTON_NONE 0
|
||||
#define CONTROLEO_BUTTON_TOP 1 // S1
|
||||
#define CONTROLEO_BUTTON_BOTTOM 2 // S2
|
||||
|
||||
// The Buzzer is on D13
|
||||
#define CONTROLEO_BUZZER_PIN 4
|
||||
|
||||
// ***** CONSTANTS *****
|
||||
#define CLOCK_INTERVAL 100 // how frequently the state machine checks for advancements (ms)
|
||||
#define SAMPLE_INTERVAL 500 // how frequently temperature measurements are updated (ms)
|
||||
#define CYCLE_INTERVAL 1000 // how frequently the oven cycles through the current phase's heating pattern (ms per bit)
|
||||
#define MAX_START_TEMP 50 // maximum temperature where a new reflow session will be allowed to start
|
||||
#define NUM_PHASES 4 // number of phases in a profile (always assume a final "cooling" phase)
|
||||
#define NUM_HEATERS 3 // number of heaters installed
|
||||
#define DEFAULT_PROFILE 0 // default temperature profile at startup (overridden by EEPROM)
|
||||
#define BUZZER_DURATION 250 // how long to play buzzer sounds
|
||||
#define ADDR_CURR_PROFILE 0 // address in EEPROM for storing the current profile ID
|
||||
|
||||
#define OFF 0
|
||||
#define ON 1
|
||||
|
||||
// ***** PRODUCT IDENTIFICATION *****
|
||||
const char* BRAND_ID = "ControLeo";
|
||||
const char* PRODUCT_ID = "Reflow Oven v2";
|
||||
|
||||
int thermoDO = 8;
|
||||
int thermoCS = 9;
|
||||
int thermoCLK = 10;
|
||||
|
||||
const int rs = A3, en = A2, d4 = A1, d5 = A0, d6 = 15, d7 = 14;
|
||||
|
||||
// ***** HARDWARE INTERFACES *****
|
||||
struct Hardware {
|
||||
unsigned long DisableBuzzerAt;
|
||||
int HeaterPins[NUM_HEATERS]; // Pin assignments
|
||||
//ControLeo2_LiquidCrystal LCD; // Specify LCD interface
|
||||
LiquidCrystal LCD;
|
||||
//ControLeo2_MAX31855 Thermocouple; // Specify MAX31855 thermocouple interface
|
||||
MAX6675 Thermocouple;
|
||||
};
|
||||
Hardware hardware = { 0, {
|
||||
7, // upper heater pin
|
||||
5, // lower heater pin
|
||||
6 // booster heater pin
|
||||
},
|
||||
//ControLeo2_LiquidCrystal(),
|
||||
LiquidCrystal(rs, en, d4, d5, d6, d7),
|
||||
MAX6675(thermoCLK, thermoCS, thermoDO)};
|
||||
|
||||
// ***** PROFILES *****
|
||||
// Each element of this array is a 8-second window for an element. For example, if the value is 0b11001111 then
|
||||
// the element will be on for 2 seconds, off for 2 seconds then on for 4 seconds. This pattern will keep
|
||||
// repeating itself until the temperature rises through the temperate transition point given in tempPoints. This
|
||||
// gives fine control over each element and has the following benefits:
|
||||
// 1. Prevents individual elements from getting too hot, perhaps burning insulation.
|
||||
// 2. Ensures heat comes from the right part of the oven at the right time
|
||||
// 3. Helps overall current draw by being able to turn off some elements while turning others on
|
||||
// ==================== YOU SHOULD TUNE THESE VALUES TO YOUR REFLOW OVEN!!! ====================
|
||||
enum CrossingDirection {
|
||||
RISE,
|
||||
FALL
|
||||
};
|
||||
struct ReflowPhase {
|
||||
char* Name;
|
||||
int ExitTemperatureC;
|
||||
CrossingDirection RisingOrFalling;
|
||||
int MinDurationS;
|
||||
int MaxDurationS;
|
||||
int TargetDurationS;
|
||||
int HeaterPattern[NUM_HEATERS];
|
||||
boolean AlarmOnExit;
|
||||
};
|
||||
ReflowPhase idlePhase = { "Idle", 0, RISE, 0, 0, 0, { 0b00000000, 0b00000000, 0b00000000 }, false };
|
||||
ReflowPhase coolingPhase = { "Cooling", MAX_START_TEMP, FALL, 0, 0, 0, { 0b00000000, 0b00000000, 0b00000000 }, false };
|
||||
|
||||
struct ReflowProfile {
|
||||
char* Name;
|
||||
ReflowPhase Phases[NUM_PHASES];
|
||||
};
|
||||
ReflowProfile profiles[] = {
|
||||
{
|
||||
"Lead-free solder",
|
||||
{ // Zone Exit(C) Direction Min(S) Max(S) Tgt(S) Upper Lower Boost Alarm
|
||||
{ "Pre-heat", 150, RISE, 0, 0, 90, { 0b11001101, 0b10111110, 0b01010011 }, false },
|
||||
{ "Soak", 205, RISE, 30, 120, 30, { 0b01000100, 0b10101011, 0b00010000 }, false },
|
||||
{ "Liquidus", 235, RISE, 30, 90, 60, { 0b11011110, 0b10111111, 0b01101101 }, false },
|
||||
{ "Reflow", 225, FALL, 30, 90, 60, { 0b00010001, 0b01000100, 0b00000000 }, true },
|
||||
}
|
||||
},
|
||||
{
|
||||
"Leaded solder",
|
||||
{ // Zone Exit(C) Direction Min(S) Max(S) Tgt(S) Upper Lower Boost Alarm
|
||||
{ "Pre-heat", 145, RISE, 0, 0, 90, { 0b11001101, 0b01110110, 0b01010011 }, false },
|
||||
{ "Soak", 180, RISE, 30, 120, 30, { 0b01000100, 0b10101011, 0b00010001 }, false },
|
||||
{ "Liquidus", 210, RISE, 30, 90, 60, { 0b10111110, 0b11110111, 0b00101000 }, true },
|
||||
{ "Reflow", 180, FALL, 30, 90, 60, { 0b01000000, 0b00011000, 0b00000100 }, false },
|
||||
}
|
||||
},
|
||||
};
|
||||
#define NUM_PROFILES (sizeof(profiles)/sizeof(ReflowProfile)) //array size is computed from initialized data
|
||||
|
||||
// ***** STATE TRACKING *****
|
||||
struct OvenState {
|
||||
int SelectedProfile;
|
||||
boolean IsFaulted;
|
||||
boolean IsActive;
|
||||
double TemperatureC;
|
||||
double PeakTemperatureC;
|
||||
unsigned long LastClocked;
|
||||
unsigned long NextClock;
|
||||
unsigned long NextSample;
|
||||
unsigned long NextCycle;
|
||||
int ActiveHeatCycle;
|
||||
int ActivePhase;
|
||||
unsigned long EnteredCurrentPhase;
|
||||
int SecInPhase;
|
||||
unsigned long ActiveSince;
|
||||
ReflowPhase PhaseSchedule[NUM_PHASES + 2];
|
||||
};
|
||||
OvenState currentState = {
|
||||
-1, false, false, 0, 0,
|
||||
0, CLOCK_INTERVAL, SAMPLE_INTERVAL, CYCLE_INTERVAL,
|
||||
0, 0, 0, 0, 0,
|
||||
{ idlePhase, idlePhase, idlePhase, idlePhase, idlePhase, coolingPhase} };
|
||||
|
||||
|
||||
void setup()
|
||||
{
|
||||
// *********** Start of ControLeo2 initialization ***********
|
||||
// Set up the buzzer and buttons
|
||||
pinMode(CONTROLEO_BUZZER_PIN, OUTPUT);
|
||||
pinMode(CONTROLEO_BUTTON_TOP_PIN, INPUT_PULLUP);
|
||||
pinMode(CONTROLEO_BUTTON_BOTTOM_PIN, INPUT_PULLUP);
|
||||
// Set the relays as outputs and turn them off
|
||||
// The relay outputs are on D4 to D7 (4 outputs)
|
||||
for (int i=4; i<8; i++) {
|
||||
pinMode(i, OUTPUT);
|
||||
digitalWrite(i, LOW);
|
||||
}
|
||||
// Set up the LCD's number of rows and columns
|
||||
// lcd.begin(16, 2);
|
||||
// Create the degree symbol for the LCD
|
||||
// unsigned char degree[8] = {12,18,18,12,0,0,0,0};
|
||||
// lcd.createChar(0, degree);
|
||||
// *********** End of ControLeo2 initialization ***********
|
||||
|
||||
InitializeDisplay();
|
||||
|
||||
Serial.begin(9600);
|
||||
Serial.print(BRAND_ID); Serial.print(" "); Serial.print(PRODUCT_ID); Serial.println(" firmware startup");
|
||||
|
||||
// configure heater GPIO
|
||||
for (int i = 0; i < NUM_HEATERS; i++) {
|
||||
pinMode(hardware.HeaterPins[i], OUTPUT);
|
||||
digitalWrite(hardware.HeaterPins[i], LOW);
|
||||
}
|
||||
|
||||
int lastProfile = EEPROM.read(ADDR_CURR_PROFILE);
|
||||
if (lastProfile == 255) {
|
||||
lastProfile = DEFAULT_PROFILE;
|
||||
Serial.print("Default profile loading: "); Serial.println(DEFAULT_PROFILE);
|
||||
} else {
|
||||
Serial.print("Last selected profile loading from eeprom: "); Serial.println(lastProfile);
|
||||
}
|
||||
|
||||
while (currentState.SelectedProfile != lastProfile) {
|
||||
AdvanceProfile(true);
|
||||
}
|
||||
|
||||
DisplaySplashScreen();
|
||||
ResetState();
|
||||
}
|
||||
|
||||
void loop()
|
||||
{
|
||||
unsigned long now = millis();
|
||||
|
||||
// check for clock overflow (should only happen if you leave the oven on for 50+ days)
|
||||
if (currentState.LastClocked > now) {
|
||||
currentState.LastClocked = 0;
|
||||
currentState.NextClock = CLOCK_INTERVAL;
|
||||
currentState.NextCycle = CYCLE_INTERVAL;
|
||||
currentState.NextSample = SAMPLE_INTERVAL;
|
||||
currentState.EnteredCurrentPhase = 0;
|
||||
}
|
||||
|
||||
// check to see if we should capture a new temperature sample
|
||||
if (currentState.NextSample <= now) {
|
||||
CaptureTemperatureSample();
|
||||
currentState.NextSample = now + SAMPLE_INTERVAL;
|
||||
}
|
||||
|
||||
// should we check for phase transition this cycle?
|
||||
if (currentState.NextClock <= now) {
|
||||
CheckForPhaseTransition();
|
||||
currentState.LastClocked = now;
|
||||
currentState.NextClock = now + CLOCK_INTERVAL;
|
||||
}
|
||||
|
||||
// advance the heater pattern
|
||||
if (currentState.NextCycle <= now) {
|
||||
AdvanceHeatingCycle();
|
||||
currentState.NextCycle = now + CYCLE_INTERVAL;
|
||||
}
|
||||
|
||||
// always check for button press so there is no unnecessary lag
|
||||
// also, in the unlikely event that a phase transition and button press happen in the same
|
||||
// cycle, the button press will always get the last s
|
||||
CheckForButtonPress();
|
||||
|
||||
// turn off the buzzer if we need to
|
||||
if (hardware.DisableBuzzerAt != 0 && hardware.DisableBuzzerAt <= now) {
|
||||
EnableBuzzer(OFF);
|
||||
}
|
||||
|
||||
delay(10);
|
||||
}
|
||||
|
||||
void InitializeDisplay()
|
||||
{
|
||||
// Degree symbol for LCD
|
||||
unsigned char degree[8] = {140,146,146,140,128,128,128,128};
|
||||
|
||||
hardware.LCD.begin(16, 2);
|
||||
hardware.LCD.createChar(0, degree);
|
||||
hardware.LCD.clear();
|
||||
delay(3000);
|
||||
}
|
||||
|
||||
void DisplaySplashScreen()
|
||||
{
|
||||
hardware.LCD.print(BRAND_ID);
|
||||
hardware.LCD.setCursor(0, 1);
|
||||
hardware.LCD.print(PRODUCT_ID);
|
||||
delay(3000);
|
||||
hardware.LCD.clear();
|
||||
|
||||
DisplayProfile();
|
||||
UpdateDisplayedTemperature();
|
||||
EnableBuzzer(ON);
|
||||
}
|
||||
|
||||
void PrintAt(int line, int column, int maxWidth, boolean rightAlign, char*msg)
|
||||
{
|
||||
int msgLen = strlen(msg);
|
||||
if (msgLen > maxWidth)
|
||||
msgLen = maxWidth;
|
||||
int startCol = column;
|
||||
if (rightAlign && (msgLen < maxWidth))
|
||||
startCol += (maxWidth - msgLen);
|
||||
|
||||
hardware.LCD.setCursor(column, line);
|
||||
for (int i = 0; i < (startCol - column); i++)
|
||||
hardware.LCD.print(" ");
|
||||
hardware.LCD.print(msg);
|
||||
for (int i = startCol + msgLen; i < maxWidth; i++)
|
||||
hardware.LCD.print(" ");
|
||||
}
|
||||
|
||||
void WriteAt(int line, int column, int glyph)
|
||||
{
|
||||
hardware.LCD.setCursor(column, line);
|
||||
hardware.LCD.write(glyph);
|
||||
}
|
||||
|
||||
void DisplayProfile()
|
||||
{
|
||||
PrintAt(0, 0, 16, false, profiles[currentState.SelectedProfile].Name);
|
||||
}
|
||||
|
||||
void DisplayPhase()
|
||||
{
|
||||
PrintAt(0, 0, 11, false, currentState.PhaseSchedule[currentState.ActivePhase].Name);
|
||||
}
|
||||
|
||||
void DisplayElapsedInPhase(boolean forceUpdate)
|
||||
{
|
||||
int secInPhase = (millis() - currentState.EnteredCurrentPhase) / 1000;
|
||||
if (forceUpdate || (secInPhase != currentState.SecInPhase)) {
|
||||
currentState.SecInPhase = secInPhase;
|
||||
char msg[6];
|
||||
sprintf(msg, " %ds", secInPhase);
|
||||
PrintAt(0, 11, 5, true, msg);
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateDisplayedTemperature()
|
||||
{
|
||||
// if the thermocouple is in a faulted state, then display error
|
||||
if (currentState.IsFaulted) {
|
||||
PrintAt(1, 0, 16, false, "fault detected");
|
||||
return;
|
||||
}
|
||||
|
||||
// Print current temperature
|
||||
char buffer[12];
|
||||
dtostrf(currentState.TemperatureC, 3, 1, buffer);
|
||||
int sz = strlen(buffer);
|
||||
char msg[12];
|
||||
sprintf(msg, "%s %s", buffer, "C");
|
||||
PrintAt(1, 0, 12, false, msg);
|
||||
Serial.println(currentState.TemperatureC);
|
||||
// draw the special glyph for degree symbol
|
||||
WriteAt(1, sz, 0);
|
||||
}
|
||||
|
||||
void EnableBuzzer(uint8_t onOrOff)
|
||||
{
|
||||
//analogWrite(CONTROLEO_BUZZER_PIN, onOrOff? 15:0);
|
||||
digitalWrite(CONTROLEO_BUZZER_PIN, onOrOff? 1:0);
|
||||
if (onOrOff == ON) {
|
||||
Serial.println("*** Alarm ***");
|
||||
hardware.DisableBuzzerAt = millis() + BUZZER_DURATION;
|
||||
} else {
|
||||
hardware.DisableBuzzerAt = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void CaptureTemperatureSample()
|
||||
{
|
||||
// Read current temperature
|
||||
//double currentTemperature = hardware.Thermocouple.readThermocouple(CELSIUS);
|
||||
double currentTemperature = hardware.Thermocouple.readCelsius();
|
||||
|
||||
|
||||
// don't bother doing anything unless it actually changes
|
||||
if (currentState.TemperatureC == currentTemperature) return;
|
||||
|
||||
// Check for thermocouple problem
|
||||
if (currentTemperature == FAULT_OPEN || currentTemperature == FAULT_SHORT_GND || currentTemperature == FAULT_SHORT_VCC) {
|
||||
// There is a problem with the thermocouple
|
||||
Serial.print("Thermocouple Fault: ");
|
||||
if (currentTemperature == FAULT_OPEN) Serial.println("FAULT_OPEN");
|
||||
if (currentTemperature == FAULT_SHORT_GND) Serial.println("FAULT_SHORT_GND");
|
||||
if (currentTemperature == FAULT_SHORT_VCC) Serial.println("FAULT_SHORT_VCC");
|
||||
currentState.IsFaulted = true;
|
||||
End(false);
|
||||
} else {
|
||||
currentState.IsFaulted = false;
|
||||
}
|
||||
|
||||
// track peaks
|
||||
if (currentTemperature > currentState.PeakTemperatureC) {
|
||||
currentState.PeakTemperatureC = currentTemperature;
|
||||
}
|
||||
|
||||
// update display
|
||||
currentState.TemperatureC = currentTemperature;
|
||||
UpdateDisplayedTemperature();
|
||||
}
|
||||
|
||||
void CheckForButtonPress()
|
||||
{
|
||||
int button = GetRisingEdgeOfButtonPress();
|
||||
if (button == CONTROLEO_BUTTON_NONE) return;
|
||||
|
||||
if (button == CONTROLEO_BUTTON_TOP) {
|
||||
Serial.println("Button Pressed: CONTROLEO_BUTTON_TOP");
|
||||
} else {
|
||||
Serial.println("Button Pressed: CONTROLEO_BUTTON_BOTTOM");
|
||||
}
|
||||
|
||||
if (button == CONTROLEO_BUTTON_TOP) {
|
||||
// top button was pressed
|
||||
|
||||
// if the oven is idle, the top button will cycle through the known profiles
|
||||
// but is ignored while the oven is operating
|
||||
if (!currentState.IsActive) {
|
||||
AdvanceProfile(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (button == CONTROLEO_BUTTON_BOTTOM) {
|
||||
// bottom button was pressed
|
||||
|
||||
// bottom button starts/stops the oven
|
||||
if (currentState.IsActive) {
|
||||
End(true);
|
||||
} else {
|
||||
Start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#define DEBOUNCE_DETECT 50 // minimum time button needs be pressed
|
||||
#define DEBOUNCE_RESET 500 // minimum time between button signals
|
||||
int GetRisingEdgeOfButtonPress()
|
||||
{
|
||||
static int heldButton = CONTROLEO_BUTTON_NONE;
|
||||
static int lastEvent = CONTROLEO_BUTTON_NONE;
|
||||
static unsigned long lastReset = 0;
|
||||
static unsigned long heldSince = 0;
|
||||
|
||||
unsigned long now = millis();
|
||||
int signal = CONTROLEO_BUTTON_NONE;
|
||||
int buttonEvent = CONTROLEO_BUTTON_NONE;
|
||||
|
||||
// Read the current button status
|
||||
signal = CONTROLEO_BUTTON_NONE;
|
||||
if (digitalRead(CONTROLEO_BUTTON_TOP_PIN) == LOW)
|
||||
signal = CONTROLEO_BUTTON_TOP;
|
||||
else if (digitalRead(CONTROLEO_BUTTON_BOTTOM_PIN) == LOW)
|
||||
signal = CONTROLEO_BUTTON_BOTTOM;
|
||||
|
||||
if (heldButton == signal && heldButton != CONTROLEO_BUTTON_NONE) {
|
||||
// a button is currently pressed, but is it pressed for long enough?
|
||||
if ((lastEvent == CONTROLEO_BUTTON_NONE) && (now - heldSince >= DEBOUNCE_DETECT)) {
|
||||
// trigger it only once
|
||||
buttonEvent = signal;
|
||||
lastEvent = signal;
|
||||
}
|
||||
}
|
||||
|
||||
if (heldButton != CONTROLEO_BUTTON_NONE && signal == CONTROLEO_BUTTON_NONE) {
|
||||
// button was released
|
||||
heldButton = CONTROLEO_BUTTON_NONE;
|
||||
lastReset = now;
|
||||
heldSince = 0;
|
||||
lastEvent = CONTROLEO_BUTTON_NONE;
|
||||
}
|
||||
|
||||
if (heldButton == CONTROLEO_BUTTON_NONE && signal != CONTROLEO_BUTTON_NONE && (now - lastReset >= DEBOUNCE_RESET)) {
|
||||
// new button is being pressed
|
||||
heldButton = signal;
|
||||
heldSince = now;
|
||||
}
|
||||
|
||||
return buttonEvent;
|
||||
}
|
||||
|
||||
void AdvanceHeatingCycle()
|
||||
{
|
||||
int mask = 0b10000000 >> currentState.ActiveHeatCycle;
|
||||
|
||||
if (currentState.IsActive) {
|
||||
currentState.ActiveHeatCycle++;
|
||||
if (currentState.ActiveHeatCycle > 7) {
|
||||
currentState.ActiveHeatCycle = 0;
|
||||
}
|
||||
} else {
|
||||
mask = 0b00000000; // disable all heaters
|
||||
}
|
||||
|
||||
// toggle heater GPIO pins
|
||||
ReflowPhase currentPhase = currentState.PhaseSchedule[currentState.ActivePhase];
|
||||
for (int heaterIx = 0; heaterIx < NUM_HEATERS; heaterIx++) {
|
||||
if (currentPhase.HeaterPattern[heaterIx] & mask) {
|
||||
digitalWrite(hardware.HeaterPins[heaterIx], HIGH);
|
||||
} else {
|
||||
digitalWrite(hardware.HeaterPins[heaterIx], LOW);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CheckForPhaseTransition()
|
||||
{
|
||||
if (!currentState.IsActive) return;
|
||||
|
||||
DisplayElapsedInPhase(false);
|
||||
|
||||
// can't leave idle without explicit start
|
||||
if (currentState.ActivePhase == 0) return;
|
||||
|
||||
ReflowPhase currentPhase = currentState.PhaseSchedule[currentState.ActivePhase];
|
||||
int timeInPhase = (millis() - currentState.EnteredCurrentPhase) / 1000;
|
||||
CrossingDirection dir = currentPhase.RisingOrFalling;
|
||||
|
||||
// check for phase change due to temperature rise
|
||||
if (dir == RISE) {
|
||||
if (currentPhase.ExitTemperatureC <= currentState.TemperatureC) {
|
||||
if (currentPhase.MinDurationS == 0) {
|
||||
MoveToNextPhase("Exit Temperature Reached", timeInPhase);
|
||||
} else {
|
||||
// but only allow phase change if any minimum time spent in phase has been met
|
||||
if (timeInPhase > currentPhase.MinDurationS) {
|
||||
MoveToNextPhase("Duration/Temperature Reached", timeInPhase);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check for phase change due to temperature fall
|
||||
if (dir == FALL) {
|
||||
if (currentPhase.ExitTemperatureC >= currentState.TemperatureC) {
|
||||
if (currentPhase.MinDurationS == 0) {
|
||||
MoveToNextPhase("Exit Temperature Reached", timeInPhase);
|
||||
} else {
|
||||
// but only allow phase change if any minimum time spent in phase has been met
|
||||
if (timeInPhase > currentPhase.MinDurationS) {
|
||||
MoveToNextPhase("Duration/Temperature Reached", timeInPhase);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check for phase change due to timer overrun
|
||||
if (currentPhase.MaxDurationS > 0) {
|
||||
if (timeInPhase >= currentPhase.MaxDurationS) {
|
||||
MoveToNextPhase("Max Duration Exceeded", timeInPhase);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MoveToNextPhase(char* reason, int timeInPhase)
|
||||
{
|
||||
Serial.print("**");
|
||||
Serial.print(reason);
|
||||
Serial.print(", Time in phase: ");
|
||||
Serial.print(timeInPhase);
|
||||
Serial.print("s, Temperature: ");
|
||||
Serial.print(currentState.TemperatureC);
|
||||
Serial.println("C");
|
||||
TransitionToPhase(currentState.ActivePhase + 1);
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
Serial.print("Starting Profile: "); Serial.println(profiles[currentState.SelectedProfile].Name);
|
||||
currentState.PeakTemperatureC = 0; // reset
|
||||
currentState.ActiveSince = millis(); // reset
|
||||
currentState.IsActive = true;
|
||||
TransitionToPhase(1);
|
||||
}
|
||||
|
||||
void End(boolean isUserInitiated)
|
||||
{
|
||||
if (isUserInitiated) {
|
||||
Serial.print("Profile Aborted by User");
|
||||
} else {
|
||||
Serial.print("Profile Finished");
|
||||
}
|
||||
Serial.print(", Total Elapsed Time: "); Serial.print((millis() - currentState.ActiveSince) / 1000); Serial.print("s");
|
||||
Serial.print(", Peak Temperature Observed: "); Serial.print(currentState.PeakTemperatureC); Serial.println("C");
|
||||
currentState.PeakTemperatureC = 0; // reset
|
||||
currentState.ActiveSince = millis(); // reset
|
||||
ResetState();
|
||||
}
|
||||
|
||||
void ResetState()
|
||||
{
|
||||
CaptureTemperatureSample();
|
||||
if (currentState.TemperatureC > MAX_START_TEMP) {
|
||||
currentState.IsActive = true;
|
||||
TransitionToPhase(NUM_PHASES + 1);
|
||||
} else {
|
||||
TransitionToPhase(0);
|
||||
currentState.IsActive = false;
|
||||
// Return to profile display
|
||||
DisplayProfile();
|
||||
}
|
||||
}
|
||||
|
||||
void TransitionToPhase(int phase)
|
||||
{
|
||||
if (phase == currentState.ActivePhase) return;
|
||||
|
||||
if (phase > NUM_PHASES + 1) {
|
||||
// transition beyond cooling phase
|
||||
End(false);
|
||||
return;
|
||||
}
|
||||
|
||||
int timeInLastPhase = (millis() - currentState.EnteredCurrentPhase) / 1000;
|
||||
ReflowPhase oldPhase = currentState.PhaseSchedule[currentState.ActivePhase];
|
||||
ReflowPhase newPhase = currentState.PhaseSchedule[phase];
|
||||
|
||||
Serial.print("Leaving Phase: "); Serial.print(oldPhase.Name);
|
||||
Serial.print(" ("); Serial.print(currentState.ActivePhase);
|
||||
Serial.print("), Elapsed: "); Serial.print(timeInLastPhase); Serial.println("s");
|
||||
|
||||
Serial.print("Entering Phase: "); Serial.print(newPhase.Name);
|
||||
Serial.print(" ("); Serial.print(phase);
|
||||
Serial.print("), Exit Temp:"); Serial.print(newPhase.ExitTemperatureC); Serial.print("C");
|
||||
if (newPhase.RisingOrFalling == FALL) Serial.print(" (falling)");
|
||||
Serial.print(", Min Time: "); Serial.print(newPhase.MinDurationS);
|
||||
Serial.print("s, Max: "); Serial.print(newPhase.MaxDurationS);
|
||||
Serial.print("s, Ideal: "); Serial.print(newPhase.TargetDurationS); Serial.print("s");
|
||||
if (newPhase.AlarmOnExit) Serial.print(", alarm sounds on exit");
|
||||
Serial.println();
|
||||
|
||||
currentState.ActivePhase = phase;
|
||||
currentState.EnteredCurrentPhase = millis();
|
||||
DisplayPhase();
|
||||
DisplayElapsedInPhase(true);
|
||||
|
||||
if (oldPhase.AlarmOnExit) {
|
||||
EnableBuzzer(ON);
|
||||
}
|
||||
}
|
||||
|
||||
void AdvanceProfile(boolean silently)
|
||||
{
|
||||
Serial.println("Advancing to next profile");
|
||||
|
||||
// select the next profile
|
||||
currentState.SelectedProfile++;
|
||||
if (currentState.SelectedProfile >= NUM_PROFILES) {
|
||||
currentState.SelectedProfile = 0;
|
||||
}
|
||||
|
||||
// copy the phases to phase schedule
|
||||
ReflowProfile newProfile = profiles[currentState.SelectedProfile];
|
||||
for (int phaseIx = 0; phaseIx < NUM_PHASES; phaseIx++) {
|
||||
// first phase in schedule is always the "idle" phase, and last is always "cooling"
|
||||
currentState.PhaseSchedule[phaseIx +1] = newProfile.Phases[phaseIx];
|
||||
}
|
||||
|
||||
if (!silently) {
|
||||
// update the UI
|
||||
DisplayProfile();
|
||||
// remember which profile was last selected
|
||||
EEPROM.write(ADDR_CURR_PROFILE, currentState.SelectedProfile);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue