John Bradnam
Published © GPL3+

SMD Reflow Hot Plate (new version)

A upgrade to my SMD Hot Plate with a larger hot plate, better cooling and updated software.

IntermediateFull instructions provided12 hours4,820
SMD Reflow Hot Plate (new version)

Things used in this project

Hardware components

Microchip ATtiny3224
×1
400W Hot Plate
220V version
×1
5V, 240VAC 2A Solid State Relay
5V variant
×1
1.8in TFT color display
Check the pins match the PCB (8 pin variant)
×1
LED (generic)
LED (generic)
1 x 3mm Red, 1 x 3mm Blue
×1
70mm x 70mm case fan
×2
Tactile Switch, Top Actuated
Tactile Switch, Top Actuated
12mm x 12mm variant with round button tops
×2
SMD components
0805 resistors - 4 x 10k, 1 x 2K2 3 x 1k, 1 x 330R, 1 x 220R , 1 x 4K7, 1 x 0R; 0805 capacitors - 3 x 0.1uF; 1206 capacitors - 1 x 10uF; Transistors SOT 23 - 3 x BC817; Diodes SOD323 - 1 x 1N4148, 1 x 1N4007 SOD-123 Mini SMA; Regulators 1117-50 SOT223-T;
×1
240VAC to 12VDC 450mA module
×1
Buzzer
Buzzer
Active buzzer
×1
100K ohm NTC 3950 Thermistor
×1

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

Fan PCB & Insulation templates

See text

STL Files

Schematics

Schematic

PCB

Eagle files

Schematic & PCB in Eagle format

PCB (PDF)

PCB copper layout as a PDF file

Code

SMDHotPlateV3.ino

Arduino
/**************************************************************************
 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
   v3: Upgraded CPU to ATtiny3224
       Removed Ambient temperature sensor
       Fan only switches on for cooling

 --------------------------------------------------------------------------
 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 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

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 = 50.0;           // ambient temperature min
float cool = 45.0;              // Cool down temperature min
volatile MODE currentMode;      // Current mode
char buf[16];                   // Used to format strings

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

float Kp = 4;                   //Mine was 2 - How fast the system responds (too high cause overshoot)
float Ki = 0.0025;              //Mine was 0.0025 - How fast the steady state error is removed
float Kd = 9;                   //Mine was 9 - How far into the future to predict the rate of change
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(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();

  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)
      {
        digitalWrite(FAN_PIN, HIGH);
        if (temperature <= cool)
        {
          //Finished
          digitalWrite(HEAT_PIN, LOW);
          digitalWrite(FAN_PIN, LOW);
          currentMode = STOP;
          digitalWrite(SPEAKER, HIGH);
          delay(2000);
          digitalWrite(SPEAKER, LOW);
        }
      }
      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;

        //switch on fan if not heating
        digitalWrite(FAN_PIN, (temperature >= ambient && PID_Output == MIN_PID_VALUE) ? HIGH : LOW);
      }
    }
    else
    {
      //switch on fan while paused or stopped if temp more than cool down
      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 (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;
        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

Arduino
Thermistor library for Arduino IDE
No preview (download only).

Credits

John Bradnam

John Bradnam

145 projects • 179 followers

Comments