marți, 24 decembrie 2019

Termostat cu MAXIDEV si Arduino


Salutare prieteni!

Iata ca dupa aproape 2 ani (!!!!) am reusit sa gasesc ragazul de a mai adauga un nou articol la blog.

Articolul de azi este primul dintr-o serie menita sa promoveze si sa testeze una din ultimele mele "creatii" si anume placa de teste universala MAXIDEV. A durat ceva pana am pus totul pe roate insa rezultatul final este staisfacator desi, probabil, va fi necesara o revizie a cablajului.

Destul cu "advertisingul", sa trecem la treaba!

Mai jos un filmulet cu modul de functionare:




Hardware


Dupa cum zice si titlul articolului propun pentu azi realizarea unui termostat programabil cu Arduino. Pentru montaj a fost folosit un Arduino in varianta sa Nano si placa de teste MAXIDEV, mai exact :
  • 4x butoane cu revenire;
  • senzorul de umidiate si temperatura DHT11;
  •  LCD-ul 16x2 
  • 1x releu.

Modul de conectare se poate vedea in poza de mai jos:



Functionare

Termostatul dispune de urmatoarele functii:

  • Control manual sau automat al elementului de executie (releu);
  • Doua moduri de functionare: Inacalzire sau racire;
  • Salvarea setarilor in EEPROM-ul microcontroller-ului.
Afisarea  parametrilor se face pe LCD-ul placii de test conectat la Arduino prin itermediul magistralei I2C. Au fost create pentru utilizator doua meniuri: unul in care se poate urmarii starea sistemului si un altul in care se pot modifica si salva parametrii. Trecerea de la un meniu la altul se face apasand butonul SW2 (BUTON-MENIU). Cele doua meniuri se pot vedea in fotografiile de  mai jos:







In primul meniu, pe langa afisarea temperaturii si umiditatii curente, sunt disponibile utilizatorului urmatoarele facilitati:

  • Selectarea modului de control - Manual/Automat (C:M/C:A). trecerea de la un mod la altul se face apasand butonul SW1 (BUTTON-UP). Dupa modificarea modului de control acesta este salvat automat in EEPROM. 
  • Selectarea modului de functionare - Incalzire/Racire (M:H/M:C) se face apasand butonul SW3(BUTTON-DOWN). La fel ca si in cazul modului de control modificarea va fi salvata automat in EEPROM.
  • In cazul in care este selectat modul de control "Manual" prin apasarea butonului SW4(BUTTON-SELECT) este permisa comanda manuala a elementului de executie.

In ceea ce priveste cel de-al doilea meniu lucrurile stau in felul urmator:
  • Cu ajutorul butoanelor SW1 (BUTTON-UP) si SW3 (BUTTON-DOWN) se incrementeaza / decrementeaza cei doi parametrii Sp (Setpoint) si Hyst (Hysteresis).
  • Selectia parametrilor se face utilizand butonul SW4 (BUTTON-SELECT)
  • Salvarea in EEPROM a parametrilor se face selectand campul "SAVE" si apsand butonul SW1 (BUTTON-UP).

Controlul automat este permis numai daca modul de control este setat ca fiind automat (C:A). In acest moment in functie de modul functionare sistemul se va comporta in mod diferit. Haideti sa vedem in cele ce urmeaza, punctual, functionarea propriu-zisa a termostatului.

Mod de functionare - Incalzire (M:H)


Sa presupunem ca dorim o temperatura maxima de 30'C (Setpoint) si ca temperatura minima de la care dorim sa inceapa incalzirea este de 27'C. Cu alte cuvinte vrem ca atunci cand temperatura mediului ambiant ajunge la 27'C elementul de incalzire sa porneasca si sa functioneze pana cand se atinge Setpoint-ul de 30'C. 

Pentru a obtine modul functionare descris mai sus, trebuie setate in cel de-al doilea meniu SP:30 si Hyst 3 (30-3=27). 


Mod de functionare - Racire (M:C)


Sa presupunem ca dorim o temperatura minima de 30'C (Setpoint) si ca temperatura maxima de la care dorim sa inceapa racirea este de 33'C. Cu alte cuvinte vrem ca atunci cand temperatura mediului ambiant ajunge la 33'C elementul de racire sa porneasca si sa functioneze pana cand se atinge Setpoint-ul de 30'C. 

Pentru a obtine modul functionare descris mai sus, trebuie setate in cel de-al doilea meniu SP:30 si Hyst 3 (30+3=33). 


Software...

Programul a fost scris pentru Arduino Nano si foloseste 3 librarii specifice: <LiquidCrystal_I2C.h>, <SimpleDHT.h> si <EEPROM.h>.

Mai jos codul sursa comentat:


  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
#include <SimpleDHT.h>
#include <EEPROM.h>

// addresses in EEPROM of parameters

#define eeprIsEmptyAddr 1
#define eeprCtrlAddr  2
#define eeprModeAddr  3
#define eeprSpAddr    4
#define eeprHystAddr  5

#include <LiquidCrystal_I2C.h>
LiquidCrystal_I2C lcd(0x27, 16, 2);

// Global Variables

int currentMenu = 0; //
int secondMenuSel = 0; // current selection in second menu
int secondMenuMaxSel = 3; // the number of selections in second menu 0,1,2
int lcdPages = 2; //number of lcdPages available on LCD, counting from 0

int dhtPin = 6; // dht data pin is connected to Arduino PIN 6
int relay_pin = 8; // relay is connected to Arduino PIN 
int samplePeriod = 1200; // dht sensor will be sampled every 1200ms

unsigned long timerMillis; // this will hold the return value of millis() function

byte buff_dhtTemp = 0; // this will hold the temperature value read from sensor
byte buff_dhtHum = 0;  // this will hold the humidity value read from sensor

unsigned int dhtTemp = -80; //initial value of temperature. This value will be displayed if dht sensor is not connected
unsigned int dhtHum = -80;  //initial value of humidity. This value will be displayed if dht sensor is not connected
unsigned int prev_dhtTemp = -80; // this variable is used to comapre the current temperature with the previous one read. This way the lcd will be updated only if there is a difference
unsigned int prev_dhtHum = -80;  // this variable is used to comapre the current humidity with the previous one read. This way the lcd will be updated only if there is a difference

int setPoint; // this is the setpoint of desired temperature
int setPointMaxTemp = 50; // the maximum setpoint that can be chosen
int setPointMinTemp = 20; // the minimum setpoint that can be chosen

int hyst; // this is the hysteresis. It's value it's uesed to regulate the control loop
int hystMax = 10; // max value of hysteresis
int hystMin = 1;  // min value of hysteresis

/* Below the control variables  */
String ctrl; // control mode "A"= automatic, "M"=manual
String mode; // operating mode "H"=heating, "C"=cooling;
String state = "Off"; // state of relay "Off" or "On"
String s_save = "Save";

String prev_ctrl = "A"; // control mode "A"= automatic, "M"=manual
String prev_mode = "H"; // operating mode "H"=heating, "C"=cooling;
String prev_state = "Off"; // state of relay "Off" or "On"

/*Class Button it is used to debounce the buttons */
class Button {

    typedef void (*cb)();
  private:
    bool trigger = true;
    bool wasPressed;
    unsigned long lastMillis;
    int pin;
    bool execute;
    cb callback;

  public:
    int debounceTime = 20;
    Button(bool _trigger, int _debounceTime, int _pin, cb _callback) {
      trigger = _trigger;
      debounceTime = _debounceTime;
      pin = _pin;
      callback = _callback;
      init_pin();
    }
    Button(int _pin , cb _callback) {
      pin = _pin;
      callback = _callback;
      init_pin();

    }
    void init_pin() {
      pinMode(pin, INPUT);
    }

    void checkStatus() {
      bool state = digitalRead(pin);
      if ((state == trigger) && (wasPressed == false)) {
        wasPressed = true;
        lastMillis = millis();
        execute = true;
      }
      if (state != trigger && wasPressed == true) {
        wasPressed = false;
      }
      if (millis() < lastMillis) {
        wasPressed = false;
      }
      if ((millis() - lastMillis) > debounceTime && wasPressed == true && execute == true) {
        execute = false;
        callback();
      }
    }
};

/*This function will update the data printed on LCD
  This program implements only 2 menus.
  The first menu displays the current temperature and humidity
  The second menu is used to set the setpoint, hysteresis and to save the data on eeprom
  The way this function was written offers the posibility to update only parts of the display.
*/
void printData(bool menuChanged, int par) {

  // menuChanged --> if menu is changed clear the lcd
  // par --> controls which paramter will be updated. If par is 0 then it will be always updated, this is used when a menu is changed

  if (menuChanged) {
    lcd.clear();
    lcd.setCursor(0, 0);
    secondMenuSel = 0;
    s_save = "Save";
  }

  if (currentMenu == 0) { //first menu
    String s_temp = "T=" + String(dhtTemp) + "'C ";
    String s_hum = "H=" + String(dhtHum) + "%";
    String s_ctrl = "C:" + ctrl + " ";
    String s_mode = "M:" + mode + " ";
    String s_state = "S:" + state + " ";

    //print temperature and humidity par variable. This group has par=1

    if (par == 1 || par == 0) {
      lcd.setCursor(0, 0);
      lcd.print(s_temp);
      lcd.setCursor(s_temp.length(), 0);
      lcd.print(s_hum);
    }

    //print control mode. This group has par=2;

    if (par == 2 || par == 0) {
      lcd.setCursor(0, 1);
      lcd.print(s_ctrl);
    }

    // print mode of working, heating or cooling. This group has par =3

    if (par == 3 || par == 0) {
      lcd.setCursor(s_ctrl.length(), 1);
      lcd.print(s_mode);
    }

    // print status of relay. This group has par=4

    if (par == 4 || par == 0) {
      lcd.setCursor(s_mode.length() + s_ctrl.length(), 1);
      lcd.print(s_state);
    }

  }
  else if (currentMenu == 1) { // second menu

    String s_setPoint = "Sp:" + String(setPoint) + "'C   ";
    String s_hyst = "Hyst:" + String(hyst) + "'C ";

    //print Setpoint value

    if (par == 1 || par == 0) {
      lcd.setCursor(1, 0);
      lcd.print(s_setPoint);
    }

    // print Hysteresys

    if (par == 2 || par == 0) {
      lcd.setCursor(1, 1);
      lcd.print(s_hyst);
    }

    // print save status

    if (par == 3 || par == 0) {
      lcd.setCursor(s_setPoint.length() + 1, 0);
      lcd.print(s_save);
    }

    // print selection char

    if (par == 4 || par == 0) {
      //first clear selection character
      lcd.setCursor(0, 0);
      lcd.print(" ");
      lcd.setCursor(s_setPoint.length(), 0);
      lcd.print(" ");
      lcd.setCursor(0, 1);
      lcd.print(" ");

      if (secondMenuSel == 0) { // setpoint is selected
        lcd.setCursor(0, 0);
        lcd.print("*");
      }
      if (secondMenuSel == 1) { // hysteresys is selected
        lcd.setCursor(0, 1);
        lcd.print("*");
      }
      if (secondMenuSel == 2) { // save settings is slected
        lcd.setCursor(s_setPoint.length(), 0);
        lcd.print("*");
      }
    }
  }
}


/* This is the callback function for the menu button
When this button is pressed (D2), after the debounce it will change the menu on the lcd.
It will call printData function and it will clear the lcd
*/
void changeMenu() {
  currentMenu++;
  if (currentMenu == lcdPages) {
    currentMenu = 0;
  }
  printData(true, 0);
}


/*This is the callback function of select button
When this button is pressed, depending on current menu, different things will happen:
If the first menu is selected then  this button will turn on/off the realy but only
if control mode is manual.
If the second menu is selected then this button will change the posistion on screen of 
the selection character (*).
*/
void f_select() {
  if (currentMenu == 0) {
    if (ctrl == "M") {
      if (state == "Off") {
        digitalWrite(relay_pin, HIGH);
        state = "On";
      }
      else {
        digitalWrite(relay_pin, LOW);
        state = "Off";
      }
    }
    printData(false, 4);
  }
  if (currentMenu == 1) {
    secondMenuSel++;
    if (secondMenuSel == secondMenuMaxSel) {
      secondMenuSel = 0;
    }
    printData(false, 4);
  }
}

/*
This is the callback function of up button.
When this button is pressed, depending on current menu, different things will happen:
If the first menu is selected then  this button will change control mode to Automatic or Manual.
Also it will save the setting to eeprom.

If the second menu is selected then this button depending of the selected parameter will increment it's value.
If Save will be selected the it will save the settings to eeprom.
*/
void f_up() {

  if (currentMenu == 0) {
    if (ctrl == "M") {
      ctrl = "A";
      EEPROM.write(eeprCtrlAddr, 128);
    }
    else {
      ctrl = "M";
      EEPROM.write(eeprCtrlAddr, 64);
    }
    printData(false, 2);
  }


  if (currentMenu == 1) {

    if (secondMenuSel == 0) { //setpoint selected;
      if (setPoint < setPointMaxTemp) {
        setPoint++;
        s_save = "Save ";
        printData(false, 1);
        printData(false, 3);

      }
    }
    if (secondMenuSel == 1) { // hyst selected;
      if (hyst < hystMax) {
        hyst++;
        s_save = "Save ";
        printData(false, 2);
        printData(false, 3);
      }
    }
    if (secondMenuSel == 2) { //save selected
      s_save = "Saved";
      EEPROM.write(eeprSpAddr, setPoint);
      EEPROM.write(eeprHystAddr, hyst);
      printData(false, 3);
    }
  }
}

/*
This is the callback function ofdown button.
When this button is pressed, depending on current menu, different things will happen:
If the first menu is selected then  this button will change the operation mode to Heating or Cooling.
Also it will save the setting to eeprom.

If the second menu is selected then this button depending of the selected parameter will decrement it's value.
*/

void f_down() {


  if (currentMenu == 0) {
    if (mode == "H") {
      mode = "C";
      EEPROM.write(eeprModeAddr, 64);
    }
    else {
      mode = "H";
      EEPROM.write(eeprModeAddr, 128);
    }
    printData(false, 3);
  }


  if (currentMenu == 1) {
    s_save = "Save ";
    if (secondMenuSel == 0) { //setpoint selected;
      if (setPoint > setPointMinTemp) {
        setPoint--;
        printData(false, 1);
        printData(false, 3);
      }
    }
    if (secondMenuSel == 1) { // hyst selected;
      if (hyst > hystMin) {
        hyst--;
        printData(false, 2);
        printData(false, 3);
      }
    }
  }
}

/*Here the buttons objects are created 
The constructor takes two paramters:
1.The Arduino pin that will be configured as input
2.The callback function that will be executed when the button was pressed and only after the debouncing.
*/
Button menu(2, changeMenu);
Button select(3, f_select);
Button up(4, f_up);
Button down(5, f_down);

// Here the SimpleDHT11 object it is created.
SimpleDHT11 dht11(dhtPin);

/*
This function is called every 1200ms (samplePeriod variable).
It will read the sensor values and if the first menu is selected 
and if the value read is different from the previous one read 
it will print the updated data
*/
void getDhtData() {
  int err = SimpleDHTErrSuccess;
  if ((err = dht11.read(&buff_dhtTemp, &buff_dhtHum, NULL)) != SimpleDHTErrSuccess) {
    dhtTemp = -80;
    dhtHum = -80;
  }
  else {
    dhtTemp = (int) buff_dhtTemp;
    dhtHum = (int) buff_dhtHum;
  }
  if (prev_dhtTemp != dhtTemp || prev_dhtHum != dhtHum) {
    prev_dhtTemp = dhtTemp;
    prev_dhtHum = dhtHum;
    if (currentMenu == 0) {
      printData(false, 1);
    }
  }
}

/*
This function impelements a millis() based timer.
After the timer will expire the getDhtData() function
will be called.
*/
void dhtSampleTimer() {
  if ((millis() - timerMillis) > samplePeriod) {
    getDhtData();
    timerMillis = millis();
  }
  if (millis() < timerMillis) {
    timerMillis = millis();
  }
}

/*
This function it is called from setup. It will read the values stored on eeprom.
If no values are present (the value of eeprIsEmptyAddr is 255) then the default ones will be written
*/

void getEepromData() {
  if (EEPROM.read(eeprIsEmptyAddr) == 255) { //this is first time when program is running so no eeprom data is available
    EEPROM.write(eeprIsEmptyAddr, 128); //data will be written to eeprom, so next time settings will be available to read
    EEPROM.write(eeprCtrlAddr, 128); //default is automatic mode
    EEPROM.write(eeprModeAddr, 128); //default is heating mode;
    EEPROM.write(eeprSpAddr, 25); //default is 25*C
    EEPROM.write(eeprHystAddr, 5); //default is 5*C
  }
  if (EEPROM.read(eeprIsEmptyAddr) == 128) { // there is data stored inside eeprom
    if (EEPROM.read(eeprCtrlAddr) == 128) {
      ctrl = "A";
    }
    else {
      ctrl = "M";
    }
    if (EEPROM.read(eeprModeAddr) == 128) {
      mode = "H";
    }
    else {
      mode = "C";
    }
    setPoint = EEPROM.read(eeprSpAddr);
    hyst = EEPROM.read(eeprHystAddr);
  }
}

/*
This function is the main control loop. This will run only if the control mode is Automatic.

***HEATING MODE***
Below an example of how it works if operating mode is Heating:
Setpoint = 33'C
Hystersis = 3'C
The heater will be turened on if the current temperature is BELOW or EQUAL to Setpoint-Hysteresis (33-3 = 30'C).
The heater will turned off if current temperature is GREATER or EQUAL to Setpoint (33'C).

***COOLING MODE****
Below an example of how it works if operating mode is Heating:
Setpoint = 26'C
Hystersis = 3'C
The heater will be turened on if the current temperature is GREATER or EQUAL to Setpoint-Hysteresis (26+3 = 29'C).
The heater will turned off if current temperature is BELOW or EQUAL to Setpoint (26'C).
*/

void tempControl() {
  if (ctrl == "A") { //automatic mode is selected
    if (mode == "H") { //heating is selected
      if (dhtTemp <= (setPoint - hyst)) {
        digitalWrite(relay_pin, HIGH);
        state = "On";
      }
      if (dhtTemp >= setPoint) {
        digitalWrite(relay_pin, LOW);
        state = "Off";
      }
      if (currentMenu == 0) {
        printData(false, 4);
      }
    }
    if (mode == "C") { // cooling is selected
      if (dhtTemp >= (setPoint + hyst)) {
        digitalWrite(relay_pin, HIGH);
        state = "On";
      }
      if (dhtTemp <= setPoint) {
        digitalWrite(relay_pin, LOW);
        state = "Off";
      }
      if (currentMenu == 0) {
        printData(false, 4);
      }
    }
  }
}

void setup() {
  pinMode(relay_pin, OUTPUT);
  digitalWrite(relay_pin, LOW);
  lcd.init();
  lcd.backlight();
  lcd.setCursor(0, 0);
  lcd.print("MAXIDEV BY");
  lcd.setCursor(3, 1);
  lcd.print("-HIGHBYTE-");
  delay(500);
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("MAXIDEV");
  lcd.setCursor(3, 1);
  lcd.print("-TERMOSTAT-");
  delay(500);
  lcd.clear();
  lcd.noBacklight();
  delay(1000);
  getEepromData();
  getDhtData();
  printData(true, 0);
  timerMillis = millis();
}

void loop() {
  menu.checkStatus();
  select.checkStatus();
  up.checkStatus();
  down.checkStatus();
  dhtSampleTimer();
  tempControl();
}

Cam atat pentru azi. Sa auzim de bine si nu va sfiiti sa lasati un comentariu.


PS: Pentru cei care prezinta interes pentru placa de teste MAXIDEV ma puteti contacta pentru mai multe detalii, oricare ar fi acestea.


Niciun comentariu:

Trimiteți un comentariu