Afisaje multiple LCD

Autor: Mustatea Radu-Ioan

Grupa: 333CA

Introducere

  • Prin intermediul a mai multi senzori, vor putea fi afisate pe un ecran LCD mai multe date, anume temperatura, umiditatea, presiunea si calitatea aerului (masurata prin PPM)
  • De asemenea, prin folosirea a unuia sau a mai multor butoane, se va putea comuta pe ecran si pentru afisarea unor sumarizari ale datelor preluate la un interval fix (initial se planuia realizarea unor grafice care sa ilustreze istoricul acestor date, din pacate constrangerile de memorie ale placutei nu au permis acest lucru)
  • Prin intermediul unui modul bluetooth, se va permite si accesul exterior la aceste date
  • Proiectul este util din punct de vedere al posibilitatilor de monitorizare (de exemplu la nivelul unei camere)

Descriere generală

  1. Schema bloc

Hardware Design

Componente:

  • Placuta Arduino UNO
  • Senzor RTC DS3231
  • Senzor BME280 de temperatura, presiune si umiditate
  • Senzor MQ-135 pentru masurarea calitatii aerului
  • Ecran LCD SPI de 2.8 inch, rezolutie 240×320
  • Modul bluetooth HC05
  • 6 rezistente de 2.2K, 6 rezistente de 5.1K
  • 3 butoane
  • Fire tata-tata, tata-mama
  • Breadboard
  • Varianta initiala presupunea si folosirea unui modul SD pentru salvarea datelor, din pacate biblioteca SD ocupa prea mult spatiu, astfel ca s-a renuntat la acesta

  • Cele 3 butoane sunt implementate clasic, folosindu-se rezistentele de pullup interne ale placutei
  • Senzorii DS3231 si BME280 sunt I2C, deci sunt conectati impreuna cu pinii A4 si A5 de pe Arduino
  • MQ135 trebuie legat la un pin analog (in cazul nostru A0)
  • Cele 3 module de pana acum pot fi conectate direct la 5V (fie au regulatoare interne, fie suporta logica de 5V)
  • Ecranul foloseste SPI, deci putem folosi chiar SPI-ul hardware din Arduino daca folosim pinii corespunzatori
  • Logica ecranului este de 3.3V, de asemenea el nu are regulatoare interne, astfel ca toti pinii cu exceptia VCC si GND necesita reducerea tensiunii de la 5 la 3.3V. Acest lucru se realizeaza prin divizoare de tensiune (folosind rezistente de 2.2K, respectiv 5.1K)
  • Similar si pentru modulul HC05, pe pinul RX este redusa la fel tensiunea de la 5V la 3.3V, acesta functionand tot cu logica de 3.3V

Software Design

  • Se folosesc bibliotecile Ucglib.h (pentru ecran), Adafruit_BME280.h (pentru senzorul BME280), RTClib.h (pentru senzorul RTC) si SoftwareSerial (pentru modulul HC05)
  • Pentru afisarea efectiva pe ecran, ne folosim de 4 macrouri de stari, anume NORMAL_STATE, MAX_STATE, MIN_STATE si AVG_STATE. Folosind un buton vom comuta printre acestea. In cazul NORMAL_STATE, vom afisa constant datele de la senzori, iar pentru celelalte stari afisam extremele sau mediile obtinute in urma inregistrarilor din istoric.
  • In cazul NORMAL_STATE, ne confruntam cu problema actualizarii foarte rapide a informatiei. Din cauza ca ecranul nu sterge caracterele scrise anterior atunci cand scriem unele noi in acelasi loc, avem nevoie de o tehnica pentru a masca acest neajuns. Tehnica presupune scrierea aceluiasi text vechi, insa folosind culoarea fundalului (in acest sens folosim string-urile oldTemp, oldBar, oldHum si oldAir), urmand ca apoi sa scriem textul nou. Problema este ca daca facem acest lucru pentru intreg textul, efectul asupra privitorului este unul obositor. Prin urmare, solutia consta in a modifica doar acele caractere care difera fata de iteratia trecuta. Pentru asta insa, trebuie sa ne asiguram ca toate caracterele au aceeasi latime in pixel (in biblioteca Ucglib, acest lucru nu e garantat). Pentru asta putem insa folosi asa numitele fonturi monospace, care asigura aceasta proprietate
  • Pentru istoric, ne folosim de vectori care retin datele preluate de la senzori (din pacate, neavand posibilitatea utilizarii unui modul SD, acest date sunt volatile). Datele sunt preluate din ora in ora. In acest sens, folosim senzorul RTC pentru a determina cand ne aflam la o ora fixa (!rtcMin), moment in care scriem datele preluate atunci in vectori. Tinem minte si cate date avem trecute in vectori (variabila writeCount). Avem doua butoane care functioneaza doar atunci cand nu ne aflam in starea uzuala si care ne permit marirea intervalului pentru care ne intereseaza o sumarizare a datelor, sau scaderea sa (intervalul e dat de numarul de ore). Variabila writeCount joaca deci un rol din punct de vedere al asigurarii ca nu vom incerca de exemplu sa afisam date pentru mai multe ore decat avem stocate
  • Nu in ultimul rand, avem nevoie de o comunicare cu modulul Bluetooth. Pentru asta, deschidem o conexiune seriala, prin care vom transmite datele la modul (la fel cum ar aparea pe ecran in starea normala). Pentru a nu trimite prea multe date pentru a putea fi percepute vizual corespunzator de catre un utilizator uman, trimiterea datelor prin Bluetooth se face doar la intervale regulate de timp (in cazul nostru 5 secunde)

Cod sursa

Cod sursa

#include "Ucglib.h"
#include <Adafruit_BME280.h>
#include "RTClib.h"
#include <SoftwareSerial.h>

#define SPI_SCK 13
#define SPI_MOSI 11
#define SPI_CS 10
#define SPI_CD 9
#define SPI_RES 8

/* Latimea fontului folosit pentru afisarile pe ecran
 * (difera in functie de font si e fixa doar pentru
 * fonturile monospace)
 */
#define FONT_WIDTH 12

/* Adresa senzorului BME280 (trebuie mentionata explicit
 * in constructorul begin, altfel senzorul nu va functiona
 */
#define BME_ADDRESS 0X76

/*
 * Culorile pentru fundalul, respectiv scrisul ecranului
 */
#define BACKGROUND_RED 0
#define BACKGROUND_GREEN 0
#define BACKGROUND_BLUE 0

#define WRITE_RED 255
#define WRITE_GREEN 255
#define WRITE_BLUE 255

/*
 * Valoare folosita pentru a plasa afisarile pe ecran
 * (nu vrem sa afisam direct in colturi din motive de estetica)
 */
#define OFFSET 20

#define AIR_PIN 0

/*
 * Valori pentru plasarea datelor preluate de la senzori
 * pe ecran
 */
#define TEMP_X 20
#define BAR_X 40
#define HUM_X 60
#define AIR_X 80

#define TX_PIN 3
#define RX_PIN 2

/*
 * Durata de timp intre doua transmii prin Bluetooth
 */
#define BLUE_TIME 5000

/*
 * Pinii la care sunt conectati butoanele
 */
#define MODE_BUTTON 4
#define INC_BUTTON 6
#define DEC_BUTTON 5

/*
 * Pentru cate ore retinem date preluate de la senzori
 */
#define HIST_COUNT 40

/*
 * Starile prin care poate cicla afisarea pe ecran
 */
#define NORMAL_STATE 0
#define MAX_STATE 1
#define MIN_STATE 2
#define AVG_STATE 3

Ucglib_ILI9341_18x240x320_HWSPI ucg(SPI_CD, SPI_CS, SPI_RES);

Adafruit_BME280 bme;

String oldTemp = "", oldHum = "", oldBar = "", oldAir = "";

RTC_DS3231 rtc;

SoftwareSerial Blue(TX_PIN, RX_PIN);
unsigned long transmissionTime; /* Variabila folosita pentru a determina cand sa trimitem date prin Bluetooth */

float temp[HIST_COUNT], bar[HIST_COUNT], hum[HIST_COUNT];
int air[HIST_COUNT];

/*
 * Determina daca suntem la momentul in care sa scriem in istoric
 */
bool writeToHistory = false;

/*
 * Ne spune cate scrieri avem in istoric
 */
short int writeCount = 0;
short int state = NORMAL_STATE, oldState = NORMAL_STATE;

/*
 * Stari pentru butoane plus pentru cate ore se va face afisarea pe ecran in cazul in care nu ne aflam in
 * starea uzuala
 */
bool oldModeState = HIGH, oldIncState = HIGH, oldDecState = HIGH;
short int hourCount = 1;

void setup() {
  Serial.begin(9600);
  Blue.begin(9600);

  pinMode(MODE_BUTTON, INPUT_PULLUP);
  pinMode(INC_BUTTON, INPUT_PULLUP);
  pinMode(DEC_BUTTON, INPUT_PULLUP);

  rtc.begin();
  rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));

  bme.begin(BME_ADDRESS);

  ucg.begin(UCG_FONT_MODE_SOLID);
  ucg.clearScreen();

  /*
   * Alegerea fontului s-a facut in primul rand monospace (m-ul din nume) pentru ca orice caracter sa aiba
   * aceeasi latime pe ecran. Acest lucru ne permite ca atunci cand modificam un sir pe ecran sa putem sterge
   * si rescrie doar caracterele care s-au schimbat, deoarece stim sigur pozitia la care se afla
   * 
   * Afisarea e rotita pentru a se afisa pe orizontala
   */
  ucg.setFont(ucg_font_courB14_mf);
  ucg.setRotate90();

  transmissionTime = millis();
}

/*
 * Functie de actualizare a afisarii pe ecran in cazul starii uzuale. Stiind care este
 * latimea fontului, ne putem pozitiona acolo unde caracterele s-au modificat
 * Avem 3 cazuri:
 * - fie avem de schimbat o litera din sirul vechi cu una din sirul nou
 * - fie scriem o parte din sirul nou acolo unde inainte nu aveam de scris nimic
 * - fie trebuie sa stergem partea din sirul vechi aflata dincolo de sirul nou
 * 
 * De asemenea, aici verificam si daca e momentul sa trimitem date prin Bluetooth
 */
void updateString(String& oldString, String& newString, unsigned char y, bool printBlue) {
  char i, oldLength = oldString.length(), newLength = newString.length();

  if (state == NORMAL_STATE) {
    for (i = 0; i < oldLength && i < newLength; ++i) {
      if (oldString[i] != newString[i]) {
          ucg.setPrintPos(OFFSET + FONT_WIDTH * i, y);
          ucg.setColor(BACKGROUND_RED, BACKGROUND_GREEN, BACKGROUND_BLUE);
          ucg.print(oldString[i]);
          ucg.setPrintPos(OFFSET + FONT_WIDTH * i, y);
          ucg.setColor(WRITE_RED, WRITE_GREEN, WRITE_BLUE);
          ucg.print(newString[i]);
      }
    }
  
    ucg.setPrintPos(OFFSET + FONT_WIDTH * oldLength, y);
    ucg.setColor(WRITE_RED, WRITE_GREEN, WRITE_BLUE);
    if (newLength > oldLength) {
      ucg.print(newString.substring(oldLength));
    }
  
    ucg.setPrintPos(OFFSET + FONT_WIDTH * newLength, y);
    ucg.setColor(BACKGROUND_RED, BACKGROUND_GREEN, BACKGROUND_BLUE);
    if (oldLength > newLength) {
      ucg.print(oldString.substring(newLength));
    }
  }

  if (printBlue) {
    Blue.println(newString);
  }

  oldString = state == NORMAL_STATE ? newString : "";
}

/*
 * Functie pentru calcularea dupa caz a valorilor maxime, minime sau medii din istoric
 */
void getRezValues(float &rezTemp, float &rezBar, float &rezHum, int &rezAir) {
  char i;

  ucg.setPrintPos(OFFSET, OFFSET);

  if (state == MAX_STATE) {
    ucg.print((String)"Max Values " + (String)hourCount + (String)" hours");
    rezTemp = -85.0;
    rezBar = 0.0;
    rezHum = 0.0;
    rezAir = 0;
    for (i = writeCount - 1; i >= writeCount - hourCount; --i) {
      rezTemp = temp[i] > rezTemp ? temp[i] : rezTemp;
      rezBar = bar[i] > rezBar ? bar[i] : rezBar;
      rezHum = hum[i] > rezHum ? hum[i] : rezHum;
      rezAir = air[i] > rezAir ? air[i] : rezAir;
    }
  }

  if (state == MIN_STATE) {
    ucg.print((String)"Min Values " + (String)hourCount + (String)" hours");
    rezTemp = 45.0;
    rezBar = 200000.0;
    rezHum = 200.0;
    rezAir = 1000;
    for (i = writeCount - 1; i >= writeCount - hourCount; --i) {
      rezTemp = temp[i] < rezTemp ? temp[i] : rezTemp;
      rezBar = bar[i] < rezBar ? bar[i] : rezBar;
      rezHum = hum[i] < rezHum ? hum[i] : rezHum;
      rezAir = air[i] < rezAir ? air[i] : rezAir;
    }
  }

  if (state == AVG_STATE) {
    ucg.print((String)"Avg Values " + (String)hourCount + (String)" hours");
    rezTemp = 0.0;
    rezBar = 0.0;
    rezHum = 0.0;
    rezAir = 0;
    for (i = writeCount - 1; i >= writeCount - hourCount; --i) {
      rezTemp += temp[i];
      rezBar += bar[i];
      rezHum += hum[i];
      rezAir += air[i];
    }
    rezTemp /= (float)hourCount;
    rezBar /= (float)hourCount;
    rezHum /= (float)hourCount;
    rezAir /= hourCount;
  }
}

/*
 * Aici actualizam scrisul de pe ecran daca ne aflam in starea uzuala sau preluam valorile dorite din istoric
 * daca este necesar (nu ne aflam in starea uzuala), De asemenea, tot aici scriem si noi valori in istoric
 * daca e cazul
 */
void updateData(bool printBlue, bool writeToHistory) {
  float bmeTemp = bme.readTemperature(), bmeBar = bme.readPressure(), bmeHum = bme.readHumidity();
  int airData = analogRead(AIR_PIN);
  float rezTemp, rezBar, rezHum;
  int rezAir;

  updateString(oldTemp, (String)"Temp = " + bmeTemp + (String)" *C", TEMP_X, printBlue);
  updateString(oldBar, (String)"Pressure = " + bmeBar / 100.0F + (String)" hPa", BAR_X, printBlue);
  updateString(oldHum, (String)"Humidity = " + bmeHum + (String)" %", HUM_X, printBlue);
  updateString(oldAir, (String)"Air PPM = " + airData + (String)" PPM", AIR_X, printBlue);

  if (state != NORMAL_STATE) {
    getRezValues(rezTemp, rezBar, rezHum, rezAir);
    ucg.setPrintPos(OFFSET, TEMP_X + OFFSET);
    ucg.print((String)"Temp = " + (String)rezTemp + (String)" *C");
    ucg.setPrintPos(OFFSET, BAR_X + OFFSET);
    ucg.print((String)"Pressure = " + rezBar / 100.0F + (String)" hPa");
    ucg.setPrintPos(OFFSET, HUM_X + OFFSET);
    ucg.print((String)"Humidity = " + rezHum + (String)" %");
    ucg.setPrintPos(OFFSET, AIR_X + OFFSET);
    ucg.print((String)"Air PPM = " + rezAir + (String)" PPM");
  }

  if (writeToHistory) {
    temp[writeCount - 1] = bmeTemp;
    bar[writeCount - 1] = bmeBar;
    hum[writeCount - 1] = bmeHum;
    air[writeCount - 1] = airData;
  }
}

void loop() {
  bool printBlue = abs(millis() - transmissionTime) >= BLUE_TIME;
  DateTime now = rtc.now();
  char i, rtcMin = now.minute();
  bool currentModeState = digitalRead(MODE_BUTTON), currentIncState = digitalRead(INC_BUTTON), currentDecState = digitalRead(DEC_BUTTON);

  /*
   * Daca nu am scris nimic in istoric, nu are rost sa verificam daca butoanele sunt apasate
   */
  if (writeCount) {
    if (currentModeState == LOW && oldModeState == HIGH) {
      if (state == NORMAL_STATE) {
        state = MAX_STATE;
      } else {
        if (state == MAX_STATE) {
          state = MIN_STATE;
        } else {
          if (state == MIN_STATE) {
            state = AVG_STATE;
          } else {
            state = NORMAL_STATE;
          }
        }
      }
      ucg.clearScreen();
    } else {
      if (currentIncState == LOW && oldIncState == HIGH && state != NORMAL_STATE) {
        hourCount += (hourCount < writeCount);
        ucg.clearScreen();
      } else {
        if (currentDecState == LOW && oldDecState == HIGH && state != NORMAL_STATE) {
          hourCount -= (hourCount > 1);
          ucg.clearScreen();
        }
      }
    }

    oldModeState = currentModeState;
    oldIncState = currentIncState;
    oldDecState = currentDecState;

    oldState = state;
  }

  /*
   * Conditia echivalenta pentru salvarea a noi date in istoric
   */
  if (!rtcMin && !writeToHistory) {
    if (writeCount >= HIST_COUNT) {
      for (i = 0; i < HIST_COUNT - 1; ++i) {
        temp[i] = temp[i + 1];
        bar[i] = bar[i + 1];
        hum[i] = hum[i + 1];
        air[i] = air[i + 1];
      }
    }
    writeCount += (writeCount < HIST_COUNT);
    if (state != NORMAL_STATE) {
      ucg.clearScreen();
    }
  }
  updateData(printBlue, !rtcMin && !writeToHistory);

  writeToHistory = !rtcMin;

  transmissionTime = printBlue ? millis() : transmissionTime;

  if (printBlue) {
    Blue.println();
  }
}

Rezultate Obţinute

  • Scopul monitorizarii este atins, atat prin ecranul LCD, cat si printr-un dispozitiv ce se poate conecta la Bluetooth (de asemenea, este necesara si o aplicatie ce permite vizualizarea unui terminal Bluetooth, cum este cea din poza)
  • Dincolo de problemele de memorie, un alt mic impediment este dat de faptul ca verificarea starii butoanelor se face doar intr-un punct in cadrul codului, astfel ca este necesara o apasare un pic mai lunga (aproximativ 0.5-1 secunda) pentru ca acestea sa aiba efect

Demo ecran fizic
Demo Bluetooth

Concluzii

  • Obtinerea unei monitorizari minimale a mai multor parametri referitori la un anumit mediu (de exemplu o camera) este perfect realizabila cu un Arduino Uno si cu senzorii aferenti. Cu toate acestea, limitele placutei sunt atinse atunci cand se doreste o mai mare interactivitate fara a se pierde din functionalitatea efectiva a programului

Download

Bibliografie/Resurse

pm/prj2021/cghenea/radumustatea.txt · Last modified: 2021/06/02 16:33 by radu_ioan.mustatea
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0