Termostato con Arduino e sensore DS18B20
In questo articolo ti mostro come ho realizzato un termostato con Arduino ed il sensore di temperatura DS18B20.
Tutto ha inizio con un piccolo scaldabagno di casa che smette di funzionare, dopo qualche indagine ed un pò di ricerche su internet capisco che quello che si è rotto è proprio il termostato.
Bene ho pensato, una decina di euro e lo scaldabagno tornerà a funzionare…
E no!
Non è proprio così.
Questo termostato non si trova facilmente, e se lo trovi costa pure un pochino. Onestamente non mi va di spendere più di 20€ per un termostato, quando con una sessantina di euro ricomprerei l’intero scaldabagno. Su Amazon supera i 30€.
Questo è il termostato in questione. Da quel che ho capito, rileva la temperatura stando a contatto con una parte non isolata termicamente dello scaldabagno.
Parte la sfida
A questo punto decido di realizzare un termostato con Arduino e quel po di materiale che mi trovo al seguito.
Anni fa in uno stock di componenti presi online mi sono ritrovato dei sensori di temperatura DS18B20 compresi di rivestimento impermeabile e cavetto in gomma resistente. Un po di ricerche e decido che potrebbe essere il sensore che fa al caso mio.
Sensore DS18B20
Il sensore DS18B20 è un termometro digitale con una risoluzione programmabile tra i 9 e i 12 bit. Consente di misurare con precisione temperature che vanno dai -55° ai + 125° in ambienti umidi e nel range -10° + 85° l’approssimazione è di ±0.5°.
Le informazioni vengono scambiate tramite un’interfaccia denominata 1-Wire, quindi su un solo filo. Per questo il sensore si compone da soli tre pin, due per l’alimentazione e uno per lo scambio dati.
L’alimentazione può variare tra i 3V e i 5.5V ed oltre alla rilevazione di temperatura è possibile settare degli allarmi direttamente sul dispositivo, in modo da far scattare procedure di sicurezza in caso di temperature oltre i range consentiti.
Una cosa molto interessante è che ogni dispositivo realizzato possiede un seriale univoco per cui sullo stesso bus dati possono coesistere teoricamente un numero illimitato di sensori.
Per maggiori dettagli ti invito a leggere il datasheet qui.
Libreria Arduino per sensore DS18B20
Esistono diversi modi per interfacciare Arduino con il sensore DS18B20, si potrebbe direttamente dialogare tramite la libreria OneWire (quindi armarsi di pazienza e seguire le istruzioni sul datasheet per lo scambio dei dati) o utilizzare una delle librerie dedicate presenti online.
Trattandosi nel mio caso di un applicazione abbastanza basilare, mi sono “accontentato” della libreria DS18B20_RT di RobTillaart. Si tratta di una versione semplificata ed alleggerita della più completa e ricca di metodi Arduino-Temperature-Control-Library di Miles Burton.
Se vorrai impiegare questo tipo di sensore in uno dei tuoi progetti, ti consiglio di dargli un’occhiata ad entrambe e valutare quale conviene in base alle tue esigenze.
Termostato con Arduino funzionamento iniziale
Inizialmente doveva essere una cosa da quattro linee di codice, ma poi man mano che scrivevo mi rendevo conto che le quattro linee non bastavano più.
In principio il film prevedeva questo:
- Leggi la temperatura
- Confrontala con quella impostata tramite potenziometro dall’utente
- Se inferiore, attiva il relè (quindi dai corrente alla resistenza)
- Se superiore disattiva il relè
- Easy
Ma poi la cosa si è fatta più interessante… intanto devo mostrare in qualche modo il valore di temperatura che si sta cercando di impostare, quindi ho pensato ad un display e visto che ultimamente ho smanettato con degli OLED 128×64, ho optato per quelli.
Seconda cosa: se va via la corrente, la temperatura impostata dall’utente deve essere memorizzata da qualche parte, e per questo ho sfruttato l’EEPROM di Arduino come visto in un precedente articolo.
Cosa succede se per qualche motivo il sensore va in errore o Arduino si blocca ed in quel momento il relè è attivo? Riscaldo l’acqua ad oltranza? Credo non sia il caso… Quindi ho pensato anche a questa eventualità.
Durante le prime prove ho realizzato che il potenziometro non è un valido alleato in queste circostanze perché il valore della sua lettura è costantemente mutevole e a me serve invece una lettura stabile.
Quindi via il potenziometro e largo all’encoder 😎
Termostato con Arduino cosa è diventato
Come avrai notato sono partito da una cosuccia da niente per finire per realizzare qualcosa di ben diverso.
Il termostato che ho realizzato utilizza quindi un piccolo display OLED da 128×64 pixel per mostrare a video le informazioni, un encoder per ricevere le impostazioni dall’utente, la EEPROM di Arduino per salvare dati in memoria, il sensore DS18B20 per leggere la temperatura, un relè per comandare il carico e per essere sicuro che il programma non rimanga mai in stallo, ho utilizzato il watchdog come visto nell’ultimo articolo.
Il display del termostato
Ecco come si presenta il display del termostato:
Schema Termostato con Arduino
Lo schema è abbastanza semplice da replicare, da prestare attenzione sulla resistenza di pull-up da 4.7K collegata tra il bus del sensore e i +5V. Senza questa resistenza potresti avere problemi di comunicazione.
Codice Termostato con Arduino
Come potrai notare dalle quattro linee di codice preventivate all’inizio, se ne sono accumulate molte di più.
Per mantenere organizzato e più leggibile il codice, come buona norma, ho diviso in funzioni separate le varie parti del programma per arrivare ad ottenere un ciclo di loop che si presenta così:
void loop() { // Controlla pressione dello switch sull'encoder checkSwitch(); // Leggi la temperatura readTemp(); // Stampa dati sul display printData(); // Gestisci relé controlRelay(); // Resetta Watchdog wdt_reset(); }
Per non inondare di commenti l’intero listato, le parti di codice che si riferiscono ad argomenti che ho già trattato sono scarsamente descritte. Per qualsiasi dubbio dai un’occhiata agli articoli linkati nel post o utilizza la sezione commenti a fondo pagina.
/* Termostato con Arduino e sensore DS18B20 Autore : Andrea Lombardo Web : http://www.lombardoandrea.com Post : https://wp.me/p27dYH-RX */ // Inclusione librerie #include <Adafruit_GFX.h> #include <Adafruit_SSD1306.h> #include <Bounce2.h> #include <DS18B20.h> #include <EEPROM.h> #include <OneWire.h> #include <avr/wdt.h> //Whatchdog di sicurezza // Definizione costanti #define EEPROM_ADDRESS 0 // Indirizzo di memoria dove leggere e scrivere la temperatura impostata #define REFRESH_RATE 5 // Ogni quanti secondi ripetere la lettura della temperatura #define MIN_TEMP 5 // Temperatura minima impostabile #define MAX_TEMP 180 // Temperatura massima impostabile #define SCREEN_WIDTH 128 // Larghezza display in pixels #define SCREEN_HEIGHT 64 // Altezza display in pixels #define DEBOUNCE_DELAY 15 // Delay per il debounce del bottone sull'encoder /* Info sulle dimensioni dei caratteri da utilizzare per il corretto posizionamento degli elementi sul display */ #define CHAR_SPACE 6 #define CHAR_HEIGHT 8 #define SM_FONT_SIZE 1 #define MD_FONT_SIZE 2 #define LG_FONT_SIZE 5 /* Per mostrare lo stato ON/OFF del relay sul display ho deciso di ricreare la grafica di un'interruttore simile a quelli che si trovano nelle impostazioni dei nostri cellulari. Ho definito delle costanti per faicilitarmi l'operazione di disegno e posizionamento */ #define TOGGLE_WIDTH 38 // Larghezza switch #define TOGGLE_HEIGHT 20 // Altezza switch #define PADDING 10 // Margine dai bordi del display #define TOGGLE_RADIUS 5 // Arrotondamento rettangolo dello switch #define TOGGLE_CIRCLE_RADIUS 7 // Raggio del pallino interno allo switch // Definizione costanti pin #define PIN_ENCODER_CLK 2 // Pin clk dell'encoder #define PIN_ENCODER_DT 3 // Pin dt dell'encoder #define PIN_ENCODER_SW 4 // Pin switch presente sull'encoder #define PIN_TEMP_SENSOR 5 // Pin per il bus dati del sensore #define PIN_RELAY 6 // Pin di comando per il relé // Variabili di appoggio long lastTempReadMills; // Appoggio ultima volta che è stata fatta una lettura int sensorTemp; // Parte intera della temperatura restituita dal sensore int sensorTempDecimals; // I decimali della temperatura che serviranno per disegnare i trattini a fondo display int setTemperature; // Temperatura desiderata int prevClk; // Gestione movimenti dell'encoder bool error; // Se il sistema è in errore (determinato dal sensore di temperatura) bool relayStatus; // Status del relé bool modEdit; // Quando true siamo in modalità modifica // Istanze librerie OneWire oneWire(PIN_TEMP_SENSOR); DS18B20 sensor(&oneWire); Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT); Bounce btnMode = Bounce(); /* Comanda il relè */ void controlRelay() { // Se il sistema è in errore stacca a prescindere il relè ed esci dalla funzione if (error) { relayStatus = false; digitalWrite(PIN_RELAY, relayStatus); return; } if (sensorTemp < setTemperature) { relayStatus = true; } else { relayStatus = false; } // Aggiorna valore sul pin del relè sol se non sono in modalità impostazione if (!modEdit) { digitalWrite(PIN_RELAY, relayStatus); } } /* Interpreta i movimenti dell'encoder. Ha effetto solo se siamo in modalità impostazione */ void checkEncoder() { if (!modEdit) return; int currClk = digitalRead(PIN_ENCODER_CLK); int currDt = digitalRead(PIN_ENCODER_DT); if (currClk != prevClk) { if (currDt == currClk) { setTemperature--; } else { setTemperature++; } prevClk = currClk; if (setTemperature > MAX_TEMP) { setTemperature = MIN_TEMP; } else if (setTemperature < MIN_TEMP) { setTemperature = MAX_TEMP; } } } /* "Ascolta" i cambiamenti dello switch per passare dalla modalità normale alla modalità impostazione */ void checkSwitch() { btnMode.update(); if (btnMode.fell()) { modEdit = !modEdit; /* Se stiamo uscendo dalla modalità impostazione salviamo il valore sulla eeprom */ if (!modEdit) { // Il metodo .update aggiorna il valore solo se diverso da quello // registrato in precedenza EEPROM.update(EEPROM_ADDRESS, setTemperature); } } } /* Esegue la lettura della temperatura ogni REFRESH_RATE secondi e solo se non siamo in modalità impostazione. */ void readTemp() { if ((millis() - lastTempReadMills >= (REFRESH_RATE * 1000)) && (!modEdit)) { // Richiedo lettura al sensore sensor.requestTemperatures(); /* Nell'attesa che avvenga la lettura e la conversione, creo un ulteriore controllo per eivtare che a causa di qualche problema con il sensore, il codice rimanga nel loop in attesa di un valore */ unsigned long timeout = millis(); while (!sensor.isConversionComplete()) { if (millis() - timeout >= 800) { // Se il sensore è andato in timeout error = true; break; } else { error = false; } } // Recupero temperatura dalla libreria float t = sensor.getTempC(); // Confronto il valore restituito con le costanti di errore della libreria if (t == DEVICE_CRC_ERROR || t == DEVICE_DISCONNECTED) { // Se il sensore restituisce un errore error = true; } else { // Altrimenti aggiorno la variabile sensorTemp sensorTemp = (int) t; // Prelevo solo la parte intera del valore restituito dal sensore float _decimals = ( t - sensorTemp) * 10; // Estraggo la parte decimale sensorTempDecimals = round(_decimals); // E la arrotondo per ottenere un numero che vada da 0 a 9 che sarà il numero di trattini a fondo display error = false; // resetto eventuale stato di errore } // Aggiorna lastTempReadMills per tenere traccia dell'ultima lettura lastTempReadMills = millis(); } } /* Stampa sul display un messaggio durante la fase di setup. */ void printInitScreen() { display.clearDisplay(); display.setTextColor(WHITE); display.setTextSize(MD_FONT_SIZE); display.setCursor(0, SCREEN_HEIGHT / 2 - ((MD_FONT_SIZE * CHAR_HEIGHT) / 2)); display.print("Avvio..."); display.display(); } /* Stampa la parte superiore del display. Il contenuto cambierà in base allo stato della variabile modEdit. */ void drawHeader() { display.fillRect(0, 0, SCREEN_WIDTH, 16, WHITE); display.setTextColor(BLACK, WHITE); display.setTextSize(SM_FONT_SIZE); display.setCursor(0, 4); // Se sono in modalità impostazione if (modEdit) { display.print("Imposta temperatura"); } else { // Altrimenti stampo riepilogo temperatura impostata display.print("Temp. impostata"); String tempToDisplay = ""; // Se il valore è inferiore a 10 aggiungo lo zero iniziale per avere // sempre una cifra a due caratteri if (setTemperature < 10) { tempToDisplay = "0"; tempToDisplay.concat(setTemperature); } else { tempToDisplay = ""; tempToDisplay.concat(setTemperature); } display.setCursor(SCREEN_WIDTH - ((SM_FONT_SIZE * CHAR_SPACE) * 2), 4); display.print(tempToDisplay); } } /* Disegna il pallino dei gradi */ void drawPallinoGradi() { unsigned int pallinoPositionX = ((LG_FONT_SIZE * CHAR_SPACE) * 2) + 5; display.drawCircle(pallinoPositionX, SCREEN_HEIGHT / 3, 5, WHITE); display.fillCircle(pallinoPositionX, SCREEN_HEIGHT / 3, 5, WHITE); display.drawCircle(pallinoPositionX, SCREEN_HEIGHT / 3, 3, BLACK); display.fillCircle(pallinoPositionX, SCREEN_HEIGHT / 3, 3, BLACK); } /* Stampa i gradi rilevati dal sensore */ void drawTemp() { display.setTextColor(WHITE); display.setTextSize(LG_FONT_SIZE); display.setCursor(0, SCREEN_HEIGHT / 3); String tempToDisplay = ""; // Se il valore è inferiore a 10 aggiungo lo zero iniziale per avere // sempre una cifra a due caratteri if (sensorTemp < 10) { tempToDisplay = "0"; tempToDisplay.concat(sensorTemp); } else if (sensorTemp > 99) { // Se il valore dovesse superare le due cifre, rimpicciolisco il carattere display.setTextSize(MD_FONT_SIZE); tempToDisplay = " "; tempToDisplay.concat(sensorTemp); } else { tempToDisplay = ""; tempToDisplay.concat(sensorTemp); } display.print(tempToDisplay); // Disegna pallino dei gradi drawPallinoGradi(); } /* Stampa i gradi da settare quando si è in modalità impostazione */ void drawEditTemp() { display.setTextColor(WHITE); display.setTextSize(LG_FONT_SIZE); display.setCursor(0, SCREEN_HEIGHT / 3); String tempToDisplay = ""; // Se il valore è inferiore a 10 aggiungo lo zero iniziale per avere // una cifra a due caratteri if (setTemperature < 10) { tempToDisplay = "0"; tempToDisplay.concat(setTemperature); } else if (setTemperature > 99) { // Se il valore dovesse superare le due cifre, rimpicciolisco il carattere display.setTextSize(MD_FONT_SIZE); tempToDisplay = " "; tempToDisplay.concat(setTemperature); } else { tempToDisplay = ""; tempToDisplay.concat(setTemperature); } display.print(tempToDisplay); // Disegna pallino dei gradi drawPallinoGradi(); } /* Disegna l'interruttore. Riempimento e posizione cambiano in base al parametro "on". */ void drawToggleSwitch(bool on) { int toggleX = (SCREEN_WIDTH - TOGGLE_WIDTH - PADDING); int toggleY = (SCREEN_HEIGHT - TOGGLE_HEIGHT - PADDING); display.setTextColor(WHITE); display.setTextSize(MD_FONT_SIZE); if (on) { display.setCursor( toggleX + ((CHAR_SPACE * MD_FONT_SIZE) / 2) + (TOGGLE_RADIUS / 2), toggleY - (CHAR_HEIGHT * MD_FONT_SIZE)); display.print("ON"); display.fillRoundRect(toggleX, toggleY, TOGGLE_WIDTH, TOGGLE_HEIGHT, TOGGLE_RADIUS, WHITE); display.fillCircle((toggleX + TOGGLE_WIDTH) - (TOGGLE_CIRCLE_RADIUS + 2), toggleY + (TOGGLE_HEIGHT / 2), TOGGLE_CIRCLE_RADIUS, BLACK); } else { display.setCursor(toggleX + (TOGGLE_RADIUS / 2), toggleY - (CHAR_HEIGHT * MD_FONT_SIZE)); display.print("OFF"); display.drawRoundRect(toggleX, toggleY, TOGGLE_WIDTH, TOGGLE_HEIGHT, TOGGLE_RADIUS, WHITE); display.fillCircle(toggleX + TOGGLE_CIRCLE_RADIUS + 2, toggleY + (TOGGLE_HEIGHT / 2), TOGGLE_CIRCLE_RADIUS, WHITE); } } /* Disegna un'icona da mostrare quando siamo in modalità impostazione. Dimensioni e posizionamento rimangono le stesse dell'icona interruttore */ void drawEditIcon() { int toggleX = (SCREEN_WIDTH - TOGGLE_WIDTH - PADDING); int toggleY = (SCREEN_HEIGHT - TOGGLE_HEIGHT - PADDING); display.setTextColor(WHITE); display.setTextSize(MD_FONT_SIZE); display.setCursor(toggleX + (TOGGLE_RADIUS / 2), toggleY - (CHAR_HEIGHT * MD_FONT_SIZE)); display.print("MOD"); display.drawRoundRect(toggleX, toggleY, TOGGLE_WIDTH, TOGGLE_HEIGHT, TOGGLE_RADIUS, WHITE); display.setTextSize(SM_FONT_SIZE); display.setCursor(toggleX + 8, toggleY + (CHAR_HEIGHT * SM_FONT_SIZE) - 2); display.print("TEMP"); } /* Disegna la scritta ERR! */ void drawError() { display.setTextColor(WHITE); display.setTextSize(LG_FONT_SIZE); display.setCursor(0, SCREEN_HEIGHT / 3); display.print("ERR!"); } /* Disegna i trattini a fondo display. I trattini rappresentano la parte decimale della lettura del sensore. Supponendo un valore di 37,38 gradi il numero dei trattini sarà 4 ovvero la parte decimale .38 arrotondata a 4. */ void drawDashes() { const int N_DASHES = 9; // Numero di trattini massimo per calcolarne la larghezza const int DASH_GAP = 4; // Spazio tra un trattino e l'altro const int DASH_H = 3; // Altezza di ogni trattino const int DASH_Y = SCREEN_HEIGHT - DASH_H; // Posizione Y di ogni trattino (fondo display - l'altezza del trattino) const int DASH_W = (SCREEN_WIDTH / N_DASHES) - DASH_GAP; // Larghezza di ogni trattino tenendo conto dello spazio tra uno e l'altro //Disegna i trattini for (int i = 0; i < sensorTempDecimals; i++) { int DASH_X = i * (DASH_W + DASH_GAP); // Calcola la posizione x di ogni trattino da disegnare display.fillRect(DASH_X, DASH_Y, DASH_W, DASH_H, WHITE); } } /* Raggruppa tutte le funzioni che disegnano sul display */ void printData() { // Pulisco buffer del display display.clearDisplay(); drawHeader(); if (error) { drawError(); } else { if (modEdit) { drawEditTemp(); drawEditIcon(); } else { drawTemp(); drawToggleSwitch(relayStatus); drawDashes(); } } // Mando in stampa i dati sul display display.display(); } void setup() { // Disabilito Watchdog per riabilitarlo dopo il setup wdt_disable(); // inizializzo variabili sensorTemp = 0; sensorTempDecimals = 0; prevClk = 0; error = false; relayStatus = false; modEdit = false; // Forzerà lettura già al primo ciclo di loop lastTempReadMills = -(REFRESH_RATE * 1000); // Leggo il valore della temperatura precedentemente impostata sulla EEPROM setTemperature = EEPROM.read(EEPROM_ADDRESS); // Se il valore all'indirizzo EEPROM_ADDRESS è uguale a 255 vuol dire che non // è mai stato settato if (setTemperature == 255) { // Quindi metto il sistema in modalità impostazione e setto momentaneamente // la temperatura al minimo modEdit = true; setTemperature = MIN_TEMP; } // setto il pin del relé come output pinMode(PIN_RELAY, OUTPUT); // Setto lo status iniziale del relè (spento di default) digitalWrite(PIN_RELAY, relayStatus); // Setto i pin dell'encoder come input e input_pullup per lo switch pinMode(PIN_ENCODER_CLK, INPUT); pinMode(PIN_ENCODER_DT, INPUT); pinMode(PIN_ENCODER_SW, INPUT_PULLUP); // Eseguo prima lettura del valore del pin Clk dell'encoder prevClk = digitalRead(PIN_ENCODER_CLK); /* Per essere sicuro che vengano intercettati tutti i cambiamenti di stato dell'encoder sfrutto gli interrupt di Arduino. */ attachInterrupt(digitalPinToInterrupt(PIN_ENCODER_CLK), checkEncoder, CHANGE); // Setto switch su istanza debounce btnMode.attach(PIN_ENCODER_SW); btnMode.interval(DEBOUNCE_DELAY); // Provo ad inizializzare il display all'indirizzo 0x3C (il tuo potrebbe // essere diverso, 0x3D per esempio) while (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Fin quando il display non è pronto rimango nel while } /* Stampo a video il messaggio iniziale. Se il messaggio non scompare per lasciare posto al resto dei dati, è sintomo di qualche problema. */ printInitScreen(); /* Attendi che il sensore sia pronto. Se il sensore da problemi rimarremo nel loop e il display sarà bloccato sulla scritta iniziale. Ciò ci farà intuire che qualcosa non va con l'inizializzazione del sensore */ while (!sensor.begin()) { // Fin quando il sensore non è pronto rimango nel while } // Imposto controllo di errore su sensore sensor.setConfig(DS18B20_CRC); // Abilito il Watchdog. // Il sistema non potrà rimanere in blocco per più di due secondi wdt_enable(WDTO_2S); } // Lavora! void loop() { // Controlla pressione dello switch sull'encoder checkSwitch(); // Leggi la temperatura readTemp(); // Stampa dati sul display printData(); // Gestisci relé controlRelay(); // Resetta Watchdog wdt_reset(); }
Sembra molto più complesso di quello che è ma sono più i commenti che il resto. Naturalmente come sempre se qualcosa non ti è chiara puoi utilizzare i commenti a fondo pagina.
Conclusioni
In conclusione, il termostato è pronto e funzionante ma non l’ho ancora testato sullo scaldabagno perché il modulo relè che ho a casa supporta fino a 10A mentre quello originale ne riporta 15A. Non so ancora come finirà la storia, ma di sicuro ho imparato e messo assieme un sacco di cose, dalla gestione grafica del display al watchdog al salvataggio dei dati in memoria.
Nato come progetto di un termostato da scaldabagno, in realtà potrà essere applicato da qualsiasi parte. In futuro se recupererò altri tipi di sensore proverò a riscrivere il codice secondo le loro librerie.
Come sempre spero possa esserti utile…
Video
A fine pagina trovi il link per scaricare il pacchetto con codici e schemi di collegamento. Come sempre ti ricordo che acquistando prodotti Amazon passando attraverso i link del mio sito, io percepisco una piccola commissione (parliamo di centesimi) in buoni regalo. Questi buoni sommati alle eventuali donazioni PayPal, servono a mantenere attivo il sito web e ad acquistare nuovi componenti.
Commentati Recentemente