Programmering og koding

LCD 16×2 med Arduino: tekst, temperatur og fuktighet

Blå LCD med 16 ganger 2 tegn som viser temperatur og fuktighet ved siden av Arduino UNO R4 og en DHT22-sensor

En LCD 16x2 på Arduino er den enkleste veien til et prosjekt som viser noe uten PC: fire ledninger, et bibliotek, og tallene står på skjermen. Du starter med en hilsen og ender med en klimamåler som viser temperatur, fuktighet, min/maks og en pil for om det blir varmere eller kaldere i rommet.

Tegning av ferdig klimamåler i en boks med LCD 16x2 som viser temperatur og fuktighet og DHT22 utenfor.
Slik kan den ferdige måleren se ut. Dette er en tegning, og tallene er eksempler. Sensoren sitter utenfor boksen fordi elektronikken varmer opp lufta inni.
Kort forklart Lodd I²C-adapteren på baksiden av LCD-en med pinne 1 mot pinne 1. Koble GND til GND, VCC til 5V, SDA til SDA og SCL til SCL på UNO R4 WiFi. Installer biblioteket hd44780, som selv finner adressen. Står det ingenting på skjermen, drei på kontrastskruen før du endrer noe i koden.
Nivå
Nybegynner til viderekommen
Tid
2 til 3 timer i alt
Lodding
Ja, 16 punkter
Du lærer
I²C, egne tegn, sensorer og knapper uten delay

Tre nivåer, tre skjermer

Hvert nivå ender med et program som virker, og du kan stoppe der du vil. Nivå 1 beviser at skjermen og loddingen er i orden. Nivå 2 gir en nyttig måler på samme skjerm. Nivå 3 bytter til fire linjer og lar programmet huske og sammenligne målinger.

Tre LCD-skjermer som viser nivåene i prosjektet: tekst, klimamåler med DHT22 og 20x4 med trend og min/maks.
Dette skal stå på skjermen etter hvert nivå. Innimellom lærer du å tegne æ, ø og å, som skjermen ikke kan fra før.

Delene du trenger

Du trenger også loddebolt og loddetinn. Har du ikke valgt loddeutstyr ennå, hjelper guiden om loddebolt og varmluft. Andre varianter av brett og ledninger finner du blant koblingsbrett og koblingsledninger.

I²C-adapter sett ovenfra med pinnene GND, VCC, SDA og SCL til høyre og LED-jumperen til venstre
Rekkefølgen på adapteren: GND, VCC, SDA, SCL. Den firkantede loddeflaten øverst til høyre er pinne 1.
DHT22-modul sett forfra med merkingen pluss, out og minus ved de tre pinnene
DHT22-modulen er merket + out - sett forfra med pinnene ned. Pluss er VCC og minus er GND. Følg alltid merkingen på ditt eget kort.
Ikke bruk Qwiic-kontakten her. Arduinos pinout merker den som bare 3,3 V, og adapteren skal ha 5 V. Bruk de vanlige pinnene 5V, GND, SDA og SCL på kortet.

Lodd adapteren riktig vei første gang

Adapteren har 16 lange pinner som skal gjennom hullene i LCD-en. Loddingen er vanskelig å gjøre om, så bruk ett minutt på retningen før du varmer opp bolten.

Lodding av I²C-adapter på LCD sett bakfra og i snitt, med pinne 1, pinne 16 og loddepunktet på skjermsiden.
Sett bakfra står pinne 16 og LED-jumperen til venstre, mens pinne 1 og de fire tilkoblingene står til høyre, slik produktbildene av adapteren viser. Pinnene stikkes inn fra baksiden og loddes på skjermsiden.
  1. Trekk ut USB og legg LCD-en med baksiden opp. Finn merkingen 1 og 16 ved hullraden.
  2. Sett adapteren inn fra baksiden, med de fire tilkoblingspinnene på samme side som pinne 1.
  3. Snu alt og hold adapteren på plass med litt tape. Lodd først ett hjørne og kontroller at adapteren står rett.
  4. Lodd de andre 15 pinnene på skjermsiden. Hold bolten på pinnen og flaten samtidig, og pass på den svarte rammen.
  5. Se etter loddebroer mellom nabopinner og matte, klumpete loddepunkter. Sjekk at adapteren ikke ligger inntil komponenter på baksiden av LCD-en.
Trygt arbeid. Lodd i et rom med god ventilasjon, ikke pust inn røyken, og vask hendene etterpå. Koble alltid fra USB før du lodder eller flytter ledninger til 5V og GND.

Nivå 1: få skjermen til å si hei

Nivå 1: nybegynner. Tid: 30 minutter pluss lodding. Du trenger LCD 2x16 med I²C-adapter, UNO R4 WiFi og fire hann-hunn-ledninger.

LCD med Arduino og I2C: hvilke fire ledninger trenger du?

GND, VCC, SDA og SCL er hele koblingen. I²C-adapteren på baksiden tar seg av de seksten pinnene på skjermen, og koden under trenger bare ett bibliotek.

Kobling av LCD 16x2 med I²C-adapter til UNO R4 WiFi med fire ledninger: GND, VCC, SDA og SCL.
Skjermen er tegnet forfra, derfor står adapterens fire pinner til venstre. Hunnenden går på adapteren og hannenden i UNO-kortet.
AdapterUNO R4 WiFiFunksjon
GNDGNDFelles jord
VCC5VStrøm til skjerm og baklys
SDASDA (samme signal som A4)Data
SCLSCL (samme signal som A5)Klokkesignal
  1. Åpne Arduino IDE, velg kortet «Arduino UNO R4 WiFi» og riktig port.
  2. Åpne Library Manager, søk etter hd44780 og installer biblioteket fra Bill Perry.
  3. Lim inn programmet under og last det opp.
  4. Drei den blå kontrastskruen langsomt med en liten skrutrekker til tegnene står skarpt.
// Nivå 1: tekst på LCD 16x2 via PCF8574-adapter (I2C).
#include <Wire.h>
#include <hd44780.h>
#include <hd44780ioClass/hd44780_I2Cexp.h>

hd44780_I2Cexp lcd;  // Ingen adresse: biblioteket finner adapteren selv

void setup() {
  int status = lcd.begin(16, 2);
  if (status) {
    // Fant ingen skjerm: den innebygde LED-en blinker feilkoden.
    hd44780::fatalError(status);
  }

  lcd.setCursor(0, 0);  // Kolonne 0, rad 0
  lcd.print("Hei fra MRSMART");
  lcd.setCursor(0, 1);  // Kolonne 0, rad 1
  lcd.print("LCD 16 x 2");
}

void loop() {
  // Teksten står på skjermen til strømmen går. Her er ingenting å gjøre.
}

hd44780_I2Cexp lcd; står uten adresse. Biblioteket leter selv på bussen og finner både adressen og hvordan adapteren er koblet til LCD-en. lcd.begin(16, 2) gir 0 når alt er i orden. Andre tall betyr feil, og da blinker fatalError den innebygde LED-en i stedet for å fortsette i blinde.

Hva ser du på skjermen, og hva betyr det?

En tekstskjerm gir bedre feilsøking enn du skulle tro, fordi hvert feilbilde peker mot sin egen årsak.

LCD 16x2 med teksten Hei fra MRSMART på første linje og LCD 16 x 2 på andre linje, slik nivå 1 skal se ut.
Slik skal skjermen se ut. «Hei fra MRSMART» er 15 tegn og får plass på en linje.
LCD 16x2 med baklys, men uten synlige tegn, bare en jevn blå flate med svake tegnceller.
Baklys, men ingen tegn: drei kontrastskruen gjennom hele området først. Skjer det fortsatt ingenting, har ikke programmet kommet så langt.
LCD 16x2 med en rad hvite blokker øverst og tom rad under, skjermen har strøm men er ikke initialisert.
Blokker på øverste rad: skjermen har strøm og kontrast, men har aldri fått oppstartskommandoene. Se etter dårlig lodding, feil SDA og SCL eller en feil fra begin().
LCD 16x2 med tilfeldige tegn over begge rader, et typisk tegn på dårlig kontakt i lodding eller ledning.
Tilfeldige tegn: skjermen mottar data, men bitene kommer feil frem. Sjekk loddepunktene og de fire ledningene, og koble USB ut og inn igjen.

Prøv gjerne ut en egen tekst før du laster opp. Feltet under viser hvor mye som får plass, og lager koden du kan lime inn i stedet for de to lcd.print-linjene. Eksemplet inneholder en felle:

Slik styrer to ledninger en hel skjerm

I²C er en felles buss: alle enhetene henger på de samme to ledningene. SCL er takten, SDA er dataene. Arduino begynner hver melding med en adresse, og bare den enheten som har adressen, svarer. Derfor kan du senere koble flere sensorer på de samme to pinnene.

I²C-buss med UNO R4 som kontroller, adapter på adresse 0x27 og utgangene P0 til P7 som går videre til LCD-kontrolleren.
Adapterens brikke, PCF8574, gjør hver mottatt byte om til åtte utganger. Den vanlige koblingen er P0 til RS, P1 til RW, P2 til E, P3 til baklyset og P4 til P7 til D4 til D7.

LCD-en selv kan ikke I²C. Den får bare halve tegn om gangen på fire ledninger, med en puls på E for hver halvdel. Hvert tegn blir derfor til flere meldinger på bussen, og det er grunnen til at programmene på nivå 2 og 3 ikke tegner hele skjermen på nytt hele tiden.

Se hvem som svarer på bussen

Dette lille programmet spør alle adresser om de svarer. Det er nyttig når skjermen ikke svarer, og senere når du kobler til flere enheter. Åpne Serial Monitor og velg 115200 baud.

// Spør alle adresser på I2C-bussen og skriver ut hvem som svarer.
#include <Wire.h>

void setup() {
  Serial.begin(115200);
  Wire.begin();
}

void loop() {
  Serial.println("Skanner I2C-bussen ...");
  int antall = 0;

  for (uint8_t adresse = 1; adresse < 127; adresse++) {
    Wire.beginTransmission(adresse);
    if (Wire.endTransmission() == 0) {  // 0 betyr at enheten svarte
      Serial.print("Fant enhet på 0x");
      if (adresse < 16) {
        Serial.print("0");
      }
      Serial.println(adresse, HEX);
      antall++;
    }
  }

  Serial.print("Antall enheter: ");
  Serial.println(antall);
  Serial.println();
  delay(5000);
}
Skanner I2C-bussen ...
Fant enhet på 0x27
Antall enheter: 1

Adressen kan være en annen. Databladet til PCF8574 oppgir området 0x20 til 0x27 avhengig av loddeflatene A0, A1 og A2, og varianten PCF8574A bruker 0x38 til 0x3F. Står det «Antall enheter: 0», er SDA og SCL byttet, eller så mangler 5V, GND eller en lodding.

Skjermen kan ikke norsk, men du kan lære den det

Tegnene ligger fast i en ROM i kontrolleren. Den vanligste varianten, A00, har japanske tegn og enkelte greske bokstaver, men ikke æ, ø og å. Skriver du «ø» i koden, kommer det noe helt annet på skjermen, fordi UTF-8-tegnet er to byte som viser to tilfeldige ROM-tegn.

Løsningen er CGRAM: et lite minne med plass til åtte egne tegn på 5 x 8 punkter. Du tegner hver rad som et tall, der 1 er et punkt som lyser.

Eget tegn ø på LCD tegnet i et rutenett med 5 ganger 8 punkter, med raden i binærkode ved siden av.
Den samme ø-en som i koden under. Hver rad er ett tall i listen, og de fem siste sifrene er punktene fra venstre mot høyre.
// Æ, Ø og Å som egne tegn på LCD 16x2.
#include <Wire.h>
#include <hd44780.h>
#include <hd44780ioClass/hd44780_I2Cexp.h>

hd44780_I2Cexp lcd;

// Hver linje er én rad med 5 punkter: 1 er på, 0 er av.
uint8_t storAE[8] = {0b01111, 0b10100, 0b10100, 0b11110, 0b10100, 0b10100, 0b10111, 0b00000};
uint8_t storOE[8] = {0b01110, 0b10011, 0b10101, 0b10101, 0b10101, 0b11001, 0b01110, 0b00000};
uint8_t storAA[8] = {0b00100, 0b01010, 0b00100, 0b01110, 0b10001, 0b11111, 0b10001, 0b10001};
uint8_t litenAE[8] = {0b00000, 0b00000, 0b01010, 0b00101, 0b01111, 0b10100, 0b01011, 0b00000};
uint8_t litenOE[8] = {0b00000, 0b00000, 0b01110, 0b10011, 0b10101, 0b11001, 0b01110, 0b00000};
uint8_t litenAA[8] = {0b00100, 0b01010, 0b00100, 0b01110, 0b00001, 0b01111, 0b10001, 0b01111};

void setup() {
  int status = lcd.begin(16, 2);
  if (status) {
    hd44780::fatalError(status);
  }

  // Plass 1 til 6. Plass 0 finnes også, men kan ikke stå inne i en tekst.
  lcd.createChar(1, storAE);
  lcd.createChar(2, storOE);
  lcd.createChar(3, storAA);
  lcd.createChar(4, litenAE);
  lcd.createChar(5, litenOE);
  lcd.createChar(6, litenAA);

  lcd.setCursor(0, 0);
  lcd.print("Bl\006b\004rsyltet\005y");
  lcd.setCursor(0, 1);
  lcd.print("\001 \002 \003   \004 \005 \006");
}

void loop() {
}
LCD 16x2 med ordet Blåbærsyltetøy og de seks norske bokstavene tegnet som egne tegn på skjermen.
Slik skal skjermen se ut med de seks egne tegnene.

Se på "Bl\006b\004rsyltet\005y". \006 er tegnet med nummer 6 skrevet inne i teksten. Tegn nummer 0 kan ikke brukes slik, fordi 0 betyr slutten på en tekst i C++. Derfor starter vi på 1.

Vil du se hva akkurat din skjerm har i ROM, kjør bibliotekets eksempel LCDcharset under hd44780examples. Det er en god måte å finne ut hvilken variant du har fått.

Nivå 2: en klimamåler med egne ikoner

Nivå 2: viderekommen. Tid: 45 minutter. Du trenger oppsettet fra nivå 1, DHT22-modul, koblingsbrett og koblingsledninger. Se også utvalget av koblingsbrett og ledninger.

UNO-kortet har bare én 5V-pinne, men nå er det to enheter som vil ha strøm. Derfor bruker vi koblingsbrettets strømskinner. Sensorens tre pinner står i hver sin kolonne, så du kan sette modulen rett i brettet.

Kobling av DHT22 og LCD 16x2 til UNO R4 WiFi på koblingsbrett, med out på D7 og strøm fra plusskinnen og minusskinnen.
DHT22 står i rad c med + i kolonne 11, out i kolonne 12 og - i kolonne 13. Ledningene går fra rad e, som er koblet til de samme kolonnene. Trykk på bildet for å forstørre.
FraTilLedning
UNO 5VPlusskinneHann-hann
UNO GNDMinusskinneHann-hann
Adapter VCCPlusskinneHunn-hann
Adapter GNDMinusskinneHunn-hann
Adapter SDA og SCLUNO SDA og SCLHunn-hann, som før
DHT22 + (kolonne 11)PlusskinneHann-hann
DHT22 - (kolonne 13)MinusskinneHann-hann
DHT22 out (kolonne 12)UNO D7Hann-hann

Modulen har allerede de passive komponentene sensoren trenger, så du skal ikke legge til noen ekstra motstand. Kablene som følger med, har hunnkontakter i begge ender på produktbildet, så i dette oppsettet bruker du dem ikke.

Installer «DHT sensor library» fra Adafruit i Library Manager, og godta at «Adafruit Unified Sensor» kommer med. Last så opp programmet.

// Nivå 2: temperatur og fuktighet fra DHT22 på LCD 16x2.
#include <Wire.h>
#include <hd44780.h>
#include <hd44780ioClass/hd44780_I2Cexp.h>
#include <DHT.h>

const uint8_t DHT_PIN = 7;                    // out på DHT22 går til D7
const unsigned long MALEINTERVALL_MS = 2500;  // Databladet krever minst 2 s

// Numrene på de egne tegnene
const uint8_t TEGN_TERMOMETER = 1;
const uint8_t TEGN_DRAPE = 2;
const uint8_t TEGN_GRAD = 3;

hd44780_I2Cexp lcd;
DHT dht(DHT_PIN, DHT22);
unsigned long sisteMaling = 0;

uint8_t termometer[8] = {0b00100, 0b01010, 0b01010, 0b01010, 0b01110, 0b11111, 0b11111, 0b01110};
uint8_t drape[8] = {0b00100, 0b00100, 0b01010, 0b01010, 0b10001, 0b10001, 0b10001, 0b01110};
uint8_t grad[8] = {0b00110, 0b01001, 0b01001, 0b00110, 0b00000, 0b00000, 0b00000, 0b00000};

// Lager tekst med én desimal og norsk komma, for eksempel "-3,5".
void medKomma(float verdi, char *tekst, size_t plass) {
  long tideler = lround(verdi * 10.0f);
  long positiv = labs(tideler);
  snprintf(tekst, plass, "%s%ld,%ld", tideler < 0 ? "-" : "", positiv / 10, positiv % 10);
}

void visMaling(float temperatur, float fuktighet) {
  char tall[8];
  char felt[8];

  medKomma(temperatur, tall, sizeof(tall));
  snprintf(felt, sizeof(felt), "%6s", tall);  // Høyrejustert, så kommaet står stille
  lcd.setCursor(0, 0);
  lcd.write(TEGN_TERMOMETER);
  lcd.print(" Temp  ");
  lcd.print(felt);
  lcd.write(TEGN_GRAD);
  lcd.print("C");

  medKomma(fuktighet, tall, sizeof(tall));
  snprintf(felt, sizeof(felt), "%6s", tall);
  lcd.setCursor(0, 1);
  lcd.write(TEGN_DRAPE);
  lcd.print(" Fukt  ");
  lcd.print(felt);
  lcd.print(" %");
}

void setup() {
  Serial.begin(115200);

  int status = lcd.begin(16, 2);
  if (status) {
    hd44780::fatalError(status);
  }
  lcd.createChar(TEGN_TERMOMETER, termometer);
  lcd.createChar(TEGN_DRAPE, drape);
  lcd.createChar(TEGN_GRAD, grad);

  lcd.setCursor(0, 0);
  lcd.print("Starter sensor");
  dht.begin();
  sisteMaling = millis();  // Første måling kommer etter 2,5 s
}

void loop() {
  if (millis() - sisteMaling < MALEINTERVALL_MS) {
    return;
  }
  sisteMaling = millis();

  float fuktighet = dht.readHumidity();
  float temperatur = dht.readTemperature();

  if (isnan(fuktighet) || isnan(temperatur)) {
    lcd.setCursor(0, 0);
    lcd.print("Sensorfeil      ");
    lcd.setCursor(0, 1);
    lcd.print("Sjekk D7,5V,GND ");
    Serial.println("Sensorfeil");
    return;
  }

  visMaling(temperatur, fuktighet);

  // Punktum her: dette formatet kan Serial Plotter tegne som graf.
  Serial.print("Temp:");
  Serial.print(temperatur, 1);
  Serial.print(",Fukt:");
  Serial.println(fuktighet, 1);
}
LCD 16x2 med temperatur og fuktighet fra DHT22, med termometer og dråpe som egne tegn på hver sin linje.
Slik skal skjermen se ut. Tallene er eksempler. Ved sensorfeil står det «Sensorfeil» og «Sjekk D7,5V,GND» i stedet.

Det som ikke er selvsagt i koden

  • Ingen delay(). loop() sjekker om 2,5 sekunder er gått, og går ellers rett videre. Det gir ingen forskjell her, men på nivå 3 må programmet følge med på en knapp samtidig.
  • medKomma. lcd.print(23.4) gir punktum. Vi regner om til tideler med lround og setter inn kommaet selv, og slipper samtidig små avrundingsfeil i flyttallet.
  • "%6s". Tallet høyrejusteres i seks tegn. Da overskriver 9,5 den gamle verdien 10,0 uten å la et siffer stå igjen, og kommaet står på samme sted.
  • isnan(). DHT-biblioteket gir NaN, ikke et tall, når lesingen feiler. Uten kontrollen ville skjermen vist tull.
Temp:23.4,Fukt:41.2
Temp:23.4,Fukt:41.3
Temp:23.5,Fukt:41.3

DHT22 pinout: hvilke pinner har sensoren?

Modulen i denne guiden har tre pinner, merket + out -. Sensoren alene har fire bein: pluss (VDD), data, ikke tilkoblet og GND.

Tegning av DHT22 pinout: sensor med fire bein til venstre og modul med tre pinner merket pluss, out og minus
Tegning av sensoren og modulen forfra. Bruker du sensoren alene, trenger data en pull-up-motstand mot pluss. Adafruit bruker 10 kΩ, og ESPHome oppgir rundt 4,7 kΩ. Les alltid merkingen på din egen modul.

Hvor mye kan du stole på en DHT22?

Skjermen viser én desimal, men det betyr ikke at målingen er så nøyaktig. Databladet skiller mellom oppløsning, nøyaktighet og gjentakbarhet, og forskjellen er verdt å kjenne:

EgenskapTemperaturFuktighetHva det betyr
Område-40 til 80 °C0 til 100 %RHDekker inneklima og det meste utendørs
Oppløsning0,1 °C0,1 %RHMinste steg på skjermen
Nøyaktighet±0,5 °C±2 %RH, høyst ±5 %RHHvor langt fra sann verdi den kan vise
Gjentakbarhet±0,2 °C±1 %RHSpredning mellom målinger under like forhold
Langtidsstabilitet±0,5 %RH per årFuktmålingen kan drive med tiden

Står det 23,4 °C, kan altså rommet være alt fra 22,9 til 23,9 °C. En endring fra 23,4 til 23,5 sier lite, mens en jevn stigning over ti minutter er reell. Derfor reagerer trendpilen på nivå 3 først ved 0,3 °C. Databladet krever også minst 2 sekunder mellom målingene og 1 sekund ro etter påslag. Begge deler er tatt hensyn til i programmene.

Plasser sensoren unna USB-kontakten og UNO-kortets spenningsregulator, direkte sol, vifter og pust. Se den som et godt verktøy for å følge trender i et rom, ikke som et kalibrert måleinstrument.

Nivå 3: 20x4 med min/maks, trendpil og knapp

Nivå 3: avansert. Tid: 1 time pluss lodding. Du trenger oppsettet fra nivå 2, LCD 4x20, en ny I²C-adapter og en trykknapp.

Den første adapteren er loddet fast, så du lodder en ny på den store skjermen på samme måte. Skjermen har samme 16 pinner, og produktbildet viser merkingen 16 til venstre og 1 til høyre på baksiden. Den leveres uten kontakter. På baksiden finnes et tomt felt med merkene U7, SCL, SDA, VDD og GND, men I²C-brikken er ikke montert der, så bruk adapteren.

Kobling av LCD 20x4, DHT22 og knapp til UNO R4 WiFi, der knappen går fra D2 til minusskinnen på koblingsbrettet.
Alt er som på nivå 2, men med ny skjerm og knapp. Knappen står over midtrennen. Bruk to diagonale bein, et til D2 og et til minusskinnen, så virker den uansett hvilken vei du har satt den.

Knappen er koblet fra pinne D2 til GND. INPUT_PULLUP holder D2 høy så lenge knappen er sluppet, og trykket drar den ned. Beinavstanden er ulik fra knapp til knapp, så flytt den gjerne en rad om den ikke passer i rad d og g.

// Nivå 3: klimamåler på LCD 20x4 med min/maks, trendpil og knapp for nullstilling.
#include <Wire.h>
#include <hd44780.h>
#include <hd44780ioClass/hd44780_I2Cexp.h>
#include <DHT.h>

const uint8_t DHT_PIN = 7;    // out på DHT22 går til D7
const uint8_t KNAPP_PIN = 2;  // Knappen går fra D2 til GND

const unsigned long MALEINTERVALL_MS = 2500;    // Databladet krever minst 2 s
const unsigned long TRENDINTERVALL_MS = 60000;  // Én trendprøve hvert minutt
const unsigned long PRELL_MS = 50;              // Ventetid mot kontaktprell
const unsigned long MELDING_MS = 2000;          // Så lenge meldingen vises

const uint8_t TREND_PROVER = 10;  // Sammenligner med målingen for ca. 10 minutter siden
const float TREND_GRENSE = 0.3f;  // Større enn sensorens gjentakbarhet på 0,2 grader
const uint8_t MAKS_FEIL = 3;      // Så mange feil på rad før feilmelding

const uint8_t TEGN_GRAD = 1;
const uint8_t TEGN_OPP = 2;
const uint8_t TEGN_NED = 3;
const uint8_t TEGN_FLAT = 4;

uint8_t grad[8] = {0b00110, 0b01001, 0b01001, 0b00110, 0b00000, 0b00000, 0b00000, 0b00000};
uint8_t pilOpp[8] = {0b00100, 0b01110, 0b10101, 0b00100, 0b00100, 0b00100, 0b00100, 0b00000};
uint8_t pilNed[8] = {0b00100, 0b00100, 0b00100, 0b00100, 0b10101, 0b01110, 0b00100, 0b00000};
uint8_t pilFlat[8] = {0b00000, 0b00100, 0b00010, 0b11111, 0b00010, 0b00100, 0b00000, 0b00000};

hd44780_I2Cexp lcd;
DHT dht(DHT_PIN, DHT22);

unsigned long sisteMaling = 0;
unsigned long sisteTrendprove = 0;
unsigned long periodeStart = 0;
unsigned long meldingSlutt = 0;
unsigned long sistVistSekund = 0;

float minTemp = 0;
float maksTemp = 0;
bool harMinMaks = false;
bool harMaling = false;
bool viserMelding = false;
uint8_t feilPaRad = 0;

float trendprover[TREND_PROVER];
uint8_t antallProver = 0;
uint8_t nesteProve = 0;

// Lager tekst med én desimal og norsk komma, for eksempel "-3,5".
void medKomma(float verdi, char *tekst, size_t plass) {
  long tideler = lround(verdi * 10.0f);
  long positiv = labs(tideler);
  snprintf(tekst, plass, "%s%ld,%ld", tideler < 0 ? "-" : "", positiv / 10, positiv % 10);
}

// Skriver en hel rad, fylt ut med mellomrom, så gammel tekst forsvinner.
void skrivRad(uint8_t rad, const char *tekst) {
  char linje[21];
  snprintf(linje, sizeof(linje), "%-20s", tekst);
  lcd.setCursor(0, rad);
  lcd.print(linje);
}

void lagreTrendprove(float temperatur) {
  trendprover[nesteProve] = temperatur;
  nesteProve = (nesteProve + 1) % TREND_PROVER;
  if (antallProver < TREND_PROVER) {
    antallProver++;
  }
}

// Rad 0, kolonne 12 til 19: pil og endring, eller hvor lenge det er til trenden er klar.
void visTrend(float temperatur) {
  char tekst[9];
  lcd.setCursor(12, 0);

  if (antallProver < TREND_PROVER) {
    snprintf(tekst, sizeof(tekst), "om %d min", TREND_PROVER - antallProver);
    lcd.print(tekst);
    return;
  }

  // nesteProve peker nå på den eldste prøven i ringbufferen.
  float endring = temperatur - trendprover[nesteProve];
  uint8_t pil = TEGN_FLAT;
  if (endring >= TREND_GRENSE) {
    pil = TEGN_OPP;
  } else if (endring <= -TREND_GRENSE) {
    pil = TEGN_NED;
  }

  char tall[8];
  medKomma(fabs(endring), tall, sizeof(tall));
  snprintf(tekst, sizeof(tekst), " %c%-5s", endring < 0 ? '-' : '+', tall);
  lcd.write(pil);
  lcd.print(tekst);
}

void visMaling(float temperatur, float fuktighet) {
  char tall[8];
  char felt[8];

  medKomma(temperatur, tall, sizeof(tall));
  snprintf(felt, sizeof(felt), "%5s", tall);
  lcd.setCursor(0, 0);
  lcd.print("Temp");
  lcd.print(felt);
  lcd.write(TEGN_GRAD);
  lcd.print("C");
  visTrend(temperatur);

  medKomma(fuktighet, tall, sizeof(tall));
  char linje[21];
  snprintf(linje, sizeof(linje), "Fukt%5s %%", tall);
  skrivRad(1, linje);
}

void visMinMaks() {
  char lav[8] = "--,-";
  char hoy[8] = "--,-";
  if (harMinMaks) {
    medKomma(minTemp, lav, sizeof(lav));
    medKomma(maksTemp, hoy, sizeof(hoy));
  }
  char linje[21];
  snprintf(linje, sizeof(linje), "Min %5s Maks %5s", lav, hoy);
  skrivRad(2, linje);
}

// Rad 3: hvor lenge min/maks har samlet målinger, hentet fra millis().
void visPeriode() {
  unsigned long sekunder = (millis() - periodeStart) / 1000;
  char tid[13];
  char linje[21];
  snprintf(tid, sizeof(tid), "%lu:%02lu:%02lu", sekunder / 3600, (sekunder / 60) % 60, sekunder % 60);
  snprintf(linje, sizeof(linje), "Periode %12s", tid);
  skrivRad(3, linje);
}

// Gir true én gang per trykk, først når signalet har vært stabilt i PRELL_MS.
bool knappTrykket() {
  static int stabil = HIGH;
  static int forrige = HIGH;
  static unsigned long endret = 0;

  int naa = digitalRead(KNAPP_PIN);
  if (naa != forrige) {
    forrige = naa;
    endret = millis();
  }
  if (millis() - endret >= PRELL_MS && naa != stabil) {
    stabil = naa;
    return stabil == LOW;
  }
  return false;
}

void nullstillMinMaks() {
  harMinMaks = false;
  periodeStart = millis();
  visMinMaks();
  skrivRad(3, "Min/maks nullstilt");
  viserMelding = true;
  meldingSlutt = millis() + MELDING_MS;
}

void gjorMaling() {
  float fuktighet = dht.readHumidity();
  float temperatur = dht.readTemperature();

  if (isnan(fuktighet) || isnan(temperatur)) {
    if (feilPaRad < MAKS_FEIL) {
      feilPaRad++;
    }
    if (feilPaRad == MAKS_FEIL) {
      skrivRad(0, "Sensorfeil: sjekk");
      skrivRad(1, "D7, 5V og GND");
      Serial.println("Sensorfeil");
    }
    return;
  }
  feilPaRad = 0;
  harMaling = true;

  // Min og maks kommer bare fra gyldige målinger.
  if (!harMinMaks) {
    minTemp = temperatur;
    maksTemp = temperatur;
    harMinMaks = true;
  } else {
    minTemp = min(minTemp, temperatur);
    maksTemp = max(maksTemp, temperatur);
  }

  // Første prøve lagres med en gang, deretter én hvert minutt.
  if (antallProver == 0 || millis() - sisteTrendprove >= TRENDINTERVALL_MS) {
    lagreTrendprove(temperatur);
    sisteTrendprove = millis();
  }

  visMaling(temperatur, fuktighet);
  visMinMaks();

  // Punktum her: dette formatet kan Serial Plotter tegne som graf.
  Serial.print("Temp:");
  Serial.print(temperatur, 1);
  Serial.print(",Fukt:");
  Serial.print(fuktighet, 1);
  Serial.print(",Min:");
  Serial.print(minTemp, 1);
  Serial.print(",Maks:");
  Serial.println(maksTemp, 1);
}

void setup() {
  Serial.begin(115200);
  pinMode(KNAPP_PIN, INPUT_PULLUP);

  int status = lcd.begin(20, 4);
  if (status) {
    hd44780::fatalError(status);
  }
  lcd.createChar(TEGN_GRAD, grad);
  lcd.createChar(TEGN_OPP, pilOpp);
  lcd.createChar(TEGN_NED, pilNed);
  lcd.createChar(TEGN_FLAT, pilFlat);

  skrivRad(0, "Inneklima 20x4");
  skrivRad(1, "Starter sensor");
  dht.begin();
  sisteMaling = millis();  // Første måling kommer etter 2,5 s
  periodeStart = millis();
}

void loop() {
  if (knappTrykket()) {
    nullstillMinMaks();
  }

  if (viserMelding && (long)(millis() - meldingSlutt) >= 0) {
    viserMelding = false;
  }

  if (millis() - sisteMaling >= MALEINTERVALL_MS) {
    sisteMaling = millis();
    gjorMaling();
  }

  // Oppdaterer tiden én gang i sekundet når det ikke vises en melding.
  unsigned long sekund = millis() / 1000;
  if (harMaling && !viserMelding && sekund != sistVistSekund) {
    sistVistSekund = sekund;
    visPeriode();
  }
}
LCD 20x4 med temperatur, trendpil, fuktighet, laveste og høyeste temperatur og tid siden min og maks ble nullstilt.
Hovedbildet, tegnet med eksempelverdier. Pilen peker opp når temperaturen er minst 0,3 °C høyere enn for omtrent ti minutter siden, og tallet ved siden av er endringen.
Fire skjermbilder på LCD 20x4 fra programmet: oppstart, venting på trend, nullstilling med knapp og sensorfeil.
De andre skjermbildene du kan møte. Etter en nullstilling står det --,- til neste måling kommer, etter høyst 2,5 sekunder. Ved sensorfeil blir min og maks stående.

Slik virker de nye delene

  • Bare ekte målinger. Min og maks får verdi først ved den første gyldige målingen. Før det viser skjermen --,-, ikke en oppdiktet startverdi.
  • Trenden. trendprover er en ringbuffer med ti plasser. Hvert minutt legges det inn en ny verdi over den eldste. Først når den er full, sammenligner vi, og frem til da står det for eksempel «om 7 min».
  • Tre feil på rad. DHT22 kan av og til gi en mislykket lesing. Programmet venter derfor til tre lesinger har feilet før det melder fra.
  • Prell. En knapp spretter noen millisekunder når den trykkes inn. knappTrykket() godtar endringen først når signalet har ligget stille i 50 ms, slik at ett trykk blir ett trykk.
  • Tid uten klokke. «Periode» kommer fra millis() og viser tiden siden start eller siste nullstilling. Programmet later ikke som det vet klokkeslettet.
  • Rad for rad. skrivRad fyller opp til 20 tegn, slik at gammel tekst ikke blir stående.

Fra koblingsbrett til boks

Når alt virker, kan du bygge det inn. Produktsiden til LCD 2x16 har en STL-fil med stativ til 3D-skriver under Dokumenter og linker. Lager du boksen selv, har 20x4-skjermen mål 98 x 60 mm og 93 x 55 mm mellom festehullene.

Sensoren ut

La DHT22 sitte utenfor boksen eller i et eget rom med lufthull. Her kan de medfølgende kablene brukes, om du setter hannpinner mellom kabelen og det du kobler til.

Kontrasten innen rekkevidde

Merk av hvor skruen sitter, eller lag et lite hull til en skrutrekker. Kontrasten kan måtte justeres igjen når skjermen er kommet på plass.

Kabelen sikret

Fest USB-kabelen med strekkavlastning, slik at et rykk ikke tar med seg kortet og ledningene.

Prøv selv

Hver utfordring bygger på programmet fra nivå 3, og alle bruker noe du allerede har på bordet.

  1. Sparemodus. Slå av baklyset med lcd.noBacklight() etter 30 sekunder uten knappetrykk, og slå det på igjen med lcd.backlight(). Tenk på at det første trykket ikke bør nullstille noe.
  2. Fuktighet også. Legg til min og maks for fuktighet, og la et langt trykk på knappen bytte mellom temperatur og fuktighet på rad 2.
  3. Varsel om fukt. La rad 1 blinke «LUFT UT» når fuktigheten er over 60 %RH. Velg en grense som passer rommet.
  4. Animasjon. Bruk et ledig CGRAM-tegn som en liten vifte, og skriv det med createChar på nytt hvert halve sekund.
  5. LED-matrisen. UNO R4 WiFi har 12 x 8 lysdioder. Vis trendpilen der også med biblioteket Arduino_LED_Matrix, så ser du den fra andre siden av rommet.
  6. Ekte klokke. Biblioteket RTC følger med kortet. Bytt ut «Periode» med klokkeslett, og tenk gjennom hva som skjer når strømmen går. Kortet har en VRTC-pinne til reservebatteri for klokken.

Hvis det ikke virker

Det du serMest sannsynligGjør dette
Ingen baklys5V, GND eller LED-jumperenSjekk at jumperen sitter på adapteren og at VCC går til 5V
Baklys, ingen tegnKontrastDrei skruen langsomt gjennom hele området
Rad med blokkerIkke initialisertKjør skanneren, deretter I2CexpDiag, og se over de 16 loddepunktene
Antall enheter: 0SDA og SCL byttet, eller bruddBytt om de to ledningene og sjekk 5V og GND
Tilfeldige tegnDårlig kontaktEtterlodd og trykk ledningene godt inn, ta USB ut og inn
Rare tegn i stedet for æ, ø, åTegnene finnes ikke i ROMBruk createChar
«Sensorfeil»out ikke på D7, eller + og - feilFølg merkingen på modulen og kontroller kolonnene
Knappen nullstiller av seg selvIkke diagonale bein, eller feil pinneMål at D2 bare går til GND når du trykker
Rester av gamle tegnKortere tekst skrives uten å fylle ut resten av radenFyll med mellomrom, slik skrivRad gjør
Skjermen virket, men sluttet etter at du koblet til DHT22

Ta bort DHT22 og kjør nivå 1 igjen. Virker skjermen da, er det kortslutning eller feil rekkefølge rundt sensoren. Se særlig etter at + og - ikke er byttet på koblingsbrettet.

Programmet laster opp, men LED-en på kortet blinker

Da har lcd.begin() gitt en feil, og fatalError blinker feilkoden. Kjør skanneren og se om adapteren svarer. Svarer den, kjører du I2CexpDiag.

Test deg selv

Test deg selv: hvorfor er 0x27 ikke oppgitt i koden?

hd44780_I2Cexp uten adresse leter på bussen og finner adapteren selv. Da virker koden også på en PCF8574A på 0x3F, uten at du endrer noe.

Test deg selv: skjermen viser 23,4 °C. Hva er temperaturen i rommet?

Et sted mellom 22,9 og 23,9 °C, med nøyaktigheten på ±0,5 °C i databladet. Desimalen sier mest om endringer over tid.

Test deg selv: hvorfor kan du ikke skrive æ rett inn i lcd.print?

Bokstaven finnes ikke i tegnsettet A00, og i kildekoden er den to byte i UTF-8. Skjermen viser da to tegn som ikke har noe med æ å gjøre.

Test deg selv: hva skjer hvis du leser DHT22 hvert halve sekund?

Adafruit-biblioteket hopper over lesinger nærmere enn 2 sekunder og gir deg forrige resultat, så du får ikke flere målinger. Databladet krever minst 2 sekunder mellom hver.

Test deg selv: hvorfor leser D2 LOW når du trykker på knappen?

INPUT_PULLUP holder pinnen høy gjennom en intern motstand, og knappen kobler den til GND. Trykket drar derfor signalet lavt.

Test deg selv: hvorfor skriver programmet 2:15:33 og ikke klokka?

UNO-kortet vet ikke hva klokka er når det starter. millis() teller tiden siden start, og da er det ærlig å vise nettopp det.

Vanlige spørsmål

Må jeg vite at adressen er 0x27?

Nei. Biblioteket i denne guiden finner adressen selv, og skanneren viser den hvis du vil vite det. Vil du bruke et annet bibliotek, må du oppgi adressen selv.

Kan jeg koble LCD-en til Qwiic-kontakten på UNO R4?

Ikke i dette oppsettet. Qwiic-kontakten er bare 3,3 V og ligger på en annen buss (Wire1), mens adapteren skal ha 5 V og koden bruker SDA og SCL på kortet.

Kan jeg bruke samme skjerm på en ESP32?

Biblioteket støtter mange kort, men adapteren har motstander opp til forsyningen på SDA og SCL. ESP32 tåler 3,3 V på pinnene, så med adapteren på 5 V trenger du nivåtilpasning.

Kan jeg slå av baklyset når det er mørkt?

Ja. lcd.noBacklight() slår av baklyset fra programmet. Du kan også fjerne jumperen på adapteren, men da må du sette den på igjen for å bruke baklyset.

Hvorfor viser DHT22 for høy temperatur?

Som oftest er det noe som varmer den, for eksempel baklyset, kortet, en regulator eller sol. Flytt sensoren bort fra elektronikken, helst utenfor boksen, og sammenlign med et termometer. Databladet oppgir ±0,5 °C, så et lite avvik er normalt.

Kan jeg bruke DHT22 med ESPHome og Home Assistant?

Ja, men da på en ESP32, ikke på UNO R4 WiFi. ESPHome har en egen komponent for DHT22 og AM2302, og den leser sensoren hvert minutt som standard. ESP32-guiden viser hvordan du kommer i gang med kortet.

Hvor lang kan kabelen til DHT22 være?

Databladet oppgir opptil 100 meter under gode forhold, men start med korte ledninger. Lange ledninger er følsomme for støy, og feilene kommer som «Sensorfeil».

Slik bygger du videre

Nå har du et program som måler, husker og viser. Neste naturlige skritt er å la det gjøre noe: guiden om relémodul med Arduino viser hvordan et lavspent utgangssignal kan styre en vifte eller lampe. Vil du måle ute, bruker værstasjonen med ESP32 og LoRa samme sensor. Inneklima handler også om mer enn fuktighet, og guiden om CO₂, VOC og PM2,5 forklarer hva de andre målingene betyr. Er selve programmeringen ny for deg, begynn med Arduino-programmering for nybegynnere.

Trenger du en annen skjerm til Arduino, finner du LCD, OLED og større fargeskjermer som IPS på 2,4 tommer under skjerm og display. Flere kort ligger under Arduino.

Kilder: pinout og datablad for UNO R4 WiFi, hd44780-biblioteket, Adafruits DHT-bibliotek, datablad for AM2302, datablad for PCF8574 og NIST om fuktighet over mettede saltløsninger.

Alle guider

Produkter omtalt i denne guiden

Produktene som brukes og sammenlignes i guiden, med pris og lagerstatus.