John Bradnam
Published © GPL3+

SMD Reflow Hot Plate

A DIY temperature controlled Hot Plate for soldering SMD components.

AdvancedFull instructions provided20 hours4,981
SMD Reflow Hot Plate

Things used in this project

Hardware components

Microchip ATtiny1614 Microprocessor
×1
200W Hot Plate
200W 220VAC variant
×1
5V, 240VAC 2A Solid State Relay
5V variant
×1
1.8in TFT color display
×1
LED (generic)
LED (generic)
1 red, 1 blue - both 3mm
×2
40mm Fan
×1
Tactile Switch, Top Actuated
Tactile Switch, Top Actuated
12x12mm with round button tops
×2
SMD components
0805 resistors - 4 x 10k, 3 x 1k, 2 x 330R, 1 x 220R , 2 x 4K7, 1 x 0R; 0805 capacitors - 3 x 0.1uF; 1206 capacitors - 2 x 10uF; Transistors SOT 23 - 3 x 2N3904; Diodes SOD323 - 1 x 1N4148
×1
240VAC to 5VDC 700mA module
Old 5V variant
×1
Buzzer
Buzzer
Active buzzer
×1
100K ohm NTC 3950 Thermistor
×2

Software apps and online services

Arduino IDE
Arduino IDE

Hand tools and fabrication machines

3D Printer (generic)
3D Printer (generic)
Soldering iron (generic)
Soldering iron (generic)

Story

Read more

Custom parts and enclosures

STL Files

STL files for 3D printing

Hot Plate Holder

Drill layout for Hot Plate holder

Heat insulation template

Schematics

Schematic

PCB

Eagle Files

Schematic and PCB in Eagle format

Code

SMDHotPlateV2.ino

C/C++
/**************************************************************************
 SMD Hot Plate

 2022-09-09 John Bradnam (jbrad2089@gmail.com)
   V1: Create program for ATtiny1614
   V2: Added PID code from electronoobs
       Added Ambient temperature support

 --------------------------------------------------------------------------
 Arduino IDE:
 --------------------------------------------------------------------------
  BOARD: ATtiny1614/1604/814/804/414/404/214/204
  Chip: ATtiny1614
  Clock Speed: 20MHz
  millis()/micros(): "Enabled (default timer)"
  Programmer: jtag2updi (megaTinyCore)

  ATTiny1614 Pins mapped to Ardunio Pins
 
              +--------+
          VCC + 1   14 + GND
  (SS)  0 PA4 + 2   13 + PA3 10 (SCK)
        1 PA5 + 3   12 + PA2 9  (MISO)
  (DAC) 2 PA6 + 4   11 + PA1 8  (MOSI)
        3 PA7 + 5   10 + PA0 11 (UPDI)
  (RXD) 4 PB3 + 6    9 + PB0 7  (SCL)
  (TXD) 5 PB2 + 7    8 + PB1 6  (SDA)
              +--------+
  
 **************************************************************************/


#include <Adafruit_GFX.h>    // Core graphics library
#include <Adafruit_ST7735.h> // Hardware-specific library
#include <SPI.h>
#include <thermistor.h>      //http://electronoobs.com/eng_arduino_thermistor.php

#define TFT_SCK 10    //PA3
#define TFT_MISO 9    //PA2
#define TFT_MOSI 8    //PA1
#define TFT_CS 0      //PA4
#define TFT_DC 4      //PB3
#define TFT_RST -1    //Not connected

#define TEMP_PIN 3    //PA7
#define AMBIENT_PIN 2 //PA6
#define HEAT_PIN 1    //PA5
#define FAN_PIN 5     //PB2
#define SWITCHES 6    //PB1
#define SPEAKER 7     //PB0

enum SWITCH {NONE, SELECT, START};
enum MODE {STOP, RUN, PAUSE};

#define EPSILON 2                 //Degrees from required to current before reacting
thermistor therm(TEMP_PIN,0);     //PA7 has 3950 Thermistor
thermistor amb(AMBIENT_PIN,0);    //PA6 has 3950 Thermistor

Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_RST);

#define STATUS_VALUE_X 4
#define TEMP_VALUE_X 64
#define TIME_VALUE_X 124
#define PROMPT_Y 116

#define GRAPH_X_MIN 22
#define GRAPH_X_DIV 8
#define GRAPH_X_GAP 15
#define GRAPH_X_TEXT 0
#define GRAPH_X_MAX (GRAPH_X_MIN + (GRAPH_X_DIV * GRAPH_X_GAP))
#define GRAPH_Y_MIN 92
#define GRAPH_Y_DIV 6
#define GRAPH_Y_GAP 15
#define GRAPH_Y_TEXT (GRAPH_Y_MIN + 5)
#define GRAPH_Y_MAX (GRAPH_Y_MIN - (GRAPH_Y_DIV * GRAPH_Y_GAP))

#define TABLES_IN_DATA_SPACE

typedef struct {
  int temp;     //Temperature to reach
  int period;   //Seconds to reach temperature
} TARGET;

#define NUMBER_OF_PERIODS 6
#ifdef TABLES_IN_DATA_SPACE
  const TARGET plot1[NUMBER_OF_PERIODS] = {{150,90},{150,180},{240,240},{240,260},{0,420},{0,0}};
  const TARGET plot2[NUMBER_OF_PERIODS] = {{150,50},{180,140},{240,175},{240,185},{120,250},{0,350}};
  const TARGET plot3[NUMBER_OF_PERIODS] = {{150,60},{200,120},{250,160},{250,190},{0,260},{0,0}};
#else
  const TARGET plot1[NUMBER_OF_PERIODS] PROGMEM = {{150,90},{150,180},{240,240},{240,260},{0,420},{0,0}};
  const TARGET plot2[NUMBER_OF_PERIODS] PROGMEM = {{150,50},{180,140},{240,175},{240,185},{120,250},{0,350}};
  const TARGET plot3[NUMBER_OF_PERIODS] PROGMEM = {{150,60},{200,120},{250,160},{250,190},{0,260},{0,0}};
#endif


#define NUMBER_OF_PLOTS 3
const TARGET* plots[NUMBER_OF_PLOTS] = {plot1, plot2, plot3};

volatile unsigned int minutes;  // In minutes
volatile unsigned int seconds;  // In seconds
volatile bool updateDisplay;    // Force display update
int currentPlot;                // Currently selected heating plot
float temperature;              // Current temperature
float ambient;                  // Outside temperature
volatile MODE currentMode;      // Current mode
bool buzzerOn;                  // Status of buzzer
char buf[16];                   // Used to format strings

/////////////////////PID VARIABLES///////////////////////
#define PID_REFRESH_RATE 50
#define MIN_PID_VALUE 0
#define MAX_PID_VALUE 180       //Max PID value. You can change this. 
uint32_t pidTimeout = 0;        //Used to hold next PID period

float Kp = 2;                   //Mine was 2
float Ki = 0.0025;              //Mine was 0.0025
float Kd = 9;                   //Mine was 9
float PID_Output = 0;
float PID_P, PID_I, PID_D;
float PID_ERROR, PREV_ERROR;
/////////////////////////////////////////////////////////

//---------------------------------------------------
// Hardware setup
void setup(void) 
{
  pinMode(TEMP_PIN, INPUT);
  pinMode(AMBIENT_PIN, INPUT);
  pinMode(HEAT_PIN, OUTPUT);
  digitalWrite(HEAT_PIN, LOW);
  pinMode(FAN_PIN, OUTPUT);
  analogWrite(FAN_PIN, 0);
  pinMode(SWITCHES, INPUT);
  pinMode(SPEAKER, OUTPUT);
  digitalWrite(SPEAKER, LOW);

  // If your TFT's plastic wrap has a Black Tab, use the following:
  tft.initR(INITR_BLACKTAB);   // initialize a ST7735S chip, black tab
  // If your TFT's plastic wrap has a Red Tab, use the following:
  //tft.initR(INITR_REDTAB);   // initialize a ST7735R chip, red tab
  // If your TFT's plastic wrap has a Green Tab, use the following:
  //tft.initR(INITR_GREENTAB); // initialize a ST7735R chip, green tab
  
  currentMode = STOP;
  minutes = 0;
  seconds = 0;
  RTCSetup();
  updateDisplay = true;

  tft.setTextWrap(false); // Allow text to run off right edge
  tft.fillScreen(ST7735_BLACK);
  tft.setRotation(3);
  tft.setTextSize(1);
  tft.setTextColor(ST7735_WHITE, ST7735_BLACK);

  drawGraphFrame();

  //Assume 10degC above current temperature is cold temperature
  ambient = amb.analog2temp() + 10;

  updateDisplay = true;
  currentPlot = 0;
  plotGraph(currentPlot);
}

//---------------------------------------------------
// Primary loop
void loop(void) 
{
  if (millis() > pidTimeout)
  {
    pidTimeout = millis() + PID_REFRESH_RATE; 
    temperature = therm.analog2temp();
    if (currentMode == RUN || currentMode == PAUSE)
    {
      int s = minutes * 60 + seconds;
      int t = timeToTemperature(currentPlot, s);
      if (s != 0 && t == 0)
      {
        //Finished
        digitalWrite(HEAT_PIN, LOW);
        currentMode = STOP;
        digitalWrite(SPEAKER, HIGH);
        buzzerOn = true;
      }
      else
      {
        //Calculate PID
        PID_ERROR = t - temperature;
        PID_P = Kp*PID_ERROR;
        PID_I = PID_I+(Ki*PID_ERROR);      
        PID_D = Kd * (PID_ERROR-PREV_ERROR);
        PID_Output = max(min(PID_P + PID_I + PID_D, MAX_PID_VALUE), MIN_PID_VALUE);
        analogWrite(HEAT_PIN, PID_Output);  //Change the Duty Cycle applied to the SSR
        PREV_ERROR = PID_ERROR;
      }
    }
    else
    {
      digitalWrite(FAN_PIN, (temperature >= ambient) ? HIGH : LOW);
    }
  }
  
  if (updateDisplay)
  {
    updateDisplay = false;

    //Status
    tft.setTextColor(ST7735_YELLOW, ST7735_BLACK);
    tft.setCursor(STATUS_VALUE_X, PROMPT_Y);
    switch(currentMode)
    {
      case STOP: strcpy(buf,"STOPPED"); break;
      case RUN: strcpy(buf,"RUNNING"); break;
      case PAUSE: strcpy(buf,"PAUSED"); break;
    }
    spadr(buf,9);
    tft.print(buf);
    
    //Temperature
    temperature = therm.analog2temp();
    tft.setCursor(TEMP_VALUE_X, PROMPT_Y);
    dtostrf(temperature, 3, 1, buf);
    strcat(buf," C");
    spadr(buf,7);
    tft.print(buf);
    if (currentMode != STOP)
    {
      plotCurrentTemperature(temperature, minutes * 60 + seconds);
    }

    //Time
    tft.setCursor(TIME_VALUE_X, PROMPT_Y);
    sprintf(buf,"%02d:%02d",minutes,seconds);
    spadr(buf,5);
    tft.print(buf);
  }

  SWITCH sw = readSwitches(true);
  if (currentMode == STOP || currentMode == PAUSE)
  {
    switch(sw)
    {
      case SELECT:
        if (buzzerOn)
        {
          digitalWrite(SPEAKER, LOW);   //Switch off speaker if on
          buzzerOn = false;
        }
        else
        {
          if (currentMode == PAUSE)
          {
            currentMode = STOP;   //Stop if paused
            minutes = 0;
            seconds = 0;
            digitalWrite(HEAT_PIN, LOW);
          }
          else
          {
            currentPlot++;
            if (currentPlot == NUMBER_OF_PLOTS)
            {
              currentPlot = 0;
            }
          }
          tft.fillScreen(ST7735_BLACK);
          drawGraphFrame();
          plotGraph(currentPlot);
          updateDisplay = true;
        }
        break;
  
      case START:
        if (currentMode == STOP)
        {
          minutes = 0;
          seconds = 0;
          tft.fillScreen(ST7735_BLACK);
          drawGraphFrame();
          plotGraph(currentPlot);
        }
        currentMode = RUN;
        digitalWrite(FAN_PIN, HIGH);      //Run fan
        updateDisplay = true;
        break;

      case NONE:
        break;
    }
  }
  else if (sw == START)
  {
    //Currently running
    currentMode = PAUSE;
    updateDisplay = true;
  }
  delay(100);
}

//---------------------------------------------------------------------
// Real-Time Clock Setup
void RTCSetup() 
{
  // Initialize RTC
  while (RTC.STATUS > 0);                           // Wait until registers synchronized

  //Use the internal oscillator
  RTC.CLKSEL = RTC_CLKSEL_INT32K_gc;                    // 32.768kHz Internal Oscillator  
  
  RTC.PITINTCTRL = RTC_PI_bm;                           //Periodic Interrupt: enabled
  RTC.PITCTRLA = RTC_PERIOD_CYC32768_gc | RTC_PITEN_bm; //RTC Clock Cycles 32768, resulting in 32.768kHz/32768 = 1Hz and enable
}

//---------------------------------------------------------------------
//RTC interrupt occurs every second
ISR(RTC_PIT_vect)
{
  if (currentMode == RUN)
  {
    if (seconds < 59)
    {
      seconds++;
    }
    else
    {
      seconds = 0;
      minutes++;
    }
  }
  updateDisplay = true;
  RTC.PITINTFLAGS = RTC_PI_bm;          //Clear flag by writing '1'
}

//---------------------------------------------------------------------
//Read current switches state
// - wait - True to wait for button released if pressed
// - Returns NONE, START or SELECT
SWITCH readSwitches(bool wait)
{
  SWITCH sw = NONE;
  int value = analogRead(SWITCHES);
  if (value < 1000)
  {
    delay(10);    //debounce
    if (value == analogRead(SWITCHES))
    {
      sw = (value < 100) ? START : SELECT;
      if (wait)
      {
        //wait for release
        while (analogRead(SWITCHES) < 1000)
        {
          delay(50);
        }
      }
    }
  }
  return sw;
}

//---------------------------------------------------------------------
// Draw grid and labels on X and Y axis
void drawGraphFrame()
{
  //5 red, 6 green, 5 blue
  #define ST7735_GRAY 0xC618 //0xC0C0C0
  for(int x = 0; x < GRAPH_X_DIV; x++)
  {
    tft.drawLine(GRAPH_X_MIN + (x * GRAPH_X_GAP), GRAPH_Y_MIN, GRAPH_X_MIN + (x * GRAPH_X_GAP), GRAPH_Y_MAX, ST7735_GRAY);
    if (x != 0)
    {
      tft.setTextColor(ST7735_WHITE, ST7735_BLACK);
      tft.setCursor(GRAPH_X_MIN + (x * GRAPH_X_GAP) - 2,GRAPH_Y_TEXT);
      if (x == (GRAPH_X_DIV - 1))
      {
        tft.print("(min)");
      }
      else
      {
        tft.print(x);
      }
    }
  }
  for(int y = 0; y < GRAPH_Y_DIV; y++)
  {
    tft.drawLine(GRAPH_X_MIN, GRAPH_Y_MIN - (y * GRAPH_Y_GAP), GRAPH_X_MAX, GRAPH_Y_MIN - (y * GRAPH_Y_GAP), ST7735_GRAY);
    if (y != 0)
    {
      tft.setTextColor(ST7735_WHITE, ST7735_BLACK);
      tft.setCursor(GRAPH_X_TEXT, GRAPH_Y_MIN - (y * GRAPH_Y_GAP) - 4);
      sprintf(buf,"%3d",y*50);
      tft.print(buf);
    }
  }
  tft.drawLine(GRAPH_X_MIN, GRAPH_Y_MIN, GRAPH_X_MIN, GRAPH_Y_MAX, ST7735_WHITE);
  tft.drawLine(GRAPH_X_MIN, GRAPH_Y_MIN, GRAPH_X_MAX, GRAPH_Y_MIN, ST7735_WHITE);
  tft.drawLine(GRAPH_X_MAX, GRAPH_Y_MIN, GRAPH_X_MAX, GRAPH_Y_MAX, ST7735_WHITE);
  tft.drawLine(GRAPH_X_MIN, GRAPH_Y_MIN - (GRAPH_Y_DIV * GRAPH_Y_GAP), GRAPH_X_MAX, GRAPH_Y_MAX, ST7735_WHITE);
}

//---------------------------------------------------------------------
//Plot current graph
// plot - Graph to plot
void plotGraph(int plot)
{
  const TARGET* p = plots[plot];
  int te, se;
  int ss = 0;
  int ts = 0;
  for (int i = 0; i < NUMBER_OF_PERIODS; i++)
  {
    #ifdef TABLES_IN_DATA_SPACE
      te = p->temp;
      se = p->period;
    #else
      te = pgm_read_word(&p->temp);
      se = pgm_read_word(&p->period);
    #endif
    if (te != 0 || se != 0 || i == 0)
    {
      tft.drawLine(toGraphX(ss),toGraphY(ts),toGraphX(se),toGraphY(te),ST7735_CYAN);
    }
    ts = te;
    ss = se;
    p++;
  }
}

//---------------------------------------------------------------------
//Convert a time in seconds to the X position on the graph
// x - time in seconds
// returns X position on graph
int toGraphX(int x)
{
  return (GRAPH_X_MIN + (x * GRAPH_X_GAP) / 60);
}

//---------------------------------------------------------------------
//Convert a temperature to the Y position on the graph
// y - temperature in degrees C
// returns Y position on graph
int toGraphY(int y)
{
  return (GRAPH_Y_MIN - (y * GRAPH_Y_GAP) / 50);
}

//---------------------------------------------------------------------
//Highlight the current temparture
// t - Current temperature
// s - Current time in seconds
void plotCurrentTemperature(float t, int s)
{
  tft.fillCircle(toGraphX(s),toGraphY(round(t)),1,ST7735_RED);
}      

//---------------------------------------------------------------------
//Highlight the expected temparture
// plot - Graph being plotted
// s - Current time in seconds
// Returns false when reached end
bool plotExpectedTemperature(int plot, int s)
{
  int t = timeToTemperature(plot, s);
  tft.fillCircle(toGraphX(s),toGraphY(t),1,ST7735_RED);
  return (s == 0 || t != 0);
}      

//---------------------------------------------------------------------
//Convert a time in seconds to a temperature
// plot - Graph being plotted
// s - Current time in seconds
// Returns temperature expected
int timeToTemperature(int plot, int s)
{
  const TARGET* p = plots[plot];
  long te, se;
  long ss = 0;
  long ts = 0;
  for (int i = 0; i < NUMBER_OF_PERIODS; i++)
  {
    #ifdef TABLES_IN_DATA_SPACE
      te = p->temp;
      se = p->period;
    #else
      te = pgm_read_word(&p->temp);
      se = pgm_read_word(&p->period);
    #endif
    if (te == 0 && se == 0)
    {
      return 0;
    }
    else if (s <= se)
    {
      //found target
      return ts + (((long)s - ss) * (te - ts)) / (se - ss);
    }
    ts = te;
    ss = se;
    p++;
  }
  return 0;
}

//---------------------------------------------------------------------
//Pad string right with spaces
// - p pointer to start of string
// - l length of final string
void spadr(char* p, int l)
{
  int k = strlen(p);
  int i = k;
  for(; i < l; i++)
  {
    p[i] = ' ';
  }
  p[i] = '\0';
}

ThermistorLibrary.zip

C/C++
No preview (download only).

Credits

John Bradnam

John Bradnam

141 projects • 167 followers
Thanks to electronoobs.

Comments