/*
Reflow Controller v1.31
Modify: Steve Barth
Date: 12-02-2022
Brief
The Reflow Controller is an ATMEGA328 microcontroller
based system that automates your heater plan to reflow your DIY SMT PCB projects.
With two profiles stored, Pb and Lead-Free solder paste.
The firmware was modified from the original Rocketscream Electronics Tiny Reflow controller code.
Required Libraries
- Arduino PID Library:
>> https://github.com/br3ttb/Arduino-PID-Library
- Adafruit MAX6675 Library:
>> https://github.com/adafruit/Adafruit_MAX6675
- Adafruit SSD1306 Library:
>> https://github.com/adafruit/Adafruit_SSD1306
- Adafruit GFX Library:
>> https://github.com/adafruit/Adafruit-GFX-Library
Initial release:
- Arduino nano or pro mini (on ATMega328 5V, 16MHz),
- Oled display (SSD1306 128x64),
- MAX6675 with K-type thermosensor (0-1024'C),
- Optopoupler (MOC3041) and triac (BTA08-600),
- Fan for cooling (depending on the power supply, 5-12V)
Memory Usage:
RAM: [==== ] 41.2% (used 844 bytes from 2048 bytes)
Flash: [======= ] 71.1% (used 21844 bytes from 30720 bytes)
*******************************************************************************/
////**** INCLUDES
#include <Arduino.h>
#include <SPI.h>
#include <Wire.h>
#include <EEPROM.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <PID_v1.h>
#include <max6675.h>
////**** STATE TYPE DEFINITIONS
typedef enum REFLOW_STATE
{
REFLOW_STATE_IDLE, // orange
REFLOW_STATE_PREHEAT, // yellow
REFLOW_STATE_SOAK, // green
REFLOW_STATE_REFLOW, // bright green
REFLOW_STATE_COOL, // blue
REFLOW_STATE_COMPLETE, // indigo
REFLOW_STATE_TOO_HOT, // violet
REFLOW_STATE_ERROR // red
} reflowState_t;
typedef enum REFLOW_STATUS
{
REFLOW_STATUS_OFF,
REFLOW_STATUS_ON
} reflowStatus_t;
typedef enum SWITCH
{
SWITCH_NONE,
SWITCH_1,
SWITCH_2
} switch_t;
typedef enum DEBOUNCE_STATE
{
DEBOUNCE_STATE_IDLE,
DEBOUNCE_STATE_CHECK,
DEBOUNCE_STATE_RELEASE
} debounceState_t;
typedef enum REFLOW_PROFILE
{
REFLOW_PROFILE_LEADFREE,
REFLOW_PROFILE_LEADED
} reflowProfile_t;
//// **** GENERAL PROFILE CONSTANTS
#define PROFILE_TYPE_ADDRESS 0
#define TEMPERATURE_ROOM 50
#define TEMPERATURE_SOAK_MIN 150
#define TEMPERATURE_COOL_MIN 100
#define SENSOR_SAMPLING_TIME 1000
#define SOAK_TEMPERATURE_STEP 5
//// **** LEAD FREE PROFILE CONSTANTS
#define TEMPERATURE_SOAK_MAX_LF 200
#define TEMPERATURE_REFLOW_MAX_LF 250
#define SOAK_MICRO_PERIOD_LF 9000
//// **** LEADED PROFILE CONSTANTS
#define TEMPERATURE_SOAK_MAX_PB 180
#define TEMPERATURE_REFLOW_MAX_PB 224
#define SOAK_MICRO_PERIOD_PB 10000
//// **** SWITCH SPECIFIC CONSTANTS
#define DEBOUNCE_PERIOD_MIN 100
//// **** DISPLAY SPECIFIC CONSTANTS
#define UPDATE_RATE 100
#define SCREEN_WIDTH 128 // oled display width, in pixels
#define SCREEN_HEIGHT 64 // oled display height, in pixels
#define X_AXIS_START 18 // X-axis starting position
//! ***** PID PARAMETERS *****
//// **** PRE-HEAT STAGE
#define PID_KP_PREHEAT 100
#define PID_KI_PREHEAT 0.025
#define PID_KD_PREHEAT 20
//// **** SOAKING STAGE
#define PID_KP_SOAK 300
#define PID_KI_SOAK 0.05
#define PID_KD_SOAK 250
//// **** REFLOW STAGE
#define PID_KP_REFLOW 300
#define PID_KI_REFLOW 0.05
#define PID_KD_REFLOW 350
#define PID_SAMPLE_TIME 1000
//// **** LCD MESSAGES
const char *lcdMessagesReflowStatus[] = {"Ready", "PreHeat", "Soak", "Reflow", "Cool", "Done!", "Hot!", "Error"};
//// **** DEGREE SYMBOL FOR LCD
unsigned char degree[8] = {140, 146, 146, 140, 128, 128, 128, 128};
//// **** PIN ASSIGNMENT
unsigned char Start_Stop = A0;
unsigned char Lf_Pb = A2;
#define MAXCS 2
#define MAXCLK 3
#define MAXDO 4
unsigned char Buzzer = 6;
unsigned char Fan = 7; // Fan sw on/off
#define Led 8 // A6
unsigned char Triac = 9;
bool coolingOn = false; // Fan status enabled or not
double Fan_timer = 0; // time lenght for the on period for Fan
//#define oled_MOSI 11
//#define oled_CLK 13
//#define oled_DC 10
//#define oled_CS 8
#define oled_RESET 12
// ***** PID CONTROL VARIABLES *****
double setpoint;
double input;
double output;
double kp = PID_KP_PREHEAT;
double ki = PID_KI_PREHEAT;
double kd = PID_KD_PREHEAT;
unsigned long windowSize;
unsigned long windowStartTime;
unsigned long nextCheck;
unsigned long nextRead;
unsigned long updateLcd;
unsigned long timerSoak;
unsigned long buzzerPeriod;
unsigned char soakTemperatureMax;
unsigned char reflowTemperatureMax;
unsigned long soakMicroPeriod;
//* Reflow oven controller state machine state variable
reflowState_t reflowState;
//* Reflow oven controller status
reflowStatus_t reflowStatus;
//* Reflow profile type
reflowProfile_t reflowProfile;
//* Switch debounce state machine state variable
debounceState_t debounceState;
//* Switch debounce timer
long lastDebounceTime;
//* Switch press status
switch_t switchStatus;
switch_t switchValue;
switch_t switchMask;
//* Seconds timer
unsigned int timerSeconds;
//* thermosensor fault status
unsigned char fault;
unsigned int timerUpdate;
unsigned char temperature[SCREEN_WIDTH - X_AXIS_START];
unsigned char x;
//* PID control interface
PID reflowOvenPID(&input, &output, &setpoint, kp, ki, kd, DIRECT);
//* OLED display SETUP
Adafruit_SSD1306 oled(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, oled_RESET);
//* MAX6675 thermosensor interface Adafruit_MAX31855 thermosensor(MAXCLK, MAXCS, MAXDO);
MAX6675 thermosensor(MAXCLK, MAXCS, MAXDO);
switch_t readSwitch(void)
{
//* Checking if either the START/STOP or CHANGE PROFILE Pins were selected
if (digitalRead(Start_Stop) == LOW)
return SWITCH_1;
if (digitalRead(Lf_Pb) == LOW)
return SWITCH_2;
if (!(digitalRead(Lf_Pb) == LOW) || !(digitalRead(Lf_Pb) == LOW))
return SWITCH_NONE;
}
void setup()
{
//* Check current selected reflow profile
unsigned char value = EEPROM.read(PROFILE_TYPE_ADDRESS);
if ((value == 0) || (value == 1))
{
//* Valid reflow profile value
reflowProfile = value;
//___ (sqiggle?!) invalid conversion from 'unsigned char' to 'reflowProfile_t {aka REFLOW_PROFILE}' [-fpermissive]
}
else
{
//* Default to Lead-Free profile
/*EEPROM.write(PROFILE_TYPE_ADDRESS, 0);
reflowProfile = REFLOW_PROFILE_LEADFREE;*/
//* Default to Pb profile
reflowProfile = REFLOW_PROFILE_LEADED;
EEPROM.write(PROFILE_TYPE_ADDRESS, 1);
}
pinMode(Start_Stop, INPUT_PULLUP);
pinMode(Lf_Pb, INPUT_PULLUP);
pinMode(Fan, OUTPUT);
digitalWrite(Fan, LOW);
//* triac pin initialization to ensure reflow oven is off
pinMode(Triac, OUTPUT);
digitalWrite(Triac, LOW);
//* Buzzer pin initialization to ensure annoying buzzer is off
pinMode(Buzzer, OUTPUT);
digitalWrite(Buzzer, LOW);
//* LED pins initialization and turn on upon start-up (active high)
pinMode(Led, OUTPUT); //
digitalWrite(Led, HIGH); //
//* Start-up splash
digitalWrite(Buzzer, HIGH);
oled.begin(SSD1306_SWITCHCAPVCC, 0x3D);
oled.display();
digitalWrite(Buzzer, LOW);
delay(200);
oled.clearDisplay();
oled.setTextSize(1);
oled.setTextColor(WHITE);
oled.setCursor(0, 4);
oled.println(F(" Reflow"));
oled.println(F(" Controller"));
oled.println(F(" v 1.31"));
delay(1500);
oled.println();
oled.println(F(" by Steve Barth"));
oled.println();
oled.println(F(" 2022.dec.2."));
oled.display();
delay(2500);
oled.clearDisplay();
//* Serial communication at 115200 bps
Serial.begin(115200);
//* Turn off LED (active high)
digitalWrite(Led, LOW);
//* Set window size
windowSize = 2000;
//* Initialize time keeping variable
nextCheck = millis();
//* Initialize thermosensor reading variable
nextRead = millis();
//* Initialize LCD update timer
updateLcd = millis();
}
void loop()
{
//* Current time
unsigned long currentTime;
//? Time to read thermosensor?
if (millis() > nextRead)
{
//* Read thermosensor next sampling period
nextRead += SENSOR_SAMPLING_TIME;
//* Read current temperature
input = thermosensor.readCelsius();
//* If any thermosensor fault is detected
if (isnan(input))
{
reflowState = REFLOW_STATE_ERROR;
reflowStatus = REFLOW_STATUS_OFF;
Serial.println(F("Error"));
}
}
if (millis() > nextCheck)
{
//* Check input in the next seconds
nextCheck += SENSOR_SAMPLING_TIME;
//* If reflow process is on going
if (reflowStatus == REFLOW_STATUS_ON)
{
//* Toggle red LED as system heart beat
digitalWrite(Led, !(digitalRead(Led)));
//* Increase seconds timer for reflow curve plot
timerSeconds++;
//* Send temperature and time stamp to serial
Serial.print(timerSeconds);
Serial.print(F("s, "));
Serial.print(setpoint);
Serial.print(F(", "));
Serial.print(input);
Serial.print(F("C, "));
Serial.println(output);
}
else
{
//* Turn off red LED
digitalWrite(Led, LOW);
}
}
if (millis() > updateLcd)
{
//* Update LCD in the next 100 ms
updateLcd += UPDATE_RATE;
oled.clearDisplay();
oled.setTextSize(2);
oled.setCursor(0, 0);
oled.print(lcdMessagesReflowStatus[reflowState]);
oled.setTextSize(1);
oled.setCursor(115, 0);
if (reflowProfile == REFLOW_PROFILE_LEADFREE)
{
oled.print(F("LF"));
}
else
{
oled.print(F("PB"));
}
//* Temperature markers
oled.setCursor(0, 18);
oled.print(F("250"));
oled.setCursor(0, 36);
oled.print(F("150"));
oled.setCursor(0, 54);
oled.print(F("50"));
//* Draw temperature and time axis on monochrome oled
oled.drawLine(18, 18, 18, 63, WHITE);
oled.drawLine(18, 63, 127, 63, WHITE);
oled.setCursor(115, 0);
//* If currently in error state
if (reflowState == REFLOW_STATE_ERROR)
{
oled.setCursor(80, 9);
oled.print(F("TS Error"));
}
else
{
//* Right align temperature reading
if (input < 10)
oled.setCursor(91, 9);
else if (input < 100)
oled.setCursor(85, 9);
else
oled.setCursor(80, 9);
// oled current temperature
oled.print(input);
oled.print((char)247);
oled.print(F("'C"));
}
if (reflowStatus == REFLOW_STATUS_ON)
{
//* We are updating the display faster than sensor reading
if (timerSeconds > timerUpdate)
{
//* Store temperature reading every 3 s
if ((timerSeconds % 3) == 0)
{
timerUpdate = timerSeconds;
unsigned char averageReading = map(input, 0, 250, 63, 19);
if (x < (SCREEN_WIDTH - X_AXIS_START))
{
temperature[x++] = averageReading;
}
}
}
}
unsigned char timeAxis;
for (timeAxis = 0; timeAxis < x; timeAxis++)
{
oled.drawPixel(timeAxis + X_AXIS_START, temperature[timeAxis], WHITE);
}
//* Update screen
oled.display();
}
//* Reflow oven controller state machine
switch (reflowState)
{
case REFLOW_STATE_IDLE:
//* If oven temperature is still above room temperature
if (input >= TEMPERATURE_ROOM)
{
reflowState = REFLOW_STATE_TOO_HOT;
}
else
{
//* If switch is pressed to start reflow process
if (switchStatus == SWITCH_1)
{
//* Send header for CSV file
Serial.println(F("Time, Setpoint, Input, Output"));
//* Intialize seconds timer for serial debug information
timerSeconds = 0;
//* Initialize reflow plot update timer
timerUpdate = 0;
for (x = 0; x < (SCREEN_WIDTH - X_AXIS_START); x++)
{
temperature[x] = 0;
}
//* Initialize index for average temperature array used for reflow plot
x = 0;
//* Initialize PID control window starting time
windowStartTime = millis();
//* Ramp up to minimum soaking temperature
setpoint = TEMPERATURE_SOAK_MIN;
//* Load profile specific constant
if (reflowProfile == REFLOW_PROFILE_LEADFREE)
{
soakTemperatureMax = TEMPERATURE_SOAK_MAX_LF;
reflowTemperatureMax = TEMPERATURE_REFLOW_MAX_LF;
soakMicroPeriod = SOAK_MICRO_PERIOD_LF;
}
else
{
soakTemperatureMax = TEMPERATURE_SOAK_MAX_PB;
reflowTemperatureMax = TEMPERATURE_REFLOW_MAX_PB;
soakMicroPeriod = SOAK_MICRO_PERIOD_PB;
}
//* Tell the PID to range between 0 and the full window size
reflowOvenPID.SetOutputLimits(0, windowSize);
reflowOvenPID.SetSampleTime(PID_SAMPLE_TIME);
//* Turn the PID on
reflowOvenPID.SetMode(AUTOMATIC);
//* Proceed to preheat stage
reflowState = REFLOW_STATE_PREHEAT;
}
}
break;
case REFLOW_STATE_PREHEAT:
reflowStatus = REFLOW_STATUS_ON;
//* If minimum soak temperature is achieve
if (input >= TEMPERATURE_SOAK_MIN)
{
//* Chop soaking period into smaller sub-period
timerSoak = millis() + soakMicroPeriod;
//* Set less agressive PID parameters for soaking ramp
reflowOvenPID.SetTunings(PID_KP_SOAK, PID_KI_SOAK, PID_KD_SOAK);
//* Ramp up to first section of soaking temperature
setpoint = TEMPERATURE_SOAK_MIN + SOAK_TEMPERATURE_STEP;
//* Proceed to soaking state
reflowState = REFLOW_STATE_SOAK;
}
break;
case REFLOW_STATE_SOAK:
//* If micro soak temperature is achieved
if (millis() > timerSoak)
{
timerSoak = millis() + soakMicroPeriod;
//* Increment micro setpoint
setpoint += SOAK_TEMPERATURE_STEP;
if (setpoint > soakTemperatureMax)
{
//* Set agressive PID parameters for reflow ramp
reflowOvenPID.SetTunings(PID_KP_REFLOW, PID_KI_REFLOW, PID_KD_REFLOW);
//* Ramp up to first section of soaking temperature
setpoint = reflowTemperatureMax;
//* Proceed to reflowing state
reflowState = REFLOW_STATE_REFLOW;
}
}
break;
case REFLOW_STATE_REFLOW:
//* We need to avoid hovering at peak temperature for too long
//* Crude method that works like a charm and safe for the components
if (input >= (reflowTemperatureMax - 5))
{
//* Set PID parameters for cooling ramp
reflowOvenPID.SetTunings(PID_KP_REFLOW, PID_KI_REFLOW, PID_KD_REFLOW);
//* Ramp down to minimum cooling temperature
setpoint = TEMPERATURE_COOL_MIN;
//* Proceed to cooling state
reflowState = REFLOW_STATE_COOL;
}
break;
case REFLOW_STATE_COOL:
//* If minimum cool temperature is achieved
if (input <= TEMPERATURE_COOL_MIN)
{
//* Retrieve current time for buzzer usage
buzzerPeriod = millis() + 1000;
//* Turn on buzzer to indicate completion
digitalWrite(Buzzer, HIGH);
//* Turn off reflow process
reflowStatus = REFLOW_STATUS_OFF;
coolingOn = true;
digitalWrite(Fan, HIGH);
Fan_timer = millis();
if (coolingOn == true && millis() - Fan_timer > 90000)
{
digitalWrite(Fan, LOW); // turn off the Fan after 90s
coolingOn = false;
}
//* Proceed to reflow Completion state
reflowState = REFLOW_STATE_COMPLETE;
}
break;
case REFLOW_STATE_COMPLETE:
//* play buzzer for buzzerperiod
if (millis() > buzzerPeriod)
{
//* Turn off buzzer
digitalWrite(Buzzer, LOW);
//* Reflow process ended
reflowState = REFLOW_STATE_IDLE;
}
break;
case REFLOW_STATE_TOO_HOT:
//* If oven temperature drops below room temperature
if (input < TEMPERATURE_ROOM)
{
//* Ready to reflow
reflowState = REFLOW_STATE_IDLE;
}
break;
case REFLOW_STATE_ERROR:
//* Check for thermosensor fault
double c = thermosensor.readCelsius();
//* If thermosensor problem is still present - kinked, or broken, or not sitting in the terminal block correctly.
if (isnan(c))
{
//* Wait until thermosensor wire is re-connected
reflowState = REFLOW_STATE_ERROR;
}
else
{
//* Clear to perform reflow process
reflowState = REFLOW_STATE_IDLE;
}
break;
}
//* If switch 1 is pressed
if (switchStatus == SWITCH_1)
{
//* If currently reflow process is ongoing
if (reflowStatus == REFLOW_STATUS_ON)
{
//* Button press is for cancelling
//* Turn off reflow process NOW
reflowStatus = REFLOW_STATUS_OFF;
//* Reinitialize state machine
reflowState = REFLOW_STATE_IDLE;
}
}
//* Switch 2 is pressed
else if (switchStatus == SWITCH_2)
{
//* Only can switch reflow profile during idle
if (reflowState == REFLOW_STATE_IDLE)
{
//* Currently using lead-free reflow profile
if (reflowProfile == REFLOW_PROFILE_LEADFREE)
{
//* Switch to leaded reflow profile
reflowProfile = REFLOW_PROFILE_LEADED;
EEPROM.write(PROFILE_TYPE_ADDRESS, 1);
}
//* Currently using leaded reflow profile
else
{
//* Switch to lead-free profile
reflowProfile = REFLOW_PROFILE_LEADFREE;
EEPROM.write(PROFILE_TYPE_ADDRESS, 0);
}
}
}
//* Switch status has been read
switchStatus = SWITCH_NONE;
//* Switch debounce state machine (analog switch)
switch (debounceState)
{
case DEBOUNCE_STATE_IDLE:
//* No valid switch press
switchStatus = SWITCH_NONE;
switchValue = readSwitch();
//* If either switch is pressed
if (switchValue != SWITCH_NONE)
{
//* Note the pressed switch
switchMask = switchValue;
//* Intialize debounce counter
lastDebounceTime = millis();
//* Proceed to check validity of button press
debounceState = DEBOUNCE_STATE_CHECK;
}
break;
case DEBOUNCE_STATE_CHECK:
switchValue = readSwitch();
if (switchValue == switchMask)
{
//* If minimum debounce period is complete
if ((millis() - lastDebounceTime) > DEBOUNCE_PERIOD_MIN)
{
//* Valid switch press
switchStatus = switchMask;
//* Wait for button release
debounceState = DEBOUNCE_STATE_RELEASE;
}
}
//* A False trigger Dettected
else
{
//* Reinitialize button debounce state machine
debounceState = DEBOUNCE_STATE_IDLE;
}
break;
case DEBOUNCE_STATE_RELEASE:
switchValue = readSwitch();
if (switchValue == SWITCH_NONE)
{
//* Reinitialize button debounce state machine when no switches are pressed
debounceState = DEBOUNCE_STATE_IDLE;
}
break;
}
//* PID computation and appropriate triac control
if (reflowStatus == REFLOW_STATUS_ON)
{
currentTime = millis();
reflowOvenPID.Compute();
if ((currentTime - windowStartTime) > windowSize)
{
//* Time to shift the Relay Window
windowStartTime += windowSize;
}
if (output > (currentTime - windowStartTime))
digitalWrite(Triac, HIGH);
else
digitalWrite(Triac, LOW);
}
//* Reflow oven process is off, ensure oven is off
else
{
digitalWrite(Triac, LOW);
}
}
/*
coolingOn = true;
digitalWrite(Fan, HIGH);
Fan_timer = millis();
if (coolingOn == true && (millis() - Fan_timer) > 4000)
{
digitalWrite(Fan, LOW); // turn off the Fan after 90s
coolingOn = false;
}
*/