Proiectul constă în construirea unei stații meteorologice care colectează informații din mediul înconjurător și le afișează pe ecran. Utilizatorul poate interacționa cu stația prin intermediul unor butoane, putând accesa, astfel, mai multe ecrane care oferă diverse funcționalități. Atunci când stația este folosită, va răspunde rapid la comenzile utilizatorului, iar când nu este folosită va consuma cât mai puțină energie prin reducerea comunicațiilor cu senzorii și display-ul.
Funcționalități oferite:
Am decis să realizez acest proiect deoarece am deja o stație meteorologică comercială și aș vrea să văd care sunt provocările construirii unui asemenea sistem. În plus, pe viitor, vreau să pot adăuga funcționalități suplimentare la acest proiect, precum un modul bluetooth pentru sincronizarea cu un telefon sau o altă placă care să colecteze informații din exteriorul locuintei.
Toate componentele care pot comunica prin I2C (senzorul de presiune, ceasul și display-ul) vor fi legate prin intermediul acestei magistrale. Senzorul DHT11 folosește un protocol one-wire și va comunica umiditatea plăcuței printr-o conexiune individuală. Senzorul BMP280 va fi folosit pentru presiune și temperatură, iar DHT11 doar pentru umiditate, deoarece este mai lent și mai imprecis decât BMP280. Pentru a reduce numărul de componente, butoanele vor fi legate la arduino folosind rezistențele interne de pe pini.
Pentru a menține timpul sincronizat cu plăcuța și pentru a evita comunicația excesivă cu ceasul extern, acesta va mai avea o conexiune cu placa care va fi folosită pentru a genera o întrerupere o dată pe secundă. Deși plăcuța poate genera întreruperi pe baza ceasurilor interne, acestea nu sunt la fel de precise ca cele generate de cel extern. Astfel, plăcuța va incrementa intern minutul și secunda și va prelua data de la ceasul extern doar o dată pe oră.
În albastru deschis sunt reprezentate modulele hardware externe, în albastru deschis cele hardware interne, iar în verde modulele software.
Descriere sumară a modulelor software:
main
: Modulul principal care inițializează celelalte module și în care se găsește funcția loopstation
: Se ocupă de citirea și stocarea datelor de la senzorii de mediu, aici sunt salvate și o parte din setăributton
: Se ocupă de citirea butoanelor. Folosind întreruperi, semnalează modulului main
dacă a fost apăsat un buton și se folosește de unul din ceasurile arduino-ului pentru a face debouncingmenu
: Conține implementarea pentru toate ecranele disponibile. Fiecare ecran implementează câte una din funcționalitățile descrise mai sus și interacționează cu alte module atunci când este nevoietime
: Realizează sincronizarea cu ceasul extern și controlează celelalte ceasuri interne ale microcontroller-uluialarm
: Se folosește de time
pentru a implementa metodele necesare pentru a configura și a declanșa o alarmăremote
: Interacționează cu receptorul și emițătorul infraroșu, salvează și retrimite la cerere un cod primit de la o telecomandăListă de piese:
Schemă:
În urma realizării legăturilor între componente:
Arduino IDE v1.18
Pentru biblioteca LiquidCrystal_I2C, am implementat deplasarea cursorului la stânga și la dreapta pentru că aceste funcționalități lipseau din biblioteca originală.
Modulul conține mai multe funcții și variabile globale care ajută comunicarea și sincronizarea cu ceasul extern DS3231. Printre altele, mai există și funcționalități pentru activarea și dezactivarea timer-elor interne, de exemplu:
void enableTimer2() { PRR &= ~(1 << PRTIM2); } void powerDownTimer2() { TCCR2B = 0; PRR |= (1 << PRTIM2); }
Similar, există și o serie de funcții pentru timer-ul 0, doar că pentru acest timer, se păstrează și se restaurează variabila TCCR0B, care este necesară pentru functiile millis() și delay(), folosite de biblioteca IRremote. Timer-ul 2 este folosit tot de biblioteca IRremote, dar aceasta îl configurează cu parametrii corespunzători, deci nu este nevoie să se salveze registrele de configurare pentru acesta.
Pentru a reduce comunicările peste I2C cu ceasul extern, modulul păstrează minutul, secunda și data curentă în memoria locală, și comunică cu ceasul o dată pe oră pentru a resincroniza aceste variabile. Funcția syncDate() este apelată de modulul main
de fiecare dată când trece o secundă.
bool syncDate() { bool ret = false; if (seconds == 60) { seconds = 0; minutes++; if (minutes == 60) { // rtc este un obiect de tipul RTC_DS3231 si reprezinta ceasul extern // metoda now() preia data intreaga de la ceas currentDate = rtc.now(); minutes = currentDate.minute(); seconds = currentDate.second(); ret = true; } } return ret; }
Secunda este incrementată odată cu apelarea întreruperii externe:
ISR(INT0_vect) { seconds++; readSensors++; alarm = true; }
Variabila alarm indică faptul că ceasul a declanșat întreruperea externă, deci a trecut o secundă. Modulul main
va reseta acest
indicator odată ce începe procesările care trebuie făcute în fiecare secundă.
void clearTickAlarm() { // Se semnalează ceasului să dezactiveze alarma, // ceea ce va face ca semnalul de pe pin să revină // pe valoarea logică 1 rtc.clearAlarm(1); alarm = 0; }
Variabila readSensors este folosită pentru a putea mări intervalul de timp la care se citesc senzorii externi fără a fi nevoie de funcționalitate în plus în modulul main
. Astfel, modulul main
poate afla dacă trebuie să citească senzorii externi folosind următoarea funcție:
bool shouldReadSensors() { // Citirea se face cand variabila are valoarea 1 altfel statia ar afisa // mult timp date incorecte atunci cand este pornita if (readSensors == 1) return true; if (readSensors == SENSOR_READ_TIME - 1) readSensors = 0; return false; }
Realizează comunicarea cu senzorul de temperatură și presiune BMP280 și cu senzorul de umiditate DHT11. Aici sunt conținute o serie de array-uri globale pentru a păstra extremele pe ore, precum și setările stației: unitatea de măsură pentru temperatură, unitatea de măsură pentru presiune și o corecție pentru presiune care este adunată atunci când se face afișarea presiunii. Funcțiile implementate aici citesc senzorii pentru a afla parametrii mediului extern, actualizează array-urile globale, extrag din aceste array-uri parametrii minimi sau maximi pe ore sau pe intreaga zi și modifică setările stației.
Deoarece se memorează foarte multe date, pentru a reduce dimensiunea ocupată de acestea, valorile citite din mediu sunt păstrate în variabile de tip int16_t în loc de float. Pentru a putea păstra variabilele în acest mod a fost nevoie să modific bibliotecile DHT și Adafruit_BMP280 ca să întoarcă tipuri întregi în loc de float-uri. Astfel, temperatura păstrată într-o variabilă este temperatura reală in grade celsius cu două zecimale înmulțită apoi cu 100, umiditatea este cea reală înmulțită cu 10, iar presiunea este cea reală în pascali din care se scade 90000. Se scade 90000 din presiunea citită de la senzor, pentru că aceasta este valoarea minimă pe care senzorul o poate citi, conform datasheet-ului, și pentru că aceasta modificare permite stocarea presiunii într-o variabilă de tip int16_t în loc de una int32_t. Mai mult, modificarea unităților de măsură nu afectează semnificația acestor variabile, ci doar modul în care se afișează.
Astfel, aceste modificări permit afișarea temperaturii, de exemplu, folosind o implementare proprie fără a mai fi nevoie de anumite flag-uri de compilare pentru a include o variantă mai completă a funcției snprintf, care ar trebui să poată afișa și float-uri 1) 2).
void printTemperature(char *buffer, temperature_t temperature) { int8_t significant; int8_t decimal; // Intai se face conversia if (getTemperatureUnit() == TEMPERATURE_F) temperature = (temperature * 9) / 5 + 3200; significant = temperature / 100; decimal = temperature % 100; // Se rotunjeste la prima zecimala if (decimal < 0) decimal = -decimal; if (decimal % 10 >= 5) decimal += 10; if (decimal >= 100) { if (significant < 0) significant--; else significant++; decimal = 0; } decimal = decimal / 10; // Unitatea de masura se afiseaza impreuna cu temperatura const char* temperatureString = getTemperatureUnit() == TEMPERATURE_C ? PSTR("%3hd.%.1hdC") : PSTR("%3hd.%.1hdF"); snprintf_P(buffer, 7, temperatureString, significant, decimal); }
Similar, se aplică aceeași procedură și pentru presiune. În schimb, umiditatea este afișată fără zecimale datorită lipsei de precizie a senzorului DHT11.
Oferă funcțiile necesare pentru a semnala dacă un buton a fost apăsat și pentru a afla care din butoane este apăsat la un moment dat. Întreruperea folosită pentru butoane este următoarea:
ISR(PCINT2_vect) { disableButtons(); // PCMSK2 = 0 checkButton = true; }
Pentru a păstra întreruperea cât mai scurtă, aici doar se semnalează dacă un buton a fost apăsat, stația poate detecta doar o apăsare de buton la un moment dat, prioritatea butoanelor fiind determinată în funcția care citește starea acestora:
button_t getButton() { button_t button = BUTTON_NONE; if (!(PIND & (1 << DDD3))) { button = BUTTON_PREV; } else if (!(PIND & (1 << DDD4))) { button = BUTTON_NEXT; } else if (!(PIND & (1 << DDD5))) { button = BUTTON_CANCEL; } else if (!(PIND & (1 << DDD6))) { button = BUTTON_OK; } else if (!(PIND & (1 << DDD7))) { button = BUTTON_EXTRA; } return button; }
Pentru a face debouncing, modulul button
foloseste exclusiv timer-ul 1. Timer-ul este configurat în functia de initializare a modulului pentru a genera o întrerupere odată la 200 de ms. Cum s-a văzut în întreruperea de mai sus, odată ce se apasă un buton, se dezactivează complet celelalte butoane până când sunt reactivate explicit. Modulul main va apela funcția blockButtons() pentru a dezactiva temporar butoanele, întreruperea timer-ului 1 le va reactiva după ce expiră timpul alocat.
void blockButtons() { disableButtons(); // PCMSK2 = BUTTON_MASK enableButtonTimer(); // reseteaza contorul timer-ului si activeaza prescaler-ul } ISR(TIMER1_COMPA_vect) { disableButtonTimer(); enableButtons(); // Se semnaleaza buclei principale ca butoanele trebuie verificate din nou // Astfel, se realizeaza o repetare daca butonul este tinut apasat checkButton = true; }
Modulul remote conține funcțiile necesare pentru a stoca și a retransmite un cod infraroșu primit de la o telecomandă. Biblioteca folosită pentru acestea are nevoie de timer-ul 2 pentru receptie și timer-ul 0 pentru transmitere. Din moment ce e de asteptat ca recepția și trasmiterea să nu se facă foarte des, alimentarea pentru cele două timere este oprită odată ce nu mai este nevoie de acestea.
Deoarece această bibliotecă este foarte complexă, au fost folosite mai multe define-uri care configurează biblioteca înspre reducerea spațiului ocupat de aceasta:
#define RAW_BUFFER_LENGTH 100 #define IR_SEND_PIN 11 #define EXCLUDE_EXOTIC_PROTOCOLS #define NO_LED_FEEDBACK_CODE #define NO_DECODER
Cea mai importantă configurare de mai sus este NO_DECODER, care exclude codul care ar fi folosit pentru decodificarea semnalelor primite. Din moment ce stația doar retransmite un cod primit, nu este nevoie să-l și decodifice. Astfel, alarma setată de utilizator poate să reproducă orice cod primit, atâta timp cât există spațiu suficient ca să-l memoreze.
Biblioteca include deja două obiecte pentru transmitere si recepție și, pentru a economisi spațiu, le-am folosit pe acestea. Opțiunea IR_SEND_PIN reduce mai mult codul pentru transmițător prin excluderea unui constructor care nu ar fi fost folosit.
Oferă sub forma clasei AlarmManager o implementare pentru o alarmă care poate să sune în anumite zile ale săptămânii și care poate să revină după un număr fix de minute. Astfel, alarma are echivalentul unei funcții de snooze, care, în realitate, va fi folosită mai departe în modulul menu
pentru a putea opri un dispozitiv pornit.
Metoda principală a acestei clase, care trebuie apelată pentru a verifica dacă alarma sună, este următoarea:
bool AlarmManager::check() { // Pentru a evita situatii in care metoda este apelata // de mai multe ori in acelasi minut, posibil in aceasi // secunda, se impune ca metoda sa intoarca true // cel mult odata pe minut if (getMinutes() == lastMinuteChecked) return false; lastMinuteChecked = getMinutes(); if (!active) return false; if (powerOffTimerActive) { powerOffTimerCounter--; if (powerOffTimerCounter <= 0) { powerOffTimerActive = false; return true; } } // getDate() si getMinutes() sunt implementate in modulul time const DateTime& currentDate = getDate(); if (dayEnabled(currentDate.dayOfTheWeek()) && currentDate.hour() == hour && getMinutes() == minute) { // Daca timer-ul de oprire e activat, atunci se incepe numaratoarea // Daca valoarea timer-ului este 0, atunci se considera ca este dezactivat if (powerOffTimerValue > 0) { powerOffTimerCounter = powerOffTimerValue; powerOffTimerActive = true; } return true; } return false; }
Metoda de mai sus trebuie apelată o dată pe minut, altfel este posibil să se piardă alarma. Odată ce sună prima oară, variabila powerOffTimerActive este pusă pe true pentru a începe numărătoarea inversă a timer-ului de oprire.
Mai mult, pentru a evita un comportament impredictibil al alarmei atunci când se modifică data sau se dezactivează alarma, timer-ul de oprire pentru dispozitiv este dezactivat:
void AlarmManager::disable() { active = false; powerOffTimerActive = false; } void AlarmManager::setTime(const uint8_t newHour, const uint8_t newMinute) { powerOffTimerActive = false; hour = newHour; minute = newMinute; }
Conține implementările pentru toate meniurile afișate utilizatorului, precum și instanțe ale acestor meniuri care vor fi folosite indirect de modulul principal prin intermediul unor funcții care apelează metodele meniului care este afișat pe ecran și printr-o funcție care modifică meniul care trebuie afișat. Tot aici este declarat și obiectul pentru interacțiunea cu display-ul. Meniurile interacționează cu aproape toate celelalte module, dar, la bază, acestea moștenesc următoarea interfață:
class Menu { public: // Functia care se apeleaza atunci cand un meniu urmeaza // sa fie afisat pe ecran virtual void load(); // Se apeleaza dupa ce meniul a fost afisat prima oara // Conventia este ca meniul sa afiseze pe ecran // doar componentele care nu se actualizeaza frecvent virtual void draw() = 0; // Metoda care este apelata o data pe secunda, dupa celelalte // doua metode de mai sus, aici meniurile trebuie sa afiseze // doar componentele care se pot schimba virtual void tick() = 0; virtual void button(button_t button); };
În general, un meniu este o mașină de stări care ia decizii în funcție de timp sau de butonul care a fost apăsat. Meniurile implementate sunt: MainMenu, MinMaxMenu, AlarmMenu, SettingsMenu, GraphMenu și ErrorMenu.
Dintre acestea, ErrorMenu este tratat diferit si va fi folosit de modulul main
pentru a afișa o eroare în caz că este nevoie. GraphMenu folosește o serie de caractere speciale care sunt încărcate în memoria display-ului atunci când se apelează metoda de inițializare pentru display. GraphMenu afișează graficul pe baza presiunii maxime și minime înregistrate în ultimele 24 de ore, fără să existe o unitate de măsură, astfel se va putea observa modificarea presiunii în timp.
AlarmMenu incorporează un AlarmManager și folosește modulul remote
pentru a recepționa un cod care este memorat tot în acest meniu. De asemenea, AlarmMenu mai are o metodă care este folosită pentru a retransmite codul stocat în caz că alarma setată este activată și sună la momentul în care această metodă este apelată. Modulul main
nu are cunoștiință că ar exista mai multe alarme, așa că apelează o funcție a acestui modul care verifică alarmele ambelor meniuri.
Tot în acest modul, funcția switchCurrentMenu(), care încarcă meniul următor, folosește o serie de variabile globale pentru a reveni întotdeauna la meniul principal după un interval de timp în care nu au mai fost apăsate butoane. Astfel, stația nu va afișa mult timp meniuri care necesită mai multe calcule sau comunicații cu alte componente. În funcție de butonul apăsat, un meniu poate să semnaleze că trebuie afișat alt meniu. Odată ce plăcuța se trezește în urma apăsării butonului, modulul main
va apela switchCurrentMenu() pentru a realiza înlocuirea. De asemenea, după ce se revine la meniul principal, backlight-ul lcd-ului va rămâne aprins doar pentru o perioadă scurtă de timp, pentru a economisi energie.
Modulul principal care reunește celelalte module. Pe lângă funcțiile arduino setup() și loop(), aici mai sunt implementate alte funcții care nu puteau să aparțină altui modul, de exemplu disableOther(), care dezactivează anumite componente ale microcontroller-ului care nu sunt folosite sau powerDownUnused() care taie alimentarea din părți ale microcontroller-ului care nu sunt folosite.
Dintre aceste funcții, funcția sleep() pune microcontroller-ul într-o stare de sleep, alegând starea de sleep în funcție de activitatea microcontroller-ului, mai exact va pune microcontroller-ul în idle dacă urmează să se reactiveze butoanele sau dacă timer-ul 2 este activat, adică meniul AlarmMenu așteaptă recepționarea unui cod infraroșu. Dacă nu există activitate, sleep() va pune plăcuța în modul power down.
Sumar, pentru a inițializa complet toate modulele și a pune stația în funcțiune, metoda setup() va face următoarele:
station
și apoi apelează metoda initEnvironment() din același modul pentru a inițializa valorile minime și maxime înregistrate în ultimele 24 de oretime
button
cu metoda setupButtons() și modulul menu
cu metoda initMenus()Odată ce stația este inițializată, în buclă se vor face următoarele:
Pentru funcția loop() am ales să realizez toate operațiile în ordinea de mai sus pentru a prioritiza citirea senzorilor, alarmele și, în special, păstrarea sincronizării cu ceasul extern. Deși există posibilitatea ca anumite apăsări de butoane să fie pierdute dacă durează prea mult ca stația să ajungă la citirea stării butoanelor, păstrând această ordine a operațiilor se asigură că stația nu se va desincroniza, nu va rata citiri ale senzorilor și nici nu va rămâne blocată. Tot pentru a păstra sincronizarea, nu se intră în sleep dacă a ceasul extern a declanșat întreruperea altfel se poate ajunge la situații în care stația se blochează dacă utilizatorul apasă un buton în același timp cu rularea întreruperii ceasului extern.
Pentru a desena graficul, valorile pentru presiune au fost inițializate în prealabil folosind un define configurabil în cod.
În urma acestui proiect am învățat multe despre ecosistemul arduino si despre programarea microcontroller-elor.
Astfel, pot spune că am tras următoarele concluzii:
Biblioteci 3rd-party:
Resurse folosite: