Контролер паяльного фена, DIY

Почему бы не сделать себе станцию с паяльным феном, подумал я? И начал потихоньку собирать материал, читать про готовые устройства, собирать комплектующие. В итоге — получилось устройство, о котором я хочу разсказать.
Будет несколько фото, радиолюбительства, программирования ардуино и самодельщины, или DIY.
И еще: я понимаю, что купить готовое — намного дешевле, чем сделать самому.
Кого не пугает, кто любит делать руками и программировать микроконтроллеры АВР — добро пожаловать :)

Сам фен, или рукоятка, продавался в украинском интернет магазине, как запасной для китайской станции с труднозапоминаемым названием. Там же была куплена подставка со встроеным магнитом. Цена была очень приятная, похоже сбывали остатки. Сейчас его нет в наличии, но такое же есть на али, тут и тут, и еще подставка: раз и два.
В рукоятке находится нагреватель с датчиком температуры, вентилятор турбинка и геркон. Я только добавил кнопочку с резистором, но об этом — ниже. На али куплен такой набор насадок.
Контролер собирал сам. Он содержит ПИД регулятор температуры, позволяет работать с двумя уставками, регулировать обороты вентилятора. Температура или скорость вентилятора выводятся на цифровой дисплей. На подставке фен удерживается на 100 градусах. При снятии — за 2..3 секунды выходит на режим и формируется короткий звуковой сигнал. При нахождении на подставке более 10 минут — фен выключается, на 8-й и 9-й минуте формируются звуковые сигналы. Температура и скорость вентилятора задаются с помощью энкодера. Коэфициенты ПИД регулятора — в диалоговом окне монитора порта, при подключении к компьютеру.

Органы управления
На передней панели находятся тумблер питания и энкодер, которым осуществляется регулировка параметра. Короткое нажатие на энкодер переключает установки температуры или оборотов вентилятора. Удержание до 4 сек. — переключает режим установки первого или второго заданий температуры. При выключении тумблера осуществляется охлаждение фена и потом отключение устройства от сети.
При работе с феном, дисплей отображает поточную темературу. При нахождении на подставке — задание или поточную температуру, это выбирается удержанием кнопки энкодера более 4 сек.
Кнопка, смонтированая на самом фене, позволяет работать с другим заданием температуры, пока она нажата. Например, если нужно кратковременно «прожарить» более высокой температурой.
Ссылки на али: разъем на 9 пин, энкодер.

Индикация
Элементы индикации: трехрозрядный цифровой индикатор и четыре лампочки. Первая лампочка: на дисплее отображается температура. Вторая — отображается второе задание. третья (зеленая) — отображается задание температуры, четвертая (синяя) — отображается производительность вентилятора.
Ссылки на али: неоновые лампочки, зеленые и синие.

Корпус
Корпус Z2P куплен в украинском интернет магазине. Рисунок передней панели сделан в Corel draw. Там же нарисован эскиз для вырезания отверстий. Порезка осуществлялась лазером на ЧПУ. Детальнее описано в конце этого обзора.
Передняя панель распечатана на обычной бумаге. После вырезания отверстий — прогнана через ламинатор и прихвачена двусторонним скотчем к пластику.
Фото передней панели


Комплектующие, сборка
Донором индикаторов послужила часть неизвестного творения инженеров СССР.

Устройство было куплено на блошином рынке, несколько лет назад. Один индикатор уже был разбит, но за него просили суму, эквивалентную стоимости бутылки водки. У меня лично электронная не бытовая промышленость СССР вызывает восхищение.
С помощью двигателя шпинделя от ЧПУ с фрезой, я акуратненько вырезал три индикатора с дешифраторами.
Еще фото устройства индикации

Радиодетали и комплектующие приобретались на али и в украинских интернет магазинах. Большинство — было найдено в закромах.
Ссылки на али: разъемы питания и нагревателя 2 пин, сигнальные разъемы XH и Dupont
В качестве мозгов — выбрана Arduino pro. Она не дорогая и справляется со всеми функциями. Приобретена в Украине, вот здесь.
Печатная плата нарисована в Sprint layout, вытравлена в перекиси с лимонной кислотой. Сверление и фрезеровка осуществлены с помощью ЧПУ станка.
Печатная плата

Источник питания 24 вольт — выдран с какого-то адаптера.
Собраная плата

Еще фото платы в сборе

Платы в корпусе, пробный запуск

Собранное устройство

Еще фото



Схема
Принципиальная схема устройства

Устройство индикации


Программа
Текст управляющей программы

// Hot air gun temperature controller
// Arduino pro mini
// 5V; 16 MHz; 328P

// Settings
#define FAN_MINIMAL 20
#define SV_MINIMAL 100
#define SV_MAXIMAL 450

#include <EEPROM.h>

// Display
#define HV 4
#define DATA 5
#define LATCH 11
#define CLK 18
byte regL, regH;

// Control
#define ENC_CLK 3
#define DIRECT 7
#define BUTTON 8
#define POW_SW 9
int bt_timer;
bool bt_tap, bt_hold, bt_hold_long;
byte SV_show_timer;
volatile bool power_control;
bool instrument_connected;
bool device_hot;
unsigned int sleep_timer;
bool sleeping;
byte eeprom_timer;
bool eeprom_store;
bool show_SV_on_iddle;

// Inputs
#define INSTRUMENT 13
#define TEMP_PV 0
#define USE 1
int PV, PV_buf [8];
byte PV_buf_index;
bool reg_action;
int disp_PV, disp_PV_buf[5];
byte disp_PV_buf_index;
volatile int SV, SV2;
int task;
bool SV2_mode;
float error, deltaPV, prevPV, integr;
byte use_value;
byte use_state_temp, use_state_temp_old, use_state_stable_count;
volatile byte use_state;
  // 0: instrument on holder
  // 1: instrument in use, button pressed
  // 2: instrument in use, button released
volatile bool stabilized;
byte stabile_timer;
// Outputs
#define HEAT 10
#define FAN 6
#define RELAY 12
#define SUPPLY 17
#define BEEP 19
volatile bool triac_control;
volatile int power;
float task_power;
float kp, ki, kd;
byte fan_speed;
volatile byte set_fan_speed;
bool relay_state, relay_state_old;
volatile bool let_work;
byte start_timer;
byte beep_timer;

byte display_mode;
// 0: PV
// 1: SV
// 2: Fan
//enum {PV,SV,FAN} display_mode; //https://alexgyver.ru/lessons/variables-types/

// Serial dialog
String inData; 
char inChar[15];
float float_var;
byte inSymbol;


void setup() {
  Serial.begin(9600);
  Serial.println("Hello");
  print_help();
  
  pinMode(HV,OUTPUT);
  pinMode(DATA,OUTPUT);
  pinMode(LATCH,OUTPUT);
  pinMode(CLK,OUTPUT);
  pinMode(ENC_CLK,INPUT_PULLUP);
  pinMode(DIRECT,INPUT);
  pinMode(BUTTON,INPUT);
  pinMode(POW_SW,INPUT);
  pinMode(INSTRUMENT,INPUT);
  pinMode(HEAT,OUTPUT);
  pinMode(FAN,OUTPUT);
  pinMode(RELAY,OUTPUT);
  pinMode(SUPPLY,OUTPUT);
  pinMode(BEEP,OUTPUT);

  TCCR1A = 0;
  //TCCR1B = 1<<WGM12 | 1<<CS11 | 1<<CS10; // enable CTC, prescale 64 
  TIMSK1 = 1<<OCIE1A; // enable compare interrupt
  //OCR1A = 1250; 
  // 250: 1mS for max power
  // 2250: 9mS for min power
  // 25: 0.1ms for open triac
  
  TCCR2A = 0; //TMR2 normal mode
  TCCR2B = 1<<CS22 | 1<<CS21 | 1<<CS20; // prescale 1024, overflow each 1/(16000000/1024/256=61Hz) = 16.4mS
  TIMSK2 = 1<<TOIE2; // enable overflow interrupt
  TCNT2 = 0; // reset counter

  EICRA = 1<<ISC11 | 1<<ISC01; // Falling edge generate interrupt
  EIMSK = 1<<INT1 | 1<<INT0; // Enable interrupt


  SV = 0; // = 300;
  power = 0;
  start_timer = 0;
  sleep_timer = 0;

  TCCR0B = 1; // default is 3 (prescaler 64)

  eeprom_timer = 255;

  analogReference(INTERNAL);

  GetSV();
  GetPIDkoef();
  print_PID();
}

void loop() {
  power_control = !(digitalRead(POW_SW));
  instrument_connected = (digitalRead(INSTRUMENT));

  digitalWrite(HV,(!(sleeping)));

  if (PV>80) device_hot = 1; if (PV<50) device_hot = 0; 
  
  if (power_control) digitalWrite(SUPPLY,1);
    else digitalWrite(SUPPLY,(device_hot && instrument_connected));
  
  if (use_state > 0) sleep_timer = 0;
  
  if (use_state > 0) 
    {
      if (SV2_mode) task = SV2; else task = SV;
    }
  else {if (sleeping) task = 0; else task = 100;} 

  if (use_state > 0) {SV2_mode = (use_state == 1);}
  
  relay_state = (PV < 480)&&(power_control)&&(!(sleeping));
  if ( relay_state_old != relay_state )
    { 
      let_work = 0; //Serial.print("Let work: "); Serial.println(let_work);
      delay(3200); //500*64
      relay_state_old = relay_state;
      digitalWrite(RELAY,relay_state);  //Serial.print("Relay: "); Serial.println(relay_state);
      delay(3200); //500*64
      let_work = 1; //Serial.print("Let work: "); Serial.println(let_work);
    }
  
  if (reg_action)
    {
      reg_action = 0;
      PV = 0;
      for (byte i = 0; i < 4; i++)
        {
          PV = PV + PV_buf[i];
        }
      PV = PV >> 2;
      if (instrument_connected) PV = map(PV,0,500,25,500); else PV = 0;
      error = task - PV;
      deltaPV = prevPV - PV;
      prevPV = PV;
      if ( (task_power>0) && (task_power<1000) ) integr = integr + (ki *  error);
      task_power = ((kp * error) + integr + (kd * deltaPV));
      power = constrain(task_power, 0, 1000);
      //Serial.print("Power:  "); Serial.print(task_power); Serial.print(", "); Serial.println(power);
      disp_PV_buf[disp_PV_buf_index] = PV;
      disp_PV_buf_index++;
      if (disp_PV_buf_index>4)
        {
          disp_PV_buf_index = 0;
          disp_PV = 0;
          for (byte i = 0; i < 4; i++)
            {
              disp_PV = disp_PV + disp_PV_buf[i];
            }
          disp_PV = disp_PV >> 2; //Serial.println(use_state);
        }
    stabilized = ((abs(PV - task)) < 5) && (use_state > 0);
    }
    
  if (power_control) 
      {
        if (sleeping)
            {
              if (device_hot) fan_speed = 30; else fan_speed = 0;
            }
        else 
            {
              if (use_state > 0) fan_speed = set_fan_speed; else fan_speed = 20;
            }
      }
  else fan_speed = 50;
  if (fan_speed < 80) analogWrite(FAN, map(fan_speed,0,80,0,100)); else analogWrite(FAN, map(fan_speed,80,100,100,255));

  if (eeprom_store)
    {
      eeprom_store = 0;
      //beep_timer = 12; // 200 mS tone
      StoreSV();
    }
  
  switch (display_mode)
    {
      case 0: DisplayValue(disp_PV); break;
      case 1: if (SV2_mode) DisplayValue(SV2); else DisplayValue(SV); break;
      case 2: DisplayValue(set_fan_speed); break;
    }

  if (bt_tap)
  {
    bt_tap = 0;
    sleep_timer = 0;
    if ( (power_control)&&(instrument_connected) ) {if (display_mode == 2) display_mode = 0; else (display_mode = 2);}
  }
  if (bt_hold)
  {
    bt_hold = 0;
    sleep_timer = 0;
    if ( (power_control)&&(instrument_connected) ) {if (use_state == 0) SV2_mode = !(SV2_mode);}
  }
  if (bt_hold_long)
  {
    bt_hold_long = 0;
    if (use_state == 0) show_SV_on_iddle = !(show_SV_on_iddle);
  }
  
  SetLamp(0,( ((display_mode == 0)||(display_mode == 1))&& instrument_connected ) ); 
  SetLamp(1,SV2_mode); 
  SetLamp(2,((display_mode == 1))); 
  SetLamp(3,(display_mode == 2)); 

  if(Serial.available() >0)
    {
    inData = "";
    while(Serial.available() > 0)
      {
      delay(1280); //20*64
      inSymbol = Serial.read();
      inData += char(inSymbol);
      }  

    inData.toCharArray(inChar, inData.length() +1);
    float_var = atof(inChar);  
    
    //Serial.println(inData);

    if (inData.indexOf("help") > -1)
      { 
        print_help();
      }
    else if (inData.indexOf(" sv1") > -1)
      { 
      SV = int(float_var);
      Serial.print("New SV is ");
      Serial.println(SV);
      eeprom_timer = 0;
      }
    else if (inData.indexOf(" sv2") > -1)
      { 
      SV2 = int(float_var);
      Serial.print("New SV2 is ");
      Serial.println(SV2);
      eeprom_timer = 0;
      }
    else if (inData.indexOf(" fan") > -1)
      { 
      set_fan_speed = byte(float_var);
      Serial.print("New FAN value is ");
      Serial.println(set_fan_speed);
      eeprom_timer = 0;
      }
    else if (inData.indexOf("list") > -1)
      { 
        print_PID();
      }
    else if (inData.indexOf("store") > -1)
      { 
        StorePIDkoef();
        Serial.println("Stored");
      }
    else if (inData.indexOf(" p") > -1)
      { 
      kp = float_var;
      Serial.print("kp: ");
      Serial.println(kp);
      }
    else if (inData.indexOf(" i") > -1)
      { 
      ki = float_var;
      Serial.print("ki: ");
      Serial.println(ki);
      }
    else if (inData.indexOf(" d") > -1)
      { 
      kd = float_var;
      Serial.print("kd: ");
      Serial.println(kd);
      }
    else print_help();

    }
  
}

ISR(TIMER2_OVF_vect) //timer 2 interrupt
{
  // temperatute measuring
  PV_buf[PV_buf_index] = analogRead(TEMP_PV)>>1;
      //Serial.print(PV_buf[PV_buf_index]); Serial.print("   "); Serial.println(PV_buf_index);
  PV_buf_index ++;
  if (PV_buf_index > 4) {PV_buf_index = 0; reg_action = 1;}
  // use state recognize
  use_value = analogRead(USE)>>2; // 0, 142, 255
  if (use_value < 70) use_state_temp = 0;
    else if (use_value < 190) use_state_temp = 1;
    else use_state_temp = 2;
  if ( use_state_temp_old == use_state_temp ) {if (use_state_stable_count < 255) use_state_stable_count++;}
    else {use_state_stable_count = 0; use_state_temp_old = use_state_temp;}
  if ( use_state_stable_count == 20 ) use_state = use_state_temp;
  // button manage
  if (digitalRead(BUTTON)) if ( (bt_timer>6) && (bt_timer<62) ) {bt_tap = 1; bt_hold = 0; bt_hold_long = 0;}// >100mS, <1000mS
  if (bt_timer==62) {bt_tap = 0; bt_hold = 1; bt_hold_long = 0;}// = 1000mS  
  if (bt_timer==244) {bt_tap = 0; bt_hold = 1; bt_hold_long = 1;}// = 4000mS  
  if (!(digitalRead(BUTTON))) {if (bt_timer<32768) bt_timer++;} else bt_timer = 0;
  // stabilization beep
  if (stabilized) {if (stabile_timer < 255) stabile_timer ++;}
    else stabile_timer = 0;
  if (stabile_timer == 182) beep_timer = 12; // after 3000 ms, 200 mS tone
  // SV show
  if (power_control)
    {
    if ( (use_state == 0) && (show_SV_on_iddle) ) { if (display_mode == 0) display_mode = 1;} else 
        { if (SV_show_timer < 122) {SV_show_timer++;} else {if (display_mode ==1) display_mode = 0;} }
    }
  else display_mode = 0;
  // eeprom store timer
  if (eeprom_timer == 250) eeprom_store = 1; // 4.1 sec
  if (eeprom_timer < 255) eeprom_timer++;
  // initial start delay
  if (start_timer < 255) start_timer++; 
  if (start_timer == 122) let_work = 1;
  // sleep
  if (sleep_timer < 65500) sleep_timer++;
  if (sleep_timer == 29268) beep_timer = 122; // 2000mS // about 480s, 8min
  if (sleep_timer == 32927) beep_timer = 244; // 4000mS // about 540s, 9min
  sleeping = (sleep_timer > 36585); // about 600s, 10min
  // beep
  if (beep_timer > 0) beep_timer--;
  digitalWrite(BEEP,(beep_timer > 0));
}

ISR(TIMER1_COMPA_vect)
{
  if (triac_control)
    {
      TCCR1B = 0;
      digitalWrite(HEAT,LOW);
      triac_control = 0;
    }
  else
    {
      TCNT1 = 0; //TCCR1B = 0;
      digitalWrite(HEAT,HIGH); //delayMicroseconds(100); digitalWrite(HEAT,LOW);
      OCR1A = 25;
      triac_control = 1; 
    }
}

ISR(INT0_vect) //INT0 interrupt, Zero cross
 {
  triac_control = 0;
  //OCR1A = 1250; 
  // 250: 1mS for max power
  // 2250: 9mS for min power
  // 25: 0.1ms for open triac
  TCNT1 = 0;
  if (power > 0)
    {
      OCR1A = map(power,1,1000,2250,250);
      TCCR1B = 1<<WGM12 | 1<<CS11 | 1<<CS10; // enable CTC, prescale 64
    }
  else digitalWrite(HEAT,LOW);
 }

ISR(INT1_vect) //INT1 interrupt, encoder
 {
    if ( ((display_mode == 0) || (display_mode == 1)) && let_work && power_control && instrument_connected)
      {
       if (SV2_mode)
        {
         if (digitalRead(DIRECT)) {if (SV2 < SV_MAXIMAL) SV2 = SV2 + 5;}
                             else {if (SV2 > SV_MINIMAL) SV2 = SV2 - 5;}
        }
       else
        {
         if (digitalRead(DIRECT)) {if (SV < SV_MAXIMAL) SV = SV + 5;}
                             else {if (SV > SV_MINIMAL) SV = SV - 5;}
        }
       eeprom_timer = 0; 
       SV_show_timer = 0;
       display_mode = 1;
       Serial.print("SV: "); Serial.print(SV); Serial.print(";   SV2: "); Serial.println(SV2);
      }
    if (display_mode == 2)
      {
       if (digitalRead(DIRECT)) {if (set_fan_speed < 100) set_fan_speed = set_fan_speed + 2;}
                           else {if (set_fan_speed > FAN_MINIMAL) set_fan_speed = set_fan_speed - 2;}
       eeprom_timer = 0;                            
       Serial.print("Fan: "); Serial.println(set_fan_speed);
      }
    sleep_timer = 0;
    //beep_timer = 31; // 500mS
//digitalWrite(BEEP, HIGH); delay(5000); digitalWrite(BEEP, LOW); 
 }

void StoreSV()
{
  EEPROM_int_write(0,SV); // 0,1
  EEPROM_int_write(2,SV2); // 2,3
  EEPROM.update(4,set_fan_speed); // 4
  EEPROM.update(5,show_SV_on_iddle); // 5  
}

void GetSV()
{
  SV = EEPROM_int_read(0); // 0,1
  if (SV > SV_MAXIMAL) SV = 0;
  SV2 = EEPROM_int_read(2); // 2,3
  if (SV2 > SV_MAXIMAL) SV2 = 0;
  set_fan_speed = EEPROM.read(4); // 4
  show_SV_on_iddle = EEPROM.read(5); // 5
}

void GetPIDkoef()
{
  kp = EEPROM_float_read(5); // 5,6,7,8
  ki = EEPROM_float_read(9); // 9,10,11,12
  kd = EEPROM_float_read(13); // 13,14,15,16
}

void StorePIDkoef()
{
  EEPROM_float_write(5,kp); // 5,6,7,8
  EEPROM_float_write(9,ki); // 9,10,11,12
  EEPROM_float_write(13,kd); // 13,14,15,16
}

void print_PID()
{
  Serial.print("p: ");
  Serial.print(kp);
  Serial.print(";   i: ");
  Serial.print(ki);
  Serial.print(";   d: ");
  Serial.print(kd);
  Serial.println(";");
}

int EEPROM_int_read(int addr) {    
  byte raw[2];
  for(byte i = 0; i < 2; i++) raw[i] = EEPROM.read(addr+i);
  int &num = (int&)raw;
  return num;
}

void EEPROM_int_write(int addr, int num) {
  byte raw[2];
  (int&)raw = num;
  for(byte i = 0; i < 2; i++) EEPROM.update(addr+i, raw[i]);
}

float EEPROM_float_read(int addr) {    
  byte raw[4];
  for(byte i = 0; i < 4; i++) raw[i] = EEPROM.read(addr+i);
  float &num = (float&)raw;
  return num;
}

void EEPROM_float_write(int addr, float num) {
  byte raw[4];
  (float&)raw = num;
  for(byte i = 0; i < 4; i++) EEPROM.update(addr+i, raw[i]);
}

void DisplayValue(int data)
{
  byte dig1,dig2,dig3;
  if (data < 100) dig3 = 0x0F; else dig3 = (data/100)%10; //Serial.print(dig3); Serial.print(" "); 
  if (data < 10) dig2 = 0x0F; else dig2 = (data/10)%10; //Serial.print(dig2); Serial.print(" ");
  dig1 = (data%10); //Serial.print(dig1); Serial.print(" ");
  regH = regH & 0xF0;
  regH = regH + dig3; //Serial.print(regH,BIN); Serial.print(" ");
  regL = dig2; 
  regL = regL << 4;
  regL = regL + dig1; //Serial.print(regL,BIN); Serial.print(" ");
  Update595();
}

void SetLamp(byte lamp, bool mode)
{
  //Serial.print(lamp); Serial.print(" ");
  switch (lamp)
    {
      case 0: if (mode) regH = regH | 0b00010000; else regH = regH & 0b11101111; break; 
      case 1: if (mode) regH = regH | 0b00100000; else regH = regH & 0b11011111; break; 
      case 2: if (mode) regH = regH | 0b01000000; else regH = regH & 0b10111111; break; 
      case 3: if (mode) regH = regH | 0b10000000; else regH = regH & 0b01111111; break; 
    }
  //Serial.print(regH,BIN); Serial.print(" "); Serial.print(regL,BIN); Serial.println(" ");
  Update595();
}

void Update595()
{
  byte i, regH_, regL_;
  regH_ = regH;
  regL_ = regL;
  digitalWrite(LATCH,LOW);
  digitalWrite(CLK,LOW);
  for (i = 0; i < 8; i++)
  {
    if (regH_ & 0x80) 
      {
        digitalWrite(DATA,HIGH);
        //Serial.print("Y ");
      }
    else
      {
         digitalWrite(DATA,LOW);
         //Serial.print("N ");
      }
    digitalWrite(CLK,HIGH);
    digitalWrite(CLK,LOW);
    regH_ = regH_ << 1;
  }
  for (i = 0; i < 8; i++)
  {
    if (regL_ & 0x80) 
      {
        digitalWrite(DATA,HIGH);
        //Serial.print("Y ");
      }
    else
      {
         digitalWrite(DATA,LOW);
         //Serial.print("N ");
      }
    digitalWrite(CLK,HIGH);
    digitalWrite(CLK,LOW);
    regL_ = regL_ << 1;
  }
  digitalWrite(LATCH,HIGH);
  digitalWrite(LATCH,LOW);
}

void print_help()
{
  Serial.println("***********************************");
  Serial.println("Commands");
  Serial.println("help: shows help");
  Serial.println("XX sv1: set new SV");
  Serial.println("XX sv2: set new SV2");
  Serial.println("XX fan: set new fan speed");
  Serial.println("XX.XX p: set new P value");
  Serial.println("XX.XX i: set new I value");
  Serial.println("XX.XX d: set new D value");
  Serial.println("list: show PID parameters");
  Serial.println("store: save PID parameters to FLASH");
  Serial.println("***********************************");
}



Регулировка температуры осуществляется по ПИД алгоритму. Подробнее о нем можно почитать здесь и здесь
Я делал по принципу библиотеки от AlexGyver.
Настройка коефициентов регулятора осуществляется вро=учную через монитор порта Arduino. Список доступных команд выводится командой help.
Регулировка мощности нагревателя осуществляется фазоимпульсным методом. Отслеживается переход синусоиды через ноль и формировка задержки подачи имульса открывания на симистор.
Осциллограмма сигнала на входе прерывания 0

Формированием задержки занимается таймер1

  TCCR1A = 0;
  //TCCR1B = 1<<WGM12 | 1<<CS11 | 1<<CS10; // enable CTC, prescale 64 
  TIMSK1 = 1<<OCIE1A; // enable compare interrupt
  //OCR1A = 1250; 
  // 250: 1mS for max power
  // 2250: 9mS for min power
  // 25: 0.1ms for open triac


На таймере2 реализовано перрывание каждые 16,4 миллисекунд

  TCCR2A = 0; //TMR2 normal mode
  TCCR2B = 1<<CS22 | 1<<CS21 | 1<<CS20; // prescale 1024, overflow each 1/(16000000/1024/256=61Hz) = 16.4mS
  TIMSK2 = 1<<TOIE2; // enable overflow interrupt
  TCNT2 = 0; // reset counter


Кроме того, задействованы аппаратные прерывания от энкодера и детектора нуля

  EICRA = 1<<ISC11 | 1<<ISC01; // Falling edge generate interrupt
  EIMSK = 1<<INT1 | 1<<INT0; // Enable interrupt


ШИМ вентилятора реализован на таймере0. По умолчанию, он тактируется через делитель на 64 и формирует частоту 976 Гц.
Вентилятор при этом сильно шумит и греется транзистор VT5. Избавится от дросселя L2 и других элекентов не желательно. В своем блоке питания я сталкивался с проблемой, когда вентилятор не хотел запускаться при питании ШИМом. Еще провода питания вентилятора проходят рядом с проводами от термопары, и лишние помехи тут ни к чему.
Я отключил этот предделитель, чтобы избавится от писка вентилятора, теперь частота ШИМ составляет 62.5 кГц

  TCCR0B = 1; // default is 3 (prescaler 64)

Но так как delay работает от этого таймера, необходимо не забывать умножать желаемое время в delay на 64.

Для аналоговых входов задействован внутренний опорный источник 1,1 В

  analogReference(INTERNAL);


Одну проблему я так и не поборол: при срабатывании реле вызывалось прерывание от энкодера. Сильно помог конденсатор на 470пФ непосредственно на выводах энкодера. Экранировка проводов к энкодеру — тоже ничего не дала. Пришлось сделать костыль в виде переменной let_work, которая выключает функции в обработчике прерываний энкодера.

  if ( relay_state_old != relay_state )
    { 
      let_work = 0; //Serial.print("Let work: "); Serial.println(let_work);
      delay(3200); //500*64
      relay_state_old = relay_state;
      digitalWrite(RELAY,relay_state);  //Serial.print("Relay: "); Serial.println(relay_state);
      delay(3200); //500*64
      let_work = 1; //Serial.print("Let work: "); Serial.println(let_work);
    }
  


В прерывании таймера 2 осуществляется замер температуры и внесение даных в буфер. После взятия 4-х измерений — выставляется флаг reg_action. А уже в основном цикле — выщитывается температура и управляющее значение для регулятора мощности.
Один раз на 4 срабатывания флага — обновляется отображаемое значение температуры.

  if (reg_action)
    {
      reg_action = 0;
      PV = 0;
      for (byte i = 0; i < 4; i++)
        {
          PV = PV + PV_buf[i];
        }
      PV = PV >> 2;
      if (instrument_connected) PV = map(PV,0,500,25,500); else PV = 0;
      error = task - PV;
      deltaPV = prevPV - PV;
      prevPV = PV;
      if ( (task_power>0) && (task_power<1000) ) integr = integr + (ki *  error);
      task_power = ((kp * error) + integr + (kd * deltaPV));
      power = constrain(task_power, 0, 1000);
      //Serial.print("Power:  "); Serial.print(task_power); Serial.print(", "); Serial.println(power);
      disp_PV_buf[disp_PV_buf_index] = PV;
      disp_PV_buf_index++;
      if (disp_PV_buf_index>4)
        {
          disp_PV_buf_index = 0;
          disp_PV = 0;
          for (byte i = 0; i < 4; i++)
            {
              disp_PV = disp_PV + disp_PV_buf[i];
            }
          disp_PV = disp_PV >> 2; //Serial.println(use_state);
        }
    stabilized = ((abs(PV - task)) < 5) && (use_state > 0);
    }


Переменная display_mode задает, что будет отображаться на дисплее в даный момент

  switch (display_mode)
    {
      case 0: DisplayValue(disp_PV); break;
      case 1: if (SV2_mode) DisplayValue(SV2); else DisplayValue(SV); break;
      case 2: DisplayValue(set_fan_speed); break;
    }

0 — поточное значение температуры
1 — задание температуры.
2 — задание производительности вентилятора.

Переменная use_state принимает три значения:
0 — инструмент находится на подставке.
1 — инструмент снят с подставки, нажата кнопка.
2 — инструмент снят с подставки, кнопка не нажата.
В прерывании от таймера 2 считывается значение аналогово входа 1, защита от дребезга сигнала и установка значения use_state

  // use state recognize
  use_value = analogRead(USE)>>2; // 0, 142, 255
  if (use_value < 70) use_state_temp = 0;
    else if (use_value < 190) use_state_temp = 1;
    else use_state_temp = 2;
  if ( use_state_temp_old == use_state_temp ) {if (use_state_stable_count < 255) use_state_stable_count++;}
    else {use_state_stable_count = 0; use_state_temp_old = use_state_temp;}
  if ( use_state_stable_count == 20 ) use_state = use_state_temp;


В этом же прерывании обрабатывается нажатие кнопки энкодера, формируются 3 логических переменных: bt_tap, bt_hold, bt_hold_long
// button manage
  if (digitalRead(BUTTON)) if ( (bt_timer>6) && (bt_timer<62) ) {bt_tap = 1; bt_hold = 0; bt_hold_long = 0;}// >100mS, <1000mS
  if (bt_timer==62) {bt_tap = 0; bt_hold = 1; bt_hold_long = 0;}// = 1000mS  
  if (bt_timer==244) {bt_tap = 0; bt_hold = 1; bt_hold_long = 1;}// = 4000mS  
  if (!(digitalRead(BUTTON))) {if (bt_timer<32768) bt_timer++;} else bt_timer = 0;


В прерывании сравнения таймера 1 (TIMER1_COMPA_vect) осуществляется формирование импульса управления симистором. Формируется задержка от 1 мС до 9 мС на включение и длительность управляющего импульса 0.1 мС

В прерывании от детектора нуля (INT0_vect) выщитывается необходимая задержка, в зависимости от значения power

В прерывании от энкодера (INT1_vect) определяется направление вращения и изменяется соответствующий параметр.

Функция DisplayValue(int data) формирует 2 байта, которые будут отправлены в сдвиговые регистры. Число разбивается на сотни, десятки, единицы. Для гашения разряда, на все 4 входа К155ИД1 подается единица (число F).

Функция SetLamp(byte lamp, bool mode) отвечает за включение и выключение лампочек путем изменения битов 7..4 в старшем байте, отправляемом на индикацию.

Функция Update595() отправляет 2 байта в регистры сдвига, для индикации.
Добавить в избранное +93 +144
+
avatar
0
Устройство было куплено на блошином рынке, несколько лет назад. Один индикатор уже был разбит, но за него просили суму, эквивалентную стоимости бутылки водки.
Дорого. Или бутылка имелась в виду 0,2 л?
В качестве мозгов — выбрана Arduino pro.
Странно, а я считал, что Arduino Pro идёт на контроллере ATMega32U4, а не ATMega328.
+
avatar
+3
Есть Pro Micro и Pro Mini, там разные контроллеры.
+
avatar
+2
бутылка 0,5 это от силы 2 бакса. И то, думаю что бывает дешевле
+
avatar
+2
0.5=89грн= 3.3$ у нас вся в одну цену. Нету смысла бодяжить, плохую просто перестали покупать и она пропала как вид.
А тут ещё и с дешифраиорами.
+
avatar
+2
Все таки 0,5
Дорого/нет — все относительно.
у меня дорога на работу — с работы в день обходится дороже
+
avatar
+1
Сейчас всякие ретро индикаторы (газоразрядные, люминесцентные, даже накальные) стоят невменяемых денег. Видимо это связано с многочисленными описаниями часов под старину.
+
avatar
  • Paolo
  • 08 июня 2020, 22:00
0
За мольберт расскажите. И ссылку бы.
+
avatar
0
Недопонял
+
avatar
  • Paolo
  • 09 июня 2020, 09:08
-1
На последнем фото (перед схемой) на заднем плане виден мольберт на телескопических ножках. Вот, про него и интересуюсь. Если есть возможность, дайте ссылку и краткий отзыв.
+
avatar
+1
Это просто презентационная комната на работе. Сабж случайно попал в кадр. Как выйдут рекламщики из карантина, поспрашиваю у них, это их парафия
+
avatar
+4
Мощно!
+
avatar
  • Totka
  • 08 июня 2020, 22:12
+8
Теплый и ламповый фен :).

Но тратить индикаторы на фен такое себе, плюс если по герберам заказывать ПП, то она довольно большого размера выходит. На стм32 намного дешевле собрать по этому проекту и прошивка доступна с исходниками. Притом прошивка есть и для паяльника (старой версии v1.4 с таобао что ли + форк на современную стм32, которая у всех уже), так что поменять шрифт или еще что-то будет несложно. Плюс комплектуха и зачастую флешка-прошивальщик с ф101-103 тоже имеется в наличии.

А индикаторы отлично подойдут к часам или ЦАПу, а не к фену, который где-то подальше стоит.
+
avatar
+7
Это ещё Kirich не видел, на что индикаторы были спущены ))
+
avatar
+2
Индикаторов было только три, на часы бы не хватило :)
Ардуину на атмеге знает «каждый студент политеха». Да и документации на русском — тоже полно, например, тот же Евстифеев. В STM чуть выше порог вхождения
+
avatar
  • usb350
  • 08 июня 2020, 23:09
+1
Отличная конструкция. Припоминаю похожие цифры на советских весах в магазине.
Но сам фен того не заслуживает, ну лично моё мнение. Трехкнопочного «контроллера» 8858 ему за глаза.

К слову, китайским жалам Т12 по $2.7 STM32 тоже на фиг не сдался.
STC вполне достаточно.
+
avatar
+1
китайским жалам Т12 за 240 рублей STM32 тоже на фиг не сдался. STC за глаза.
тут уже дело о фломастерах ), у меня есть оба, но с STM-кой мне больше нравится.
Трехкнопочного «контроллера» 8858 ему за глаза.
сам по себе 8858 нормальный фен, недостаток в трехкнопочной системе управления. Не оперативная регулировка потока воздуха. Я свой переделывал, температура кнопками, а поток потенциометром.
+
avatar
+2
китайским жалам Т12 по $2.7 STM32 тоже на фиг не сдался
Как раз наоборот — в контроллере на СТМ есть калибровка по трем точкам, китайские жала без нее сильно другую температуру имеют.
+
avatar
  • kiv69
  • 15 июня 2020, 17:01
+1
Учитывая стоимость голубой таблетки, контроллер на STM32 получается дешевле, чем на STC.
Как покупатель готового на STC и «собиратель» на STM32, считаю лучшим STM32
+
avatar
+11
Не совсем понятно, зачем десять минут держать фен при 100С. Ну и второе. Я как бы гораздо дольше имел дело с ламповыми элементами, но ностальгией по ним абсолютно не болею, но это личное. Не совсем понимаю, зачем было скрещивать ужа с ежом, то есть современные технологии с такой индикацией.
О, успели поставить минус, приверженцы теплых ламповых технологий. Ну не люблю лампы, хоть и вырос на них.
+
avatar
  • mrac
  • 09 июня 2020, 00:55
+4
Когда учился в университете тоже жутко не любил на лабораторных работах использовать частотомер с такими лампами и старые осциллографы. Мечтал о «современных» ЖК-экранах. Как-то мне современная индикация больше по душе. А вот друзья, лет на 10 младше меня, они восторгаются этими лампами.
+
avatar
0
«Во первых, это красиво» — из анекдота про евреев и обрезание.
100 градусов — дежурный режим, что бы быстрее выходить на заданную температуру.
10 минут, если забыть выключить тумблер.
+
avatar
0
Ну у меня фен 8858, на 350 градусов выходит за 4 секунды.
+
avatar
  • scorry
  • 15 июня 2020, 13:20
+2
Тут дело даже не в лампах, а в параллаксе наблюдаемых цифр. Любой матричный индикатор работает в одной плоскости; ИН-ки всегда бесили разрывами в цифрах и прыжками значений в глубину.
+
avatar
  • kiv69
  • 15 июня 2020, 17:09
0
Аналогично. Ламповых индикаторов просто не перевариваю, такая бяка.
Возможно, они кому-то и нравятся, но этот кто-то явно не был вынужден наблюдать их годами.
Учитывая, что сейчас IPS или OLED дисплей стоит дешевле лампового индикатора и по всем параметрам его превосходит, занятие лампами считаю тупиковым.
+
avatar
  • scorry
  • 16 июня 2020, 11:03
0
Вот именно. Любое наблюдение за ИН-ками даже в течение минуты-двух на многозначной индикации просто срывает мозг и крышу попыткой зафиксировать, куда прыгает очередная цифра при смене показаний. Особенно когда частотомер цифр на десять стоит не прямо перед глазами, и надо одним коротким взглядом, вскользь снять показания.
+
avatar
+3
необычненько.
+
avatar
+5
Ламповая паялка :)
Реально круто вышло!
+
avatar
  • Rzzz
  • 08 июня 2020, 23:40
+3
Неонки там вообще не в тему. Делал бы на светодиодах. Или на ЖК 1602

А на неонках — часы. Тем более, что неонки классические, с правильной цифрой «5».
+
avatar
  • sunpp
  • 09 июня 2020, 00:10
-1
про ошибку в слове «контроЛЛер» писали?
+
avatar
  • ewavr
  • 09 июня 2020, 01:30
+1
Гм, в атмеге324 в АЦП можно включить усиление х10, х100, х200, зачем нужен внешний усилитель сигнала термопары?
+
avatar
0
Как то промогргал этот момент. спасибо, буду знать
+
avatar
  • Stress
  • 09 июня 2020, 05:47
+1
Отлично!
Как говорит дядька Максим: «чётко! в натуре класс!»
+
avatar
+1
А мне нравится. Единственное современный пластиковый корпус как то не вяжется в этой конструкции.
+
avatar
+2
современный пластиковый корпус как то не вяжется в этой конструкции.
ну да, под эту индикацию больше идёт корпус с металлическим кожухом и в молотковой эмали.
+
avatar
0
Вероятно ещё мало ходил по инету, так как паяльных станций с газоразрядными индикаторами ещё не встречал.
+
avatar
+1
За проделанную работу огромный плюс. ( Жаль только 1 можно поставить)
Тип индикации — на любителя.
А так — все отлично получилось.
Уважаю людей с прямыми руками.
+
avatar
+1
Открыл обзор только чтобы убедиться, что мне не показалось и индикация действительно на ИНках сделана.
+
avatar
+1
Спасибо за труды.
+
avatar
0
Столько труда, а на выходе — уродство и безвкусица.
+
avatar
0
Как и программа, как и железо, увы.
+
avatar
  • VG1544
  • 11 июня 2020, 21:25
0
а есть готовый контроллер для фена на алике, до 10$? что бы не покупать готовую паяльную станцию, а взять фен и его, т.к. всё остальное есть.
+
avatar
  • kiv69
  • 15 июня 2020, 17:12
+1
Есть, у меня именно на нём и собрано. Брал, правда, за 12 или 13 баксов, но давно это было.
И переделывать пришлось, чтобы питался от одного напряжения.
+
avatar
  • maks_
  • 15 июня 2020, 18:32
0
Ссылку! Ну или слова для поиска?
+
avatar
  • GDN
  • 16 июня 2020, 19:37
+2
На плате маркировка 858D
Первая, что нашел поиском:
https://aliexpress.ru/item/item/32903214068.html,searchweb201602_,searchweb201603_

Или собрать самому ;-)

radiokot.ru/forum/viewtopic.php?f=25&t=8505&start=10420
+
avatar
  • Demo65
  • 14 июня 2020, 18:56
+2
Автору респект! Собрано устройство для себя и с душой.