Этот устройство предназначено для получения пара с контролем температуры титановой спирали (для никелевой или железной — нужно поменять коэффициент RTCHANGE), что позволяет получать много (или очень много, если фитиль подводит достаточно жидкости) пара без перегрева с образованием разных нехороших веществ, продуктов распада глицерина, пропиленгликоля и ароматизаторов.
Схема предназначена для работы с батареей для RC-моделей, из двух Li-ячеек. Мной был выбран аккумулятор 500 mAh 25C напряжение 7.4V. Ток и нагрузочную способность можно варьировать в широких пределах, лишь бы выдавал достаточный ток с холодной спиралью, когда ее сопротивление минимально.
Теоретически, можно переделать и под аккумулятор с одной ячейкой, но нужно подбирать соответствующие MOSFET-ключи — с низким напряжением переключения и сопротивлением в открытом состоянии. Можно будет даже убрать делители R1+R2 и R3+R4, повысив точность измерения в два раза (хотя реального эффекта будет немного).
Резисторы R5 и R6 — должны быть мощностью не менее 0.25 ватт (а лучше и больше). Выход LED — для пары белых светодиодов, чтобы мог работать как аварийный фонарик.
Отсутствует аппаратный выключатель, что приводит к довольно быстрому разряду батареи (за день или около того), несмотря на спящий режим. То ли я его неправильно использую, то ли ардуинка с регуляторами напряжения в ней и экране — потребляют слишком много даже в неактивном состоянии. Надо было, наверное, поставить еще один MOSFET-ключ на всю схему, с маленьким переключателем, для включения/отключения напряжения питания.
Корпус был напечатан на 3D принтере из PLA. Пустое место под разъемом атомайзера — для проводов батарейки, их на модельных аккумуляторах делают довольно длинными, а обрезать не хотелось.
// Temperature controller for cigarette, schematics in ard-smoke.sch // // Copyright (C) 2015 Vasim V. // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see <http://www.gnu.org/licenses/>. #include <U8glib.h> #include <MsTimer2.h> #include <TimerOne.h> #include <EEPROM.h> #include <avr/sleep.h> // Additional info to console // #define DEBUG // Temperature ADC pin - ITEMP #define P_ITEMP 0 // Battery ADC pin - CHECK #define P_VMAIN 1 // Coil N-FET gate - OOn #define P_OON 9 // Temperature check N-FET gate - OTest #define P_OTEST 3 #define NBUTTONS 3 // Coil button #define P_BCOIL 10 // Plus button */ #define P_BPLUS 11 // Minus button */ #define P_BMINUS 12 // Delay for FET change, microseconds #define FETSW_DELAY 10 // Interrupt period (timer2), microseconds #define TIMER_PERIOD 10000 // PWM Period, microseconds #define PWM_PERIOD 1024 // Test resistor resistance, in Ohm #define RTEST 4.7f // Temparture-resistance koefficient // Titan #define RTCHANGE 0.0035f // Kantal - not working (too low resolution) // #define RTCHANGE 0.0001f // Default zero (when no difference between rcoil_zero and rcoil) temperature #define TEMP_ZERO 25 // Sleep timer (in timer2 cycles, seconds*100), activate if coil button was pressed for #define SLEEP_TIME 12000 // How long you should keep button pressed to change temperature, in time2 cycles (seconds*100) #define BUTTON_SENS_TIME 100 #define PID_P 60.0 #define PID_I 0.75 #define PID_D 20.0 U8GLIB_SSD1306_128X64 u8g(U8G_I2C_OPT_DEV_0|U8G_I2C_OPT_NO_ACK|U8G_I2C_OPT_FAST); // Coil is on or not boolean coil_on; // Voltage readings from ADCs int coilv; int vmainv; // Calculated from vmainv, in volts float vbat; // Calculated from coilv, in volts and ohms float voff; float rcoil; // Coil resistance at 25C float rcoil_zero = 0.0f; // Current temperature in celsius double tcur; // Cut-off temperature in celsius double tcut; // PID regulator stuff double Output; // Buttons debounce stuff #define I_BCOIL 0 #define I_BPLUS 1 #define I_BMINUS 2 uint8_t pbuttons[NBUTTONS] = {P_BCOIL, P_BPLUS, P_BMINUS}; uint8_t sw_check[NBUTTONS]; // Buttons state after debouncing boolean sbuttons[NBUTTONS] = {false, false, false}; // How long buttons state wasn't changed (in timer2 cycles) uint32_t idlebuttons[NBUTTONS] = {0, 0, 0}; // Ignore if buttons were pressed at startup boolean ignore_buttons = false; // In deep sleep boolean sleeping; double errSum, lastErr; double outputLast = 0.0; #define MAX_CHANGE 1023.0 void MyPIDStart() { errSum = lastErr = outputLast = 0.0; MyPIDCompute(); } // Calculate output void MyPIDCompute() { double timeChange = TIMER_PERIOD / 1000; double error = tcut - tcur; errSum += (error * timeChange); double dErr = (error - lastErr) / timeChange; double preOutput; /*Compute PID Output*/ preOutput = PID_P * error + PID_I * errSum + PID_D * dErr; if ((preOutput - outputLast) > MAX_CHANGE) preOutput = outputLast + MAX_CHANGE; else if ((outputLast - preOutput) > MAX_CHANGE) preOutput = outputLast - MAX_CHANGE; Output = preOutput - outputLast; if (Output < 0) Output = 0; /*Remember some variables for next time*/ outputLast = preOutput; lastErr = error; } // Calculate coil resistance and temperature void update_temp() { rcoil = (vmainv * RTEST) / (coilv * 1.0f + 1) - RTEST; if (rcoil < 0) rcoil = 0; if (rcoil_zero > 0.01) tcur = double((rcoil / rcoil_zero - 1) / RTCHANGE); else tcur = 0; } void eeprom_writef(int addr, float x) { uint8_t i; for (i = 0; i < sizeof(float); i++) { EEPROM.update(addr + i, *((char *) &x + i)); } } float eeprom_readf(int addr) { float x; int i; for (i = 0; i < sizeof(float); i++) { *((char *) &x + i) = EEPROM.read(addr + i); } // Test if we did read float really, not "NAN" if ((x+1.0) > x) return x; return 0; } // Timer interrupt - Test buttons, check temperature and fire coil, etc void intcallback() { uint8_t i; // Debounce buttons for (i = 0; i < NBUTTONS; i++) { if (sbuttons[i] != (digitalRead(pbuttons[i]) == LOW)) { if (sw_check[i] > 5) { sw_check[i] = 0; sbuttons[i] = (digitalRead(pbuttons[i]) == LOW); idlebuttons[i] = 0; } else { sw_check[i]++; idlebuttons[i]++; } } else { sw_check[i] = 0; idlebuttons[i]++; } } // Measuring temperature - turning off main coil FET before if (coil_on || ignore_buttons) { Timer1.disablePwm(P_OON); digitalWrite(P_OON, LOW); delayMicroseconds(FETSW_DELAY); } // Don't do anything else if (sleeping || ignore_buttons) return; digitalWrite(P_OTEST, HIGH); delayMicroseconds(FETSW_DELAY); coilv = analogRead(P_ITEMP); vmainv = analogRead(P_VMAIN); digitalWrite(P_OTEST, LOW); delayMicroseconds(FETSW_DELAY); // Measuring is done, turning on coil back if (coil_on) Timer1.pwm(P_OON, Output, PWM_PERIOD); else { Timer1.disablePwm(P_OON); digitalWrite(P_OON, LOW); } // Calculate temperature update_temp(); if (coil_on) // PID processing (calculate output) MyPIDCompute(); #ifdef DEBUG Serial.print("Temp: "); Serial.print(tcur+TEMP_ZERO); Serial.print("C, rcoil: "); Serial.print(rcoil); Serial.print("Ohm, Output: "); Serial.println(Output); #endif // Fire coil if button pressed if (!coil_on && sbuttons[I_BCOIL] && !ignore_buttons && (tcut >= tcur)) { MyPIDStart(); coil_on = true; Timer1.pwm(P_OON, Output, PWM_PERIOD); } // Coil shutdown if button is released or more than 50 celsius above if (!sbuttons[I_BCOIL] || ((tcur - 5) > tcut)) { coil_on = false; Output = 0; Timer1.setPwmDuty(P_OON, 0); Timer1.disablePwm(P_OON); digitalWrite(P_OON, LOW); } } void setup() { Serial.begin(115200); ignore_buttons = true; sleeping = false; // ADC pins pinMode(P_ITEMP, INPUT); pinMode(P_VMAIN, INPUT); // Coil/check FETs pinMode(P_OON, OUTPUT); digitalWrite(P_OON, LOW); coil_on = false; pinMode(P_OTEST, OUTPUT); digitalWrite(P_OTEST, LOW); // Configuring buttons pins pinMode(P_BCOIL, INPUT_PULLUP); pinMode(P_BPLUS, INPUT_PULLUP); pinMode(P_BMINUS, INPUT_PULLUP); // Display init // Rotate if needed // u8g.setRot180(); // From u8g_lib example - assign default color value if ( u8g.getMode() == U8G_MODE_R3G3B2 ) { u8g.setColorIndex(255); // white } else if ( u8g.getMode() == U8G_MODE_GRAY2BIT ) { u8g.setColorIndex(3); // max intensity } else if ( u8g.getMode() == U8G_MODE_BW ) { u8g.setColorIndex(1); // pixel on } else if ( u8g.getMode() == U8G_MODE_HICOLOR ) { u8g.setHiColorByRGB(255,255,255); } // Read coil resistance at 25C from eeprom, update current values // rcoil_zero = eeprom_readf(0); tcut = eeprom_readf(4); if ((tcut < 100.0) || (tcut > 300.0)) tcut = 180; // Disable buttons until released if any of it pressed at startup if ((digitalRead(P_BPLUS) == LOW) || (digitalRead(P_BMINUS) == LOW) || (digitalRead(P_BCOIL) == LOW)) { int i; ignore_buttons = true; for (i = 0; i < NBUTTONS; i++) sbuttons[i] = (digitalRead(pbuttons[i]) == LOW); } else ignore_buttons = false; // Timers initialize MsTimer2::set(TIMER_PERIOD / 1000, intcallback); MsTimer2::start(); Timer1.initialize(1000); Timer1.pwm(9, 0, PWM_PERIOD); } static int loopcycle = 0; static int showtemp; static float showrcoil; void draw() { char tmpbuf[16]; if (!sleeping && idlebuttons[I_BCOIL] > SLEEP_TIME) return; u8g.setFont(u8g_font_helvR12); memcpy(tmpbuf, "Batt", 4); if (vmainv < 1022) { dtostrf(vbat, 4, 1, tmpbuf+4); memcpy(tmpbuf+8, "V - ", 4); } else memcpy(tmpbuf+4, " MAX", 5); if (vbat > 6.3) memcpy(tmpbuf+12, "OK", 3); else { if (loopcycle % 2) u8g.setFont(u8g_font_helvB12); memcpy(tmpbuf+12, "LOW!", 5); } u8g.drawStr(0, 64, tmpbuf); u8g.setFont(u8g_font_helvR12); if (sbuttons[I_BCOIL]) u8g.drawStr(90, 16, "C"); else u8g.drawStr(90, 16, "."); if (sbuttons[I_BPLUS]) u8g.drawStr(99, 16, "+"); else u8g.drawStr(99, 16, "."); if (sbuttons[I_BMINUS]) u8g.drawStr(108, 16, "-"); else u8g.drawStr(108, 16, "."); memcpy(tmpbuf, "= ", 3); dtostrf(tcut+25.0f, 4, 0, tmpbuf+2); u8g.drawStr(0, 48, tmpbuf); tmpbuf[0] = 'R'; dtostrf(rcoil, 5, 2, tmpbuf+1); memcpy(tmpbuf+6, "Ohm", 4); u8g.drawStr(0, 32, tmpbuf); dtostrf(float(showtemp), 4, 0, tmpbuf); tmpbuf[4] = 'C'; tmpbuf[5] = '\0'; u8g.drawStr(18, 16, tmpbuf); if (coil_on) u8g.drawStr(0, 16, ">>"); else u8g.drawStr(0, 16, " "); dtostrf(float(Output), 5, 0, tmpbuf); u8g.drawStr(64, 48, tmpbuf); } // To test if minus button was pressed 3 times in 6 seconds - exit sleep mode boolean last_b_state; int nchanges; int ncycles; // Update EEPROM with temperature limit boolean update_eeprom = false; void loop() { int i; vbat = (10.0 * vmainv) / 1024.0; voff = (10.0 * coilv) / 1024.0; showtemp = tcur + TEMP_ZERO; // Redraw screen if (!sleeping) { // u8g_lib picture loop u8g.firstPage(); do { draw(); } while( u8g.nextPage() ); } // Change temperature limit if MINUS or PLUS pressed if (!ignore_buttons) { if (sbuttons[I_BMINUS] && idlebuttons[I_BMINUS] > BUTTON_SENS_TIME) { tcut = tcut - 5; idlebuttons[I_BMINUS] = 0; update_eeprom = true; } if (sbuttons[I_BPLUS] && idlebuttons[I_BPLUS] > BUTTON_SENS_TIME) { tcut = tcut + 5; idlebuttons[I_BPLUS] = 0; update_eeprom = true; } } // Sleep after turning off screen if (!sleeping && (idlebuttons[I_BCOIL] > SLEEP_TIME)) { sleeping = true; ignore_buttons = true; u8g.sleepOn(); } // Check if should wake up (MINUS button pressed few times) if (sleeping) { ncycles++; if (last_b_state != sbuttons[I_BMINUS]) { ncycles = 0; last_b_state = sbuttons[I_BMINUS]; nchanges++; if (nchanges >= 6) { rcoil_zero = 0; sleeping = false; u8g.sleepOff(); for (i = 0; i < NBUTTONS; i++) idlebuttons[i] = 0; } } else { if (ncycles > 20) { ncycles = 0; nchanges = 0; } } } // Check if all buttons are released if (ignore_buttons && !sleeping) { int i; boolean pressed = false; for (i = 0; i < NBUTTONS; i++) { if (sbuttons[i]) pressed = true; } if (!pressed) ignore_buttons = false; } loopcycle++; // Serial.println(vmainv); if (sleeping) { ignore_buttons = true; set_sleep_mode(SLEEP_MODE_PWR_SAVE); sleep_enable(); } if (((rcoil_zero < 0.01) && (rcoil > 0.01)) || (showtemp < -50)) { rcoil_zero = rcoil; // update_eeprom = true; } if (update_eeprom) { eeprom_writef(0, rcoil_zero); eeprom_writef(4, tcut); update_eeprom = false; } delay(333); }
ссылка на оригинал статьи http://geektimes.ru/post/268872/
Добавить комментарий