Project tutorial
DIY SMD Rework Station

DIY SMD Rework Station © GPL3+

DIY Arduino hot air SMD rework station.

  • 37,106 views
  • 219 comments
  • 76 respects

Components and supplies

Necessary tools and machines

09507 01
Soldering iron (generic)

Apps and online services

About this project

Intro: DIY SMD Rework Station

In this tutorial you can learn how to make a hot air gun controller using Arduino and other common components. In this project, PID algorithm is used to calculate the power required and is controlled by a isolated Triac driver.

This project uses a handle compatible with 858D. It has a K-type thermocouple, 700 watt 230 VAC heater and a 24 VDC fan.

This controller is efficient and reliable compared to the commercial one and is easy to build.

Step 1: Wiring

The complete schematis shown on the picture below.


Wiring for I2C LCD module:

I2C Module<--------------> Arduino Pro Mini

GND<----------------------->GND<--------->GND

VCC<------------------------>VCC<--------->5V

SDA<------------------------------------------->A4

SCL<------------------------------------------->A5.

Wiring for rotary encoder module:

Encoder<---------------------->Arduino

GND<--------------------------->GND

+<-------------------------------->NC(Not Connected, code uses inbuilt input pull-up of arduino)

SW<----------------------------->D5

DT<------------------------------>D3

CLK<---------------------------->D4.

Wiring of handle: (7 wire)

3pin connector - (Green, Black, Red)

Red wire<----------------------->Thermocouple +

Green wire<-------------------->Reed Switch

Black wire<--------------------->Common ground.

2 pin connector - (Blue, Yellow)

Blue wire<--------------------------> Fan +0

Yellow wire<------------------------>Fan - (or GND)

2 Big pin connector -(White, Brown)

White wire<-----------------------> Heater

Brown wire<----------------------> Heater(no polarity)

NOTE:

The wiring of the hot air gun handle may be different for different type of wands. So, refer to the wiring diagram in the photo and follow the path of the wire to find the respective pins.

Step 2: Circuit Diagram

The circuit consists of 3 parts mainly.

The Interface Part:

It consists of a 1602 LCD display with I2C module and a rotary encoder with push button. The display shows the set temperature, current temperature, Fan speed and applied power and current status of the handle. The encoder is used for various inputs and to navigate through the options and controls.

The Sensor Part:

It consists of a K-type thermocouple for temperature sensing and a reed switch for determining the position of the handle. The thermocouple's voltage is amplified by the op-amp to a voltage level measurable by the arduino. The gain of the op-amp is controlled by 200K trim pot.

The Controller Part:

There are mainly 2 controllers in this circuit. The one is a simple PWM Fan speed controller with a MOSFET. The other one is a isolated controller for heater. It consists of a TRIAC driven by a opto-coupled DIAC and it is done by controlling the number of wave cycles that is delivered to the heater. The 4N25 optocoupler helps to maintain the sync with the AC waveform.

Step 3: PCB

The circuit of this project is a bit complicated, So I recommend you to use a printed board than a dot PCB. If you want to make your own PCB I have attached the eagle files at the end of the project. But, If you want to get them done by a PCB manufacturing company you can order it from JLCPCB

. You can view the Easy EDA design through this link : https://easyeda.com/ManojBR/harws1-1

Step 4: The Code and Libraries

The program is the most crucial part of the project and thanks a lot for sfrwmaker writing the program. The program uses PID algorithm to control the power to maintain the set temperature. It works by controlling the number of wave cycles delivered to the handle per second.

When the controller is turned on the wand will be in OFF state. By rotating the encoder the temperature and fan speed can be adjusted. Short press of the encoder will switch between the Fan speed and Set temperature adjustment.

The Hot air gun starts heating as soon as it is lifted up from the holder and shows Ready and make a short beep when it reaches the set temperature. It will turn off heating as soon as it is put back into the holder. But, the fan will continue to blow until it reaches the safe temperature. After the temperature falls below 50 C it will make a short beep and displays COLD.

When the hot air gun is off, the controller will enter Setup mode if the encoder is long pressed.

The setup mode has Calibrate, Tune, Save and Cancel and Reset Config options.

Note: If you are using PCB from easyEDA then you should change the pin number of reed switch to pin no. 8 and Buzzer pin to pin no.6

you have to install the Commoncontrols-master library andtime-master library for the code to work properly.

Caution: Do not connect the controller to the power outlet when flashing the firmware. Non-isolated power supply of the fan could damage your laptop.

download sketch source code from the project page.

Step 5: Setup

The temperature readings should be calibrated with the original value to get reasonable readings. So, in order to do that you should follow the following steps.

First, go to the setup mode and select the Tune option.In the tune mode the internal temperature (0-1023) is displayed on the screen.Rotate the encoder to manually select the applied power to the hot air gun. Heat the gun to 400 degrees.When the temperature and dispersion become low, the controller beeps. Then tune the trim-pot to set the internal temperature about 900 (in the internal units). Long press to the encoder return to the menu

Then, go to the setup mode select Calibrate option. Choose the calibration point: 200, 300 or 400 degrees, press the encoder. The hot gun will reach the desired temperature and beeps. By rotating the encoder, enter the real temperature. Then select another reference point and repeat this process for all the calibration point.

After this long press and come to main screen and then again go to Setup mode and select save.

And now the Hot air rework station is done.

Step 6: Video!

Have a look at the working of controller in the video.

Thank you sfrwmaker for writing the code.

Thanks to LCSC for their support. LCSC Electronics is one of the fastest growing suppliers of electronic components in China. LCSC has been committed to offering multitudinous, genuine and in-stock items, since its founding in 2011. Aiming to provide the whole world with more superior parts from Asia. More details please visit: https://lcsc.com/

If you have to make your own PCB at home, checkout this tutorial: https://www.instructables.com/id/PCB-Making-1/

Thank you.

Code

Firmware 1.4C/C++
Active/passive buzzer support. Pleas change the BUZZER_ACTIVE parameter
No more increased rotary encoder. Value changes by 1.
/*
 * Hot air gun controller based on atmega328 IC
 * Version 1.4
 * Released Dec 05, 2020
 */
#include <avr/io.h>
#include <avr/interrupt.h>
#include <Wire.h> 
#include <LiquidCrystal_I2C.h>
#include <CommonControls.h>
#include <EEPROM.h>
#include <SPI.h>

const uint16_t temp_minC 	= 100;                                          // Minimum temperature the controller can check accurately
const uint16_t temp_maxC	= 500;                                          // Maximum possible temperature
const uint16_t temp_ambC    = 25;                                           // Average ambient temperature
const uint16_t temp_tip[3] = {200, 300, 400};                               // Temperature reference points for calibration
const uint16_t min_working_fan = 100;                                       // Minimal possible fan speed

const uint8_t AC_SYNC_PIN   = 2;                                            // Outlet 220 v synchronization pin. Do not change!
const uint8_t HOT_GUN_PIN   = 7;                                            // Hot gun heater management pin
const uint8_t FAN_GUN_PIN   = 9;                                            // Hot gun fan management pin. Do not change! 
const uint8_t TEMP_GUN_PIN	= A0;                                           // Hot gun temperature checking pin

const uint8_t R_MAIN_PIN	= 3;                                            // Rotary encoder main pin. Do not change!
const uint8_t R_SECD_PIN	= 4;                                            // Rotary encoder secondary pin
const uint8_t R_BUTN_PIN	= 5;                                            // Rotary encoder button pin

const uint8_t REED_SW_PIN   = 8;                                            // Reed switch pin
const uint8_t BUZZER_PIN	= 6;                                            // Buzzer pin
const bool    BUZZER_ACTIVE = true;                                         // Active buzzer beeps when +5v supplied to it

//------------------------------------------ Configuration data ------------------------------------------------
/* Config record in the EEPROM has the following format:
 * uint32_t ID                           each time increment by 1
 * struct cfg                            config data, 8 bytes
 * byte CRC                              the checksum
*/
struct cfg {
    uint32_t    calibration;                                                // Packed calibration data by three temperature points
    uint16_t    temp;                                                       // The preset temperature of the IRON in internal units
    uint8_t     fan;                                                        // The preset fan speed 0 - 255
    uint8_t     off_timeout;                                                // Automatic switch-off timeout
};

class CONFIG {
    public:
        CONFIG() {
            can_write     = false;
            buffRecords   = 0;
            rAddr = wAddr = 0;
            eLength       = 0;
            nextRecID     = 0;
            uint8_t rs = sizeof(struct cfg) + 5;                             // The total config record size
            // Select appropriate record size; The record size should be power of 2, i.e. 8, 16, 32, 64, ... bytes
            for (record_size = 8; record_size < rs; record_size <<= 1);
        }
        void init();
        bool load(void);
        void getConfig(struct cfg &Cfg);                                    // Copy config structure from this class
        void updateConfig(struct cfg &Cfg);                                 // Copy updated config into this class
        bool save(void);                                                    // Save current config copy to the EEPROM
        bool saveConfig(struct cfg &Cfg);                                   // write updated config into the EEPROM

    protected:
        struct   cfg Config;

    private:
        bool     readRecord(uint16_t addr, uint32_t &recID);
        bool     can_write;                                                 // The flag indicates that data can be saved
        uint8_t  buffRecords;                                               // Number of the records in the outpt buffer
        uint16_t rAddr;                                                     // Address of thecorrect record in EEPROM to be read
        uint16_t wAddr;                                                     // Address in the EEPROM to start write new record
        uint16_t eLength;                                                   // Length of the EEPROM, depends on arduino model
        uint32_t nextRecID;                                                 // next record ID
        uint8_t  record_size;                                               // The size of one record in bytes
};

 // Read the records until the last one, point wAddr (write address) after the last record
void CONFIG::init(void) {
    eLength = EEPROM.length();
    uint32_t recID;
    uint32_t minRecID = 0xffffffff;
    uint16_t minRecAddr = 0;
    uint32_t maxRecID = 0;
    uint16_t maxRecAddr = 0;
    uint8_t  records = 0;

    nextRecID = 0;

    // read all the records in the EEPROM find min and max record ID
    for (uint16_t addr = 0; addr < eLength; addr += record_size) {
        if (readRecord(addr, recID)) {
            ++records;
            if (minRecID > recID) {
                minRecID = recID;
                minRecAddr = addr;
            }
            if (maxRecID < recID) {
                maxRecID = recID;
                maxRecAddr = addr;
            }
        } else {
            break;
        }
    }

    if (records == 0) {
        wAddr = rAddr = 0;
        can_write = true;
        return;
    }

    rAddr = maxRecAddr;
    if (records < (eLength / record_size)) {                                // The EEPROM is not full
        wAddr = rAddr + record_size;
        if (wAddr > eLength) wAddr = 0;
    } else {
        wAddr = minRecAddr;
    }
    can_write = true;
}

void CONFIG::getConfig(struct cfg &Cfg) {
    memcpy(&Cfg, &Config, sizeof(struct cfg));
}

void CONFIG::updateConfig(struct cfg &Cfg) {
    memcpy(&Config, &Cfg, sizeof(struct cfg));
}

bool CONFIG::saveConfig(struct cfg &Cfg) {
    updateConfig(Cfg);
    return save();                                                          // Save new data into the EEPROM
}

bool CONFIG::save(void) {
    if (!can_write) return can_write;
    if (nextRecID == 0) nextRecID = 1;

    uint16_t startWrite = wAddr;
    uint32_t nxt = nextRecID;
    uint8_t summ = 0;
    for (uint8_t i = 0; i < 4; ++i) {
        EEPROM.write(startWrite++, nxt & 0xff);
        summ <<=2; summ += nxt;
        nxt >>= 8;
    }
    uint8_t* p = (byte *)&Config;
    for (uint8_t i = 0; i < sizeof(struct cfg); ++i) {
        summ <<= 2; summ += p[i];
        EEPROM.write(startWrite++, p[i]);
    }
    summ ++;                                                                // To avoid empty records
    EEPROM.write(wAddr+record_size-1, summ);

    rAddr = wAddr;
    wAddr += record_size;
    if (wAddr > EEPROM.length()) wAddr = 0;
    nextRecID ++;                                                           // Get ready to write next record
    return true;
}

bool CONFIG::load(void) {
    bool is_valid = readRecord(rAddr, nextRecID);
    nextRecID ++;
    return is_valid;
}

bool CONFIG::readRecord(uint16_t addr, uint32_t &recID) {
    uint8_t Buff[record_size];

    for (uint8_t i = 0; i < record_size; ++i) 
        Buff[i] = EEPROM.read(addr+i);
  
    uint8_t summ = 0;
    for (byte i = 0; i < sizeof(struct cfg) + 4; ++i) {
        summ <<= 2; summ += Buff[i];
    }
    summ ++;                                                                // To avoid empty fields
    if (summ == Buff[record_size-1]) {                                      // Checksumm is correct
        uint32_t ts = 0;
        for (char i = 3; i >= 0; --i) {
            ts <<= 8;
            ts |= Buff[byte(i)];
        }
        recID = ts;
        memcpy(&Config, &Buff[4], sizeof(struct cfg));
        return true;
    }
    return false;
}

//------------------------------------------ class HOT GUN CONFIG ----------------------------------------------
class HOTGUN_CFG : public CONFIG {
    public:
        HOTGUN_CFG()                                                        { }
        void     init(void);
        uint16_t tempPreset(void);                                          // The preset temperature in internal units
		uint8_t	 fanPreset(void);                                           // The preset fan speed 0 - 255 
        uint16_t tempInternal(uint16_t temp);                               // Translate the human readable temperature into internal value
        uint16_t tempHuman(uint16_t temp);                                  // Translate temperature from internal units to the Celsius
        void     save(uint16_t temp, uint8_t fanSpeed);                     // Save preset temperature in the internal units and fan speed
        void     applyCalibrationData(uint16_t tip[3]);
        void     getCalibrationData(uint16_t tip[3]);
        void     saveCalibrationData(uint16_t tip[3]);
        void     setDefaults(bool Write);                                   // Set default parameter values if failed to load data from EEPROM
    private:
        uint16_t t_tip[3];
        const   uint16_t def_tip[3] = {587, 751, 850};                      // Default values of internal sensor readings at reference temperatures
        const   uint16_t min_temp  = 50;
        const   uint16_t max_temp  = 900;
        const   uint16_t def_temp  = 600;                                   // Default preset temperature
        const   uint8_t  def_fan   = 64;                                  	// Default preset fan speed 0 - 255
        const   uint16_t ambient_temp = 0;
        const   uint16_t ambient_tempC= 25;
};

void HOTGUN_CFG::init(void) {
    CONFIG::init();
    if (!CONFIG::load()) setDefaults(false);                                // If failed to load the data from EEPROM, initialize the config data with the default values
    uint32_t   cd = Config.calibration;
    t_tip[0] = cd & 0x3FF; cd >>= 10;                                       // 10 bits per calibration parameter, because the ADC readings are 10 bits
    t_tip[1] = cd & 0x3FF; cd >>= 10;
    t_tip[2] = cd & 0x3FF;
    // Check the tip calibration is correct
    if ((t_tip[0] >= t_tip[1]) || (t_tip[1] >= t_tip[2])) {
        setDefaults(false);
        for (uint8_t i = 0; i < 3; ++i)
            t_tip[i] = def_tip[i];
    }
    return;
}
    uint32_t    calibration;                                                // Packed calibration data by three temperature points
    uint16_t    temp;                                                       // The preset temperature of the IRON in internal units
    uint8_t     fan;                                                        // The preset fan speed 0 - 255
    uint8_t     off_timeout;                                                // Automatic switch-off timeout

uint16_t HOTGUN_CFG::tempPreset(void) {
    return Config.temp;
}

uint8_t HOTGUN_CFG::fanPreset(void) {
    return Config.fan;
}

uint16_t HOTGUN_CFG::tempInternal(uint16_t t) {                             // Translate the human readable temperature into internal value
    t = constrain(t, temp_minC, temp_maxC);
    uint16_t left   = 0;
    uint16_t right  = 1023;                                                 // Maximum temperature value in internal units
    uint16_t temp = map(t, temp_tip[0], temp_tip[2], t_tip[0], t_tip[2]);

    if (temp > (left+right)/ 2) {
        temp -= (right-left) / 4;
    } else {
        temp += (right-left) / 4;
    }

    for (uint8_t i = 0; i < 20; ++i) {
        uint16_t tempH = tempHuman(temp);
        if (tempH == t) {
            return temp;
        }
        uint16_t new_temp;
        if (tempH < t) {
            left = temp;
             new_temp = (left+right)/2;
            if (new_temp == temp)
                new_temp = temp + 1;
        } else {
            right = temp;
            new_temp = (left+right)/2;
            if (new_temp == temp)
                new_temp = temp - 1;
        }
        temp = new_temp;
    }
    return temp;
}

// Thanslate temperature from internal units to the human readable value (Celsius or Fahrenheit)
uint16_t HOTGUN_CFG::tempHuman(uint16_t temp) {
    uint16_t tempH = 0;
    
    if (temp <= ambient_temp) {
        tempH = ambient_tempC;
    } else if (temp < t_tip[0]) {
        tempH = map(temp, ambient_temp, t_tip[0], ambient_tempC, temp_tip[0]);
    } else if (temp <= t_tip[1]) {
        tempH = map(temp, t_tip[0], t_tip[1], temp_tip[0], temp_tip[1]);
    } else if (temp <= t_tip[2]) {
        tempH = map(temp, t_tip[1], t_tip[2], temp_tip[1], temp_tip[2]);
    } else {
        tempH = map(temp, t_tip[0], t_tip[2], temp_tip[0], temp_tip[2]);
    }
    return tempH;
}

void HOTGUN_CFG::save(uint16_t temp, uint8_t fanSpeed) {
    Config.temp        = constrain(temp, min_temp, max_temp);
    Config.fan         = fanSpeed;
    CONFIG::save();                                                         // Save new data into the EEPROM
}

void HOTGUN_CFG::applyCalibrationData(uint16_t tip[3]) {
    if (tip[0] < ambient_temp) {
        uint16_t t = ambient_temp + tip[1];
        tip[0] = t >> 1;
    }
    t_tip[0] = tip[0];
    t_tip[1] = tip[1];
    if (tip[2] > max_temp) tip[2] = max_temp; 
    t_tip[2] = tip[2];
}

void HOTGUN_CFG::getCalibrationData(uint16_t tip[3]) {
    tip[0] = t_tip[0];
    tip[1] = t_tip[1];
    tip[2] = t_tip[2];
}

void HOTGUN_CFG::saveCalibrationData(uint16_t tip[3]) {
    if (tip[2] > max_temp) tip[2] = max_temp;
    uint32_t cd = tip[2] & 0x3FF; cd <<= 10;                                // Pack tip calibration data in one 32-bit word: 10-bits per value
    cd |= tip[1] & 0x3FF; cd <<= 10;
    cd |= tip[0];
    Config.calibration = cd;
    t_tip[0] = tip[0];
    t_tip[1] = tip[1];
    t_tip[2] = tip[2];
}

void HOTGUN_CFG::setDefaults(bool Write) {
    uint32_t c = def_tip[2] & 0x3FF; c <<= 10;
    c |= def_tip[1] & 0x3FF;         c <<= 10;
    c |= def_tip[0] & 0x3FF;
    Config.calibration = c;
    Config.temp        = def_temp;
    Config.fan         = def_fan;
    if (Write) {
        CONFIG::save();
    }
}

//------------------------------------------ class BUZZER ------------------------------------------------------
class BUZZER {
    public:
        BUZZER(byte buzzerP, bool active = true)
                            { buzzer_pin = buzzerP; this->active = active; }
        void init(void);
        void shortBeep(void);
        void lowBeep(void);
        void doubleBeep(void);
        void failedBeep(void);
    private:
        byte buzzer_pin;
        bool active;
};

void BUZZER::init(void) {
    pinMode(buzzer_pin, OUTPUT);
    if (active) {
        digitalWrite(buzzer_pin, LOW);
    } else {
        noTone(buzzer_pin);
    }
}

void BUZZER::shortBeep(void) {
    if (active) {
       digitalWrite(buzzer_pin, HIGH);
       delay(80);
       digitalWrite(buzzer_pin, LOW); 
    } else {
        tone(buzzer_pin, 3520, 160);
    }
}

void BUZZER::lowBeep(void) {
    if (active) {
        digitalWrite(buzzer_pin, HIGH);
        delay(160);
        digitalWrite(buzzer_pin, LOW);
    } else {
        tone(buzzer_pin,  880, 160);
    }
}

void BUZZER::doubleBeep(void) {
    if (active) {
        digitalWrite(buzzer_pin, HIGH);
        delay(160);
        digitalWrite(buzzer_pin, LOW);
        delay(150);
        digitalWrite(buzzer_pin, HIGH);
        delay(160);
        digitalWrite(buzzer_pin, LOW);
    } else {
        tone(buzzer_pin, 3520, 160);
        delay(300);
        tone(buzzer_pin, 3520, 160);
    }
}

void BUZZER::failedBeep(void) {
    if (active) {
        digitalWrite(buzzer_pin, HIGH);
        delay(170);
        digitalWrite(buzzer_pin, LOW);
        delay(10);
        digitalWrite(buzzer_pin, HIGH);
        delay(80);
        digitalWrite(buzzer_pin, LOW);
        delay(100);
        digitalWrite(buzzer_pin, HIGH);
        delay(80);
        digitalWrite(buzzer_pin, LOW); 
    } else {
        tone(buzzer_pin, 3520, 160);
        delay(170);
        tone(buzzer_pin,  880, 250);
        delay(260);
        tone(buzzer_pin, 3520, 160);
    }
}

//------------------------------------------ class lcd DSPLay for soldering IRON -----------------------------
class DSPL : protected LiquidCrystal_I2C {
    public:
        DSPL(void) : LiquidCrystal_I2C(0x27, 16, 2) { }
        void    init(void);
        void    clear(void)                                                 { LiquidCrystal_I2C::clear(); }
        void    tSet(uint16_t t, bool Celsius = true);                      // Show the preset temperature
        void    tCurr(uint16_t t);                                          // Show the current temperature
        void    tInternal(uint16_t t);                                      // Show the current temperature in internal units
        void    tReal(uint16_t t);                                          // Show the real temperature in Celsius in calibrate mode
        void    fanSpeed(uint8_t s);                                        // Show the fan speed
		void	appliedPower(uint8_t p, bool show_zero = true);			    // Show applied power (%)
        void    setupMode(uint8_t mode);
        void    msgON(void);                                                // Show message: "ON"
        void    msgOFF(void);
        void    msgReady(void);
        void    msgCold(void);
        void    msgFail(void);                                              // Show 'Fail' message
        void    msgTune(void);                                              // Show 'Tune' message
    private:
        bool 	full_second_line;                                           // Whether the second line is full with the message
		char 	temp_units;
        const   uint8_t custom_symbols[3][8] = {
                          { 0b00110,                                        // Degree
                            0b01001,
                            0b01001,
                            0b00110,
                            0b00000,
                            0b00000,
                            0b00000,
                            0b00000
                          },
                          { 0b00100,                                        // Fan sign
                            0b01100,
                            0b01100,
                            0b00110,
                            0b01011,
                            0b11001,
                            0b10000,
                            0b00000
                          },
                          { 0b00011,                                        // Power sign
                            0b00110,
                            0b01100,
                            0b11111,
                            0b00110,
                            0b01100,
                            0b01000,
                            0b10000
                          }
                        };
};

void DSPL::init(void) {
    LiquidCrystal_I2C::begin();
    LiquidCrystal_I2C::clear();
    for (uint8_t i = 0; i < 3; ++i)
        LiquidCrystal_I2C::createChar(i+1, (uint8_t *)custom_symbols[i]);
    full_second_line = false;
	temp_units = 'C';
}

void DSPL::tSet(uint16_t t, bool Celsius) {
    char buff[10];
	if (Celsius) {
		temp_units = 'C';
	} else {
		temp_units = 'F';
	}
    LiquidCrystal_I2C::setCursor(0, 0);
    sprintf(buff, "Set:%3d%c%c", t, (char)1, temp_units);
    LiquidCrystal_I2C::print(buff);
}

void DSPL::tCurr(uint16_t t) {
    char buff[6];
    LiquidCrystal_I2C::setCursor(0, 1);
    if (t < 1000) {
        sprintf(buff, "%3d%c ", t, (char)1);
    } else {
        LiquidCrystal_I2C::print(F("xxx"));
        return;
    }
    LiquidCrystal_I2C::print(buff);
    if (full_second_line) {
        LiquidCrystal_I2C::print(F("           "));
        full_second_line = false;
    }
}

void DSPL::tInternal(uint16_t t) {
    char buff[6];
    LiquidCrystal_I2C::setCursor(0, 1);
    if (t < 1023) {
        sprintf(buff, "%4d ", t);
    } else {
        LiquidCrystal_I2C::print(F("xxxx"));
        return;
    }
    LiquidCrystal_I2C::print(buff);
    if (full_second_line) {
        LiquidCrystal_I2C::print(F("           "));
        full_second_line = false;
    }
}

void DSPL::tReal(uint16_t t) {
    char buff[6];
    LiquidCrystal_I2C::setCursor(11, 1);
    if (t < 1000) {
        sprintf(buff, ">%3d%c", t, (char)1);
    } else {
        LiquidCrystal_I2C::print(F("xxx"));
        return;
    }
    LiquidCrystal_I2C::print(buff);
}

void DSPL::fanSpeed(uint8_t s) {
    char buff[6];
    s = map(s, 0, 255, 0, 99);
    sprintf(buff, " %c%2d%c", (char)2, s, '%');
    LiquidCrystal_I2C::setCursor(11, 1);
    LiquidCrystal_I2C::print(buff);
}

void DSPL::appliedPower(uint8_t p, bool show_zero) {
	char buff[6];
	if (p > 99) p = 99;
    LiquidCrystal_I2C::setCursor(5, 1);
    if (p == 0 && !show_zero) {
        LiquidCrystal_I2C::print(F("     "));
    } else {
	    sprintf(buff, " %c%2d%c", (char)3, p, '%');
        LiquidCrystal_I2C::print(buff);
    }
}

void DSPL::setupMode(byte mode) {
    LiquidCrystal_I2C::clear();
    LiquidCrystal_I2C::print(F("setup"));
    LiquidCrystal_I2C::setCursor(1,1);
    switch (mode) {
        case 0:                                                             // tip calibrate
            LiquidCrystal_I2C::print(F("calibrate"));
            break;
        case 1:                                                             // tune
            LiquidCrystal_I2C::print(F("tune"));
            break;
        case 2:                                                             // save
            LiquidCrystal_I2C::print(F("save"));
            break;
        case 3:                                                             // cancel
            LiquidCrystal_I2C::print(F("cancel"));
            break;
        case 4:                                                             // set defaults
            LiquidCrystal_I2C::print(F("reset config"));
            break;
        default:
            break;
    }
}

void DSPL::msgON(void) {
    LiquidCrystal_I2C::setCursor(10, 0);
    LiquidCrystal_I2C::print(F("    ON"));
}

void DSPL::msgOFF(void) {
    LiquidCrystal_I2C::setCursor(10, 0);
    LiquidCrystal_I2C::print(F("   OFF"));
}


void DSPL::msgReady(void) {
    LiquidCrystal_I2C::setCursor(10, 0);
    LiquidCrystal_I2C::print(F(" Ready"));
}

void DSPL::msgCold(void) {
    LiquidCrystal_I2C::setCursor(10, 0);
    LiquidCrystal_I2C::print(F("  Cold"));
}

void DSPL::msgFail(void) {
    LiquidCrystal_I2C::setCursor(0, 1);
    LiquidCrystal_I2C::print(F(" -== Failed ==- "));
}

void DSPL::msgTune(void) {
    LiquidCrystal_I2C::setCursor(0, 0);
    LiquidCrystal_I2C::print(F("Tune"));
}

//------------------------------------------ class HISTORY ----------------------------------------------------
#define H_LENGTH 16
class HISTORY {
	public:
		HISTORY(void)                               						{ len = 0; }
		void     init(void)                         						{ len = 0; }
		uint16_t last(void);
		uint16_t top(void)                          						{ return queue[0]; }
		void     put(uint16_t item);                						// Put new entry to the history
		uint16_t average(void);                     						// calculate the average value
        float    dispersion(void);                                          // calculate the math dispersion
	private:
		volatile uint16_t queue[H_LENGTH];
		volatile byte len;                          						// The number of elements in the queue
		volatile byte index;                        						// The current element position, use ring buffer
};

void HISTORY::put(uint16_t item) {
	if (len < H_LENGTH) {
		queue[len++] = item;
	} else {
		queue[index ] = item;
		if (++index >= H_LENGTH) index = 0;         						// Use ring buffer
	}
}

uint16_t HISTORY::last(void) {
    if (len == 0) return 0;
	uint8_t i = len - 1;
	if (index)
		i = index - 1;
	return queue[i];
}

uint16_t HISTORY::average(void) {
	uint32_t sum = 0;
    if (len == 0) return 0;
	if (len == 1) return queue[0];
	for (uint8_t i = 0; i < len; ++i) sum += queue[i];
	sum += len >> 1;                              							// round the average
	sum /= len;
	return uint16_t(sum);
}

float HISTORY::dispersion(void) {
    if (len < 3) return 1000;
    uint32_t sum = 0;
    uint32_t avg = average();
    for (uint8_t i = 0; i < len; ++i) {
        long q = queue[i];
        q -= avg;
        q *= q;
        sum += q;
    }
    sum += len << 1;
    float d = (float)sum / (float)len;
    return d;
}

//-------------------------------------------class Exponential average ----------------------------------------
class EMP_AVERAGE {
    public:
        EMP_AVERAGE(uint8_t h_length = 8)               { emp_k = h_length; emp_data = 0; }
        void            length(uint8_t h_length)        { emp_k = h_length; emp_data = 0; }
        void            reset(void)                     { emp_data = 0; }
        int32_t         average(int32_t value);
        void            update(int32_t value);
        int32_t         read(void);
    private:
        volatile    uint8_t     emp_k       = 8;
        volatile    uint32_t    emp_data    = 0;
};

int32_t EMP_AVERAGE::average(int32_t value) {
    uint8_t round_v = emp_k >> 1;
    update(value);
    return (emp_data + round_v) / emp_k;
}

void EMP_AVERAGE::update(int32_t value) {
    uint8_t round_v = emp_k >> 1;
    emp_data += value - (emp_data + round_v) / emp_k;
}

int32_t EMP_AVERAGE::read(void) {
    uint8_t round_v = emp_k >> 1;
    return (emp_data + round_v) / emp_k;
}

//------------------------------------------ class PID algoritm to keep the temperature -----------------------
/*  The PID algorithm 
 *  Un = Kp*(Xs - Xn) + Ki*summ{j=0; j<=n}(Xs - Xj) + Kd(Xn - Xn-1),
 *  Where Xs - is the setup temperature, Xn - the temperature on n-iteration step
 *  In this program the interactive formula is used:
 *    Un = Un-1 + Kp*(Xn-1 - Xn) + Ki*(Xs - Xn) + Kd*(Xn-2 + Xn - 2*Xn-1)
 *  With the first step:
 *  U0 = Kp*(Xs - X0) + Ki*(Xs - X0); Xn-1 = Xn;
 *  
 *  PID coefficients history:
 *  10/14/2017  [768,     32, 328]
 *  11/27/2019  [ 2009, 1600,  20]
 *  04/27/2020  [   50,   16,  50]
 */
class PID {
    public:
        PID(void) {
            Kp = 50;
            Ki = 16;
            Kd = 50;
        }
        void resetPID(int temp = -1);                                       // reset PID algorithm history parameters
        // Calculate the power to be applied
        long reqPower(int temp_set, int temp_curr);
        int  changePID(uint8_t p, int k);                                   // set or get (if parameter < 0) PID parameter
    private:
        void  debugPID(int t_set, int t_curr, long kp, long ki, long kd, long delta_p);
        int   temp_h0, temp_h1;                                             // previously measured temperature
        bool  pid_iterate;                                                  // Whether the iterative process is used
        long  i_summ;                                                       // Ki summary multiplied by denominator
        long  power;                                                        // The power iterative multiplied by denominator
        long  Kp, Ki, Kd;                                                   // The PID algorithm coefficients multiplied by denominator
        const byte denominator_p = 11;                                      // The common coefficient denominator power of 2 (11 means divide by 2048)
};

void PID::resetPID(int temp) {
    temp_h0 = 0;
    power  = 0;
    i_summ = 0;
    pid_iterate = false;
    if ((temp > 0) && (temp < 1000))
        temp_h1 = temp;
    else
        temp_h1 = 0;
}

int PID::changePID(uint8_t p, int k) {
    switch(p) {
        case 1:
            if (k >= 0) Kp = k;
            return Kp;
        case 2:
            if (k >= 0) Ki = k;
            return Ki;
        case 3:
            if (k >= 0) Kd = k;
            return Kd;
        default:
        break;
    }
    return 0;
}

long PID::reqPower(int temp_set, int temp_curr) {
    if (temp_h0 == 0) {
        // When the temperature is near the preset one, reset the PID and prepare iterative formula                        
        if ((temp_set - temp_curr) < 30) {
            if (!pid_iterate) {
                pid_iterate = true;
                power = 0;
                i_summ = 0;
            }
        }
        i_summ += temp_set - temp_curr;                                     // first, use the direct formula, not the iterate process
        power = Kp*(temp_set - temp_curr) + Ki*i_summ;
    // If the temperature is near, prepare the PID iteration process
    } else {
        long kp = Kp * (temp_h1 - temp_curr);
        long ki = Ki * (temp_set - temp_curr);
        long kd = Kd * (temp_h0 + temp_curr - 2*temp_h1);
        long delta_p = kp + ki + kd;
        power += delta_p;                                                   // power kept multiplied by denominator!
    }
    if (pid_iterate) temp_h0 = temp_h1;
    temp_h1 = temp_curr;
    long pwr = power + (1 << (denominator_p-1));                            // prepare the power to delete by denominator, round the result
    pwr >>= denominator_p;                                                  // delete by the denominator
    return pwr;
}

//--------------------- High frequency PWM signal calss on D9 pin ------------------------- ---------------
class FastPWM_D9 {
    public:
        FastPWM_D9()                                { }
        void init(void);
        void        duty(uint8_t d)                 { OCR1A = d; }
        uint8_t     fanSpeed(void)                  { return OCR1A; }
};

void FastPWM_D9::init(void) {
    pinMode(9, OUTPUT);
    digitalWrite(9, LOW);
    noInterrupts();
    TCNT1   = 0;
    TCCR1B  = _BV(WGM13);                           // set mode as phase and frequency correct pwm, stop the timer
    TCCR1A  = 0;
    ICR1    = 256;
    TCCR1B  = _BV(WGM13) | _BV(CS10);               // Top value = ICR1, prescale = 1
    TCCR1A |= _BV(COM1A1);                          // XOR D9 on OCR1A, detached from D10
    OCR1A   = 0;                                    // Switch-off the signal on pin 9;
    interrupts();
}

//--------------------- Hot air gun manager using total sine shape to power on the hardware ---------------
class HOTGUN : public PID {
    public:
        typedef enum { POWER_OFF, POWER_ON, POWER_FIXED, POWER_COOLING } PowerMode;
        HOTGUN(uint8_t HG_sen_pin, uint8_t HG_pwr_pin);
        void        init(void);
		bool		isOn(void)												{ return (mode == POWER_ON || mode == POWER_FIXED);             }
		void        setTemp(uint16_t temp)                                  { temp_set  = constrain(temp, 0, int_temp_max);                 }
		uint16_t	getTemp(void)											{ return temp_set;                                              }
		uint16_t	getCurrTemp(void)										{ return h_temp.last();                                         }
		uint16_t 	tempAverage(void)                  						{ return h_temp.average();                                      }
        uint8_t     powerAverage(void)                                      { return h_power.average();                                     }
		uint8_t     appliedPower(void)                						{ return actual_power;                                          }
		void		setFanSpeed(uint8_t f)									{ fan_speed = constrain(f, min_working_fan, max_fan_speed);   }
		uint8_t	    getFanSpeed(void)   									{ return fan_speed;                                             }
        uint16_t    tempDispersion(void)                                    { return h_temp.dispersion();                                   }
        bool        isCold(void)                                            { return h_temp.average() < temp_gun_cold;                      }
        bool        areExternalInterrupts(void)                             { return millis() - last_period < period * 15;                  }
        uint8_t     avgPowerPcnt(void);
        void        switchPower(bool On);
        void        fixPower(uint8_t Power);                                // Set the specified power to the the hot gun
		void     	keepTemp(void);
        uint8_t     getMaxFixedPower(void)                                  { return max_fix_power; }
        bool        syncCB(void);											// Return true at the end of the power period
    private:
        bool        isGunConnected(void)                                    { return true; }
        void        shutdown(void);
        uint16_t    emulateTemp(void);                                      // To debug the project, simulate the Hot Air Gun heating process
        FastPWM_D9  hg_fan;
		uint16_t	temp_set;												// The preset temperature of the hot air gun (internal units)
		uint8_t		fan_speed;
        uint8_t     sen_pin;
		uint8_t		gun_pin;
		HISTORY  	h_power;                           						// The history queue of power applied values
		HISTORY  	h_temp;                            						// The history queue of the temperature
        EMP_AVERAGE e_sensor;                                               // The exponential average of sensor data
		volatile    uint8_t     cnt;
        volatile    uint8_t     actual_power;
        volatile    bool        active;
        uint8_t     actual_fan  = 0;                                        // Power applied to the fan (can be turned off)
        uint8_t     fix_power   = 0;                                        // Fixed power value of the Hot Air Gun (or zero if off)
        PowerMode   mode        = POWER_OFF;
        bool        chill;                                                  // To chill the hot gun
        volatile    uint32_t    last_period;                                // The time in ms when the counter reset
        const       uint8_t     period 			= 100;
        const       uint16_t    int_temp_max    = 900;
        const       uint8_t     max_fix_power   = 70;
        const       uint8_t     max_power       = 99;
        const       uint16_t    min_fan_speed   = 30;
        const       uint16_t    max_fan_speed   = 255;
        const       uint16_t    max_cool_fan    = 220;
        const       uint16_t    temp_gun_cold   = 20;
        const       uint8_t     e_sensor_length = 40;                       // Exponential average length of sensor data 
};

HOTGUN::HOTGUN(uint8_t HG_sen_pin, uint8_t HG_pwr_pin) {
    sen_pin = HG_sen_pin;
	gun_pin	= HG_pwr_pin;
}

void HOTGUN::init(void) {
    cnt             = 0;
    fan_speed       = 0;
    actual_power    = 0;
    fix_power       = 0;
    active          = false;
    chill           = false;
    last_period     = 0;
    pinMode(sen_pin, INPUT);
    pinMode(gun_pin, OUTPUT);
	digitalWrite(gun_pin, LOW);
    hg_fan.init();
	h_temp.init();
    e_sensor.length(e_sensor_length);
    resetPID();
}

bool HOTGUN::syncCB(void) {
    if (++cnt >= period) {
        cnt = 0;
        last_period = millis();                                             // Save the current time to check the external interrupts
        if (!active && (actual_power > 0)) {
            digitalWrite(gun_pin, HIGH);
            active = true;
        }
    } else if (cnt >= actual_power) {
        if (active) {
            digitalWrite(gun_pin, LOW);
            active = false;
        }
    }
    if (!active) {
        e_sensor.update(analogRead(sen_pin));
    }
	return (cnt == 0);														// End of the Power period (period AC voltage shapes)
}

void HOTGUN::switchPower(bool On) {
    switch (mode) {
        case POWER_OFF:
            if (hg_fan.fanSpeed() == 0) {                                   // Not power supplied to the Fan
                if (On)                                                     // !FAN && On
                    mode = POWER_ON;
            } else {
                if (On) {
                    if (isGunConnected()) {                                 // FAN && On && connected
                        mode = POWER_ON;
                    } else {                                                // FAN && On && !connected
                        shutdown();
                    }
                } else {
                    if (isGunConnected()) {                                 // FAN && !On && connected
                        if (isCold()) {                                     // FAN && !On && connected && cold
                            shutdown();
                        } else {                                            // FAN && !On && connected && !cold
                            mode = POWER_COOLING;
                        }
                    }
                }
            }
            break;
        case POWER_ON:
            if (!On) {
                mode = POWER_COOLING;
            }
            break;
        case POWER_FIXED:
            if (hg_fan.fanSpeed()) {
                if (On) {                                                   // FAN && On
                    mode = POWER_ON;
                } else {                                                    // FAN && !On
                    if (isGunConnected()) {                                 // FAN && !On && connected
                        if (isCold()) {                                     // FAN && !On && connected && cold
                            shutdown();
                        } else {                                            // FAN && !On && connected && !cold
                            mode = POWER_COOLING;
                        }
                    }
                }
            } else {                                                        // !FAN
                if (!On) {                                                  // !FAN && !On
                    shutdown();
                }
            }
            break;
        case POWER_COOLING:
            if (hg_fan.fanSpeed()) {
                if (On) {                                                   // FAN && On
                    if (isGunConnected()) {                                 // FAN && On && connected
                        mode = POWER_ON;
                    } else {                                                // FAN && On && !connected
                        shutdown();
                    }
                } else {                                                    // FAN && !On
                    if (isGunConnected()) {
                        if (isCold()) {                                     // FAN && !On && connected && cold
                            shutdown();
                        }
                    } else {                                                // FAN && !On && !connected
                        shutdown();
                    }
                }
            } else {
                if (On) {                                                   // !FAN && On
                    mode = POWER_ON;
                }
            }
    }
    h_power.init();
}

// This routine is used to keep the hot air gun temperature near required value
void HOTGUN::keepTemp(void) {
    //uint16_t temp = analogRead(sen_pin);                                    // Check the hot air gun temperature
    //uint16_t temp   = emulateTemp();
    uint16_t temp = e_sensor.read();                                        // Average value of the hot air gun temperature
    h_temp.put(temp);
    
...

This file has been truncated, please download it to see its full contents.
Github
https://github.com/ManojBR105/ARDUINO-SMD-REWORK-STATION

Schematics

Etched Copper board
Etched pcb vft3axav3p
Finished circuit board
Assembled pcb top ltykj3napw
Project Assembly
Project assembly gu4q4zebip
Power supply schematic
This is the power supply circuit to provide necessary voltage for the controller.
Sch2 cgnv2i6fyb
Eagle schematics
hot_air_gunsch_l627KvauMg.sch

Comments

Similar projects you might like

DIY 3-Axis CNC VMC

Project tutorial by Amit Nandi (BigWiz)

  • 22,675 views
  • 11 comments
  • 81 respects

DIY Retro Look FM Radio with TEA5767 Module

Project tutorial by Mirko Pavleski

  • 12,089 views
  • 7 comments
  • 48 respects

DIY CD Spot Welder - Part 1

Project in progress by Westpol

  • 9,295 views
  • 33 comments
  • 29 respects

Portable Temperature Station V2: IOT Edition

Project tutorial by Isaac100

  • 3,646 views
  • 0 comments
  • 8 respects

DIY 7-Segment Display

Project tutorial by Pop Gheorghe

  • 2,393 views
  • 2 comments
  • 2 respects

Micro Soldering Station

Project tutorial by Paulo Bruckmann

  • 18,845 views
  • 17 comments
  • 52 respects
Add projectSign up / Login