Du våkner med tungt hode og tett luft på soverommet. Er det CO₂? En luftkvalitetssensor kan svare på deler av spørsmålet, men bare hvis du vet hva den måler. CO₂, VOC og PM2,5 er tre ulike ting som krever tre ulike sensorer. Her ser du hvordan de virker, hvilke norske tall du kan sammenligne med, og hvordan du bygger din egen luftvakt med skjerm, lamper og logg.
Hvorfor kjennes soverommet tett om morgenen?
To voksne puster ut omtrent 22 liter CO₂ i timen mens de sover. I et lite rom med lukket dør og vindu har gassen ingen steder å gå. Kurvene over kommer fra en enkel massebalanse, som du kan regne ut med en kalkulator:
C(t) = C_ute + G/Q + (C_start - C_ute - G/Q) x e^(-Q x t / V) C = CO2-andel, G = CO2 fra personene i m³/h Q = friskluft i m³/h, V = romvolum i m³, t = timer
Kravet i byggteknisk forskrift § 13-2 er 26 m³ friskluft per time per sengeplass, altså 52 m³/h for to personer. Da ender regnestykket på rundt 840 ppm. Med 15 m³/h havner du på rundt 1860 ppm, og et nesten tett rom med 6 m³/h kommer opp i omtrent 3380 ppm før klokka sju.
Tallet for utånding kommer fra Persily og de Jonge. Med deres formel gir en mann på 70 kg og en kvinne på 60 kg i søvn til sammen rundt 22 liter i timen. Større personer puster ut mer.
Tre sensorer, tre helt ulike fysiske prinsipper
Navnene ligner, men det som skjer inne i sensorene har lite til felles. Når du vet hva som måles, forstår du også hva som kan lure dem.
CO₂: lyset blir til lyd
Sensirion SCD41 bruker det produsenten kaller fotoakustisk NDIR. Det betyr at sensoren ser direkte på CO₂-molekylene. Databladet oppgir ±(50 ppm + 2,5 % av målingen) mellom 400 og 1000 ppm. En visning på 1000 ppm kan altså i virkeligheten være alt fra 925 til 1075 ppm, så ikke heng deg opp i siste siffer.
VOC: en varm overflate som endrer motstand
En metalloksidsensor (MOX) ser ikke enkeltstoffer. BME688 gir ut en gassmotstand i ohm, og mer flyktige gasser gir som regel lavere motstand. Sprit, parfyme, nystekt brød og fersk maling gir alle utslag. Du får vite at noe skjedde, ikke hva det var.
Partikler: lys som spres
Partikkelsensoren i dette prosjektet er en Sharp GP2Y1010AU0F. Den har infrarød diode, fototransistor og et lufthull på 9 mm, men verken laser eller vifte. Dyrere partikkelsensorer bruker gjerne laser og vifte og teller partikler i størrelsesklasser. Denne gir bare en spenning som stiger med mengden partikler i hullet.
Velg luftkvalitetssensor etter det du lurer på
du lurer på ventilasjonen på soverom, kontor eller klasserom. CO2L har SCD41 og trenger en kontroller.
du vil følge lukt, rengjøring og nye materialer som trend. BME688-modulen måler også temperatur, fukt og trykk.
du vil se når matos, stearinlys eller vedovn slår ut. GP2Y1010 viser hendelser og trend, ikke et døgnmiddel du kan stole på.
du vil slippe å bygge. SenseCAP Indicator D1Pro har SCD41, SGP40, AHT20 og 4-tommers berøringsskjerm i samme enhet.

| Sensor | Måler | Enhet | Svarer på | Svarer ikke på |
|---|---|---|---|---|
| SCD41 (CO2L) | CO₂ direkte | ppm | Får rommet nok friskluft? | Er det kjemikalier eller røyk i lufta? |
| BME688 | Gassmotstand | ohm | Endret noe seg i lufta? | Hvilket stoff er det, og hvor mye? |
| SGP40 (i D1Pro) | Gassrespons | VOC-indeks | Er lufta verre enn vanlig her? | Absolutt mengde VOC |
| GP2Y1010 | Spredt lys | volt | Kom det en røyk- eller støvtopp? | Holder vi FHIs norm for PM2,5? |
CO2-måler til hjemmet: kjøpe ferdig eller bygge selv?
Kjøp ferdig hvis du bare vil se tallet. Bygg selv hvis du vil ha logg, egne grenser eller en lampe som reagerer.
Uansett hva du velger, bør beskrivelsen si NDIR eller fotoakustisk. Står det eCO₂ eller TVOC, er tallet beregnet, og det forklarer neste del. Ferdig enhet finner du i SenseCAP Indicator, og flere sensorer ligger under luftkvalitetssensorer og elektroniske sensorer.
eCO₂: tallet som ser ut som CO₂, men ikke er det
Mange billige luftmålere viser «CO₂» uten å ha en CO₂-sensor. Tallet kalles eCO₂ eller CO₂-ekvivalent og beregnes fra VOC-signalet. Boschs programvare BSEC gir for eksempel både IAQ og CO₂-ekvivalenter fra BME688. Slike beregninger bygger på at flyktige gasser og CO₂ ofte stiger samtidig når folk er i rommet, men sensoren har ikke sett en eneste CO₂-verdi.
Test deg selv: måleren viser 2500 ppm rett etter at du vasket gulvet. Må du lufte?
Å lufte er sjelden feil, men sjekk først om måleren har ekte CO₂-sensor. Gulvvask tilfører VOC og nesten ingen CO₂. Står det eCO₂ eller TVOC i databladet, har den regnet seg frem til tallet.
Test deg selv: en måler viser «CO₂ 1200 ppm» rett etter at du har brukt deodorant. Hva kan forklaringen være?
Måleren har trolig en VOC-sensor og viser eCO₂. Den regner om gasser fra sprayen til et CO₂-lignende tall. En sensor med ekte CO₂-måling, som SCD41, reagerer ikke på samme måte.
Hvilke tall er høye? Norske grenser, sjekket hos kilden
| Stoff | Verdi | Hva det er | Kilde |
|---|---|---|---|
| CO₂ | 1000 ppm | Anbefalt faglig norm som maksimumsverdi, en indikator på luftskifte | FHI |
| CO₂ | 1000 ppm | God ventilasjon skal holde seg under denne normen | Arbeidstilsynet |
| CO₂ | 5000 ppm | Grenseverdi i arbeidsatmosfæren, ikke et mål for inneklima | Forskrift om tiltaks- og grenseverdier |
| PM2,5 | 15 µg/m³ | Anbefalt faglig norm, 24 timers middel | FHI |
| PM2,5 | 8 µg/m³ | Anbefalt faglig norm, årsmiddel | FHI |
| TVOC | Ingen tallverdi | FHI mener grunnlaget er utilstrekkelig for en helsebasert norm | FHI |
Legg merke til at PM2,5-normen gjelder middelverdier over et døgn eller et år. En kort topp fra stekepanna bryter ikke normen i seg selv. Og siden FHI ikke har noen tallgrense for TVOC, finnes det heller ikke noe norsk tall å holde en VOC-indeks opp mot.
Plassering betyr mer enn en ekstra desimal
- Pustehøyde: sett sensoren omtrent der hodet er når du sitter eller ligger, men minst en meter fra ansiktet.
- Fri luft rundt: ikke i lukket skuff eller tett boks. Lag åpninger hvis du bygger kabinett.
- Unna varme: sol, radiator og annen elektronikk gjør temperatur og fukt feil.
- Unna kilden: over komfyren ser du bare matos. Står du ved siden av, ser du bare deg selv.
- Støvsensoren loddrett: GP2Y1010 har ingen vifte, så lufta må kunne drive gjennom hullet av seg selv.
Test deg selv: du legger CO₂-måleren på nattbordet, helt inntil hodet. Hva blir feil?
Du måler utåndingen din, ikke lufta i rommet, så tallet blir for høyt. Sett den omtrent i pustehøyde, men minst en meter fra ansiktet.
Kjøkken, stearinlys og vedovn: er det verdt å måle partikler?
Ja, hvis du vil se hendelser. FHI anslår ut fra få målinger at PM2,5 i norske hjem uten røyking ligger rundt 10 µg/m³ eller lavere, men at kilder som gamle vedovner kan gi vesentlig høyere nivåer i perioder. Rapporten trekker frem matlaging, stearinlys, oljelamper og peis som innekilder.
En norsk undersøkelse som FHI viser til, fant mindre økning ved vedfyring i ovn, og betydelig økning ved peisfyring. Rådene er enkle: sørg for god trekk i ovn og peis, begrens stearinlys og duftlys, og bruk kjøkkenvifte med avtrekk under steking.
Prosjekt: bygg Luftvakta med UNO R4 WiFi
- Nivå
- Nybegynner til viderekommen
- Tid
- 30 minutter til 2 timer
- Lodding
- Kun stiftlist på BME688
- Du lærer
- To I²C-busser, tegning på OLED, pulsstyrt avlesning og CSV-logg
Vi har kompilert alle tre programmene for UNO R4 WiFi (FQBN arduino:renesas_uno:unor4wifi), men ikke kjørt dem på maskinvare. Tegningene av skjermen er nettopp det: tegninger.
Passer delene sammen?
De fire produktene som ofte nevnes sammen, er ikke et ferdig sett. SenseCAP Indicator er en komplett enhet og trenger ikke de andre. Da står tre sensorer igjen, og to av dem stiller krav til kontrolleren:
- GP2Y1010 vil ha 5 V (±0,5 V), og signalet kan nå over 3,4 V. Et ESP32-kort med 3,3 V på pinnene ville trengt en spenningsdeler. UNO R4 WiFi går på 5 V, så her går det direkte.
- CO2L har 3,3 V på I²C. Skjemaet viser pull-up til 3,3 V og ingen nivåomformer, og SCD41 tåler bare forsyningen + 0,3 V på SDA og SCL. Derfor skal den ikke på 5 V-bussen. UNO R4 WiFi har en egen Qwiic-buss på 3,3 V.
- BME688 fra DFRobot er vist koblet med VCC på 5 V og SDA/SCL rett på et Uno-kort, så den kan dele buss med skjermen.
Dette trenger du
Arduino UNO R4 WiFi1 stk, hjernen i alle tre nivåer
M5Stack CO2L (SCD41)1 stk, nivå 1 til 3: ekte CO₂
Qwiic til Grove-kabel1 stk, fra kortet til CO2L
OLED 0,96 tommer, I²C1 stk, nivå 2 og 3: tall og kurve- LED-sett med 1,2 kΩ-motstanderGrønn, gul og rød LED, fire motstander, nivå 2 og 3
- Koblingsbrett 400 hull1 stk, nivå 2 og 3
- Koblingsledninger hann-hannSett, nivå 2 og 3
Miljøsensor BME6881 stk, nivå 3: VOC-trend og lufttrykk
Støvsensor GP2Y1010AU0F1 stk med kabel, 150 Ω og 220 µF, nivå 3- NPN-transistor BC337-401 stk, nivå 3: styrer IR-dioden
Se også koblingsbrett og koblingsledninger hvis du vil ha andre størrelser. Bibliotekene installerer du i Arduino IDE: Sensirion I2C SCD4x, Adafruit SSD1306, Adafruit GFX Library og Adafruit BME680 Library. Sistnevnte brukes fordi DFRobots eget BME68x-bibliotek ikke ligger i bibliotekshåndtereren.
Nivå 1: CO₂ uten ledninger
Her er hele programmet. Det leser CO₂ fra Wire1, skriver til Serial Monitor og tegner en søyle på LED-matrisen.
// Luftvakt nivå 1: ekte CO2 fra M5Stack CO2L (SCD41) på Arduino UNO R4 WiFi.
// Sensoren står i Qwiic-kontakten, som er den andre I2C-bussen på kortet: Wire1.
#include <Wire.h>
#include <SensirionI2cScd4x.h>
#include <Arduino_LED_Matrix.h>
SensirionI2cScd4x sensor;
ArduinoLEDMatrix matrise;
uint8_t ramme[8][12];
const unsigned long OPPVARMING_MS = 180000UL; // tre ganger responstiden på 60 s
// Skriver desimaltall med komma, slik et norsk regneark forventer.
void skrivDesimal(float verdi, int desimaler) {
String tekst(verdi, desimaler);
tekst.replace('.', ',');
Serial.print(tekst);
}
// Én kolonne per 100 ppm over 400 ppm: seks kolonner tilsvarer 1000 ppm.
void visStolpe(uint16_t co2) {
int kolonner = (int(co2) - 400) / 100;
kolonner = constrain(kolonner, 0, 12);
for (int y = 0; y < 8; y++) {
for (int x = 0; x < 12; x++) {
ramme[y][x] = (x < kolonner) ? 1 : 0;
}
}
matrise.renderBitmap(ramme, 8, 12);
}
void setup() {
Serial.begin(115200);
while (!Serial && millis() < 3000) {
}
matrise.begin();
Wire1.begin();
sensor.begin(Wire1, SCD41_I2C_ADDR_62);
// Sett sensoren i kjent tilstand, også etter en omstart av bare kortet.
sensor.wakeUp();
sensor.stopPeriodicMeasurement();
sensor.reinit();
int16_t feil = sensor.startPeriodicMeasurement();
if (feil != 0) {
Serial.print("Fant ikke SCD41, feilkode ");
Serial.println(feil);
}
Serial.println("Luftvakt nivå 1. Ny måling hvert 5. sekund.");
}
void loop() {
bool klar = false;
sensor.getDataReadyStatus(klar);
if (!klar) {
delay(200);
return;
}
uint16_t co2 = 0;
float temperatur = 0.0;
float fukt = 0.0;
if (sensor.readMeasurement(co2, temperatur, fukt) != 0 || co2 == 0) {
return; // ugyldig avlesning, vent på neste
}
visStolpe(co2);
Serial.print("CO2 ");
Serial.print(co2);
Serial.print(" ppm | ");
skrivDesimal(temperatur, 1);
Serial.print(" C | ");
skrivDesimal(fukt, 0);
Serial.print(" % RF");
if (millis() < OPPVARMING_MS) {
Serial.print(" | varmer opp");
}
Serial.println();
}
Wire1 er Qwiic-bussen, mens vanlig Wire ligger på SDA og SCL. Nullstillingen i setup() er med fordi sensoren fortsatt har strøm når du bare trykker reset på kortet, og da svarer den ikke på en ny start uten at måleserien stoppes først.
Luftvakt nivå 1. Ny måling hvert 5. sekund. CO2 612 ppm | 22,4 C | 38 % RF | varmer opp CO2 608 ppm | 22,4 C | 38 % RF | varmer opp CO2 640 ppm | 22,9 C | 37 % RF
Hvorfor «varmer opp»? Sensoren leverer nye tall hvert 5. sekund, men databladet oppgir responstid på typisk 60 sekunder til 63 % av et sprang. Tre slike tidskonstanter er tre minutter, og da er 95 % av endringen med. Tre minutter er altså vårt eget valg ut fra dette tallet.
Nivå 2: skjerm med kurve og trafikklys
| Fra | Til | Merk |
|---|---|---|
| UNO 5V | Plusskinne | Rød |
| UNO GND | Minusskinne | Svart |
| OLED VCC | Plusskinne | 3,3 til 5 V ifølge produktsiden |
| OLED GND | Minusskinne | |
| OLED SCL | UNO SCL | Wire |
| OLED SDA | UNO SDA | Wire |
| UNO D5 | Grønn LED | Katode via 1,2 kΩ til GND |
| UNO D6 | Gul LED | Katode via 1,2 kΩ til GND |
| UNO D7 | Rød LED | Katode via 1,2 kΩ til GND |
| CO2L | Qwiic | Samme kabel som på nivå 1 |
// Luftvakt nivå 2: CO2-tall og kurve på OLED, grønn, gul og rød LED.
// CO2L på Wire1 (Qwiic, 3,3 V). OLED på Wire (SDA/SCL på kortet, 5 V).
#include <Wire.h>
#include <SensirionI2cScd4x.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
const int LED_GRONN = 5;
const int LED_GUL = 6;
const int LED_ROD = 7;
const uint16_t GUL_FRA = 800; // vårt eget tidlige varsel, ikke en norm
const uint16_t ROD_OVER = 1000; // FHIs anbefalte faglige norm
const unsigned long OPPVARMING_MS = 180000UL;
const unsigned long LOGG_HVERT_MS = 60000UL;
const int PUNKTER = 120; // ett punkt i minuttet gir to timer på skjermen
SensirionI2cScd4x sensor;
Adafruit_SSD1306 skjerm(128, 64, &Wire, -1);
uint16_t historikk[PUNKTER];
int antallPunkter = 0;
unsigned long sisteLogg = 0;
uint16_t co2 = 0;
float temperatur = 0.0;
void leggTilPunkt(uint16_t verdi) {
if (antallPunkter < PUNKTER) {
historikk[antallPunkter++] = verdi;
return;
}
memmove(historikk, historikk + 1, (PUNKTER - 1) * sizeof(uint16_t));
historikk[PUNKTER - 1] = verdi;
}
// 400 ppm havner på y = 63 og 2000 ppm på y = 27.
int ppmTilY(uint16_t ppm) {
long y = 63L - (long(ppm) - 400L) * 36L / 1600L;
return constrain(y, 27, 63);
}
void skrivHoyre(int y, const String &tekst) {
skjerm.setTextSize(1);
skjerm.setCursor(128 - int(tekst.length()) * 6, y);
skjerm.print(tekst);
}
const char *statusTekst() {
if (millis() < OPPVARMING_MS) return "VARMER";
if (co2 > ROD_OVER) return "LUFT UT!";
if (co2 >= GUL_FRA) return "SNART";
return "GOD";
}
void tegnSkjerm() {
skjerm.clearDisplay();
skjerm.setTextColor(SSD1306_WHITE);
String tall(co2);
skjerm.setTextSize(3);
skjerm.setCursor(0, 0);
skjerm.print(tall);
skjerm.setTextSize(1);
skjerm.setCursor(int(tall.length()) * 18 + 2, 17);
skjerm.print("ppm");
skrivHoyre(0, statusTekst());
String grader(temperatur, 1);
grader.replace('.', ',');
skrivHoyre(10, grader + " C");
// Stiplet linje ved 1000 ppm.
int normY = ppmTilY(ROD_OVER);
for (int x = 8; x < 128; x += 4) {
skjerm.drawPixel(x, normY, SSD1306_WHITE);
}
// Kurven: nyeste punkt lengst til høyre.
int start = 128 - antallPunkter;
for (int i = 1; i < antallPunkter; i++) {
skjerm.drawLine(start + i - 1, ppmTilY(historikk[i - 1]), start + i, ppmTilY(historikk[i]), SSD1306_WHITE);
}
skjerm.display();
}
void settLys() {
bool varm = millis() >= OPPVARMING_MS;
digitalWrite(LED_GRONN, varm && co2 < GUL_FRA);
digitalWrite(LED_GUL, varm && co2 >= GUL_FRA && co2 <= ROD_OVER);
digitalWrite(LED_ROD, varm && co2 > ROD_OVER);
}
void setup() {
Serial.begin(115200);
pinMode(LED_GRONN, OUTPUT);
pinMode(LED_GUL, OUTPUT);
pinMode(LED_ROD, OUTPUT);
Wire.begin();
if (!skjerm.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println("Fant ikke OLED på 0x3C");
}
Wire1.begin();
sensor.begin(Wire1, SCD41_I2C_ADDR_62);
sensor.wakeUp();
sensor.stopPeriodicMeasurement();
sensor.reinit();
if (sensor.startPeriodicMeasurement() != 0) {
Serial.println("Fant ikke SCD41 på Wire1");
}
Serial.println("sekunder;co2_ppm");
}
void loop() {
bool klar = false;
sensor.getDataReadyStatus(klar);
if (!klar) {
delay(200);
return;
}
uint16_t ny = 0;
float fukt = 0.0;
float grader = 0.0;
if (sensor.readMeasurement(ny, grader, fukt) != 0 || ny == 0) {
return;
}
co2 = ny;
temperatur = grader;
unsigned long naa = millis();
if (naa >= OPPVARMING_MS && naa - sisteLogg >= LOGG_HVERT_MS) {
sisteLogg = naa;
leggTilPunkt(co2);
Serial.print(naa / 1000);
Serial.print(';');
Serial.println(co2);
}
settLys();
tegnSkjerm();
}
memmove flytter alle punktene ett hakk mot venstre når listen er full, så kurven ruller. Linjen skjerm.begin(SSD1306_SWITCHCAPVCC, 0x3C) forutsetter adresse 0x3C, som er vanlig på slike moduler. Står det noe annet på baksiden av din, endrer du tallet.
sekunder;co2_ppm 180;655 240;662 300;671
Nivå 3: VOC, støv og en logg du kan åpne i regneark
| Fra | Til | Merk |
|---|---|---|
| BME688 VCC | Plusskinne | 5 V |
| BME688 GND | Minusskinne | |
| BME688 SCL | OLED SCL | Felles Wire-buss |
| BME688 SDA | OLED SDA | Felles Wire-buss |
| Støvsensor pinne 1 (V-LED) | 150 Ω til 5 V og pluss på 220 µF | Minus på kondensatoren til GND |
| Støvsensor pinne 2 (LED-G) | Minusskinne | |
| Støvsensor pinne 3 (LED) | Kollektor på BC337 | Emitter til GND |
| Støvsensor pinne 4 (S-GND) | Minusskinne | |
| Støvsensor pinne 5 (Vo) | UNO A0 | Analog inngang |
| Støvsensor pinne 6 (VCC) | Plusskinne | 5 V |
| UNO D2 | 1,2 kΩ til basen på BC337 | Flat side mot deg: C, B, E fra venstre |
// Luftvakt nivå 3: CO2, VOC-trend og støvtrend på tre skjermsider, med CSV-logg.
// CO2L på Wire1 (Qwiic). OLED og BME688 på Wire. GP2Y1010AU0F på A0 og D2 via NPN-transistor.
#include <Wire.h>
#include <SensirionI2cScd4x.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_BME680.h>
const int STOV_LED = 2; // HIGH leder transistoren og tenner IR-dioden i støvsensoren
const int STOV_UT = A0;
const int LED_GRONN = 5;
const int LED_GUL = 6;
const int LED_ROD = 7;
const uint16_t GUL_FRA = 800; // vårt eget tidlige varsel, ikke en norm
const uint16_t ROD_OVER = 1000; // FHIs anbefalte faglige norm
const int STOV_TOPP = 100; // grovt anslag i ug/m3 som markerer en hendelse
const unsigned long OPPVARMING_MS = 180000UL;
const unsigned long LOGG_HVERT_MS = 60000UL;
const unsigned long SIDE_HVERT_MS = 5000UL;
const unsigned long BME_HVERT_MS = 10000UL;
const unsigned long STOV_HVERT_MS = 1000UL;
const int PUNKTER = 120; // ett punkt i minuttet gir to timer på skjermen
SensirionI2cScd4x sensor;
Adafruit_SSD1306 skjerm(128, 64, &Wire, -1);
Adafruit_BME680 miljo(&Wire);
uint16_t histCo2[PUNKTER];
uint16_t histVoc[PUNKTER];
uint16_t histStov[PUNKTER];
int antallPunkter = 0;
uint16_t co2 = 0;
float temperatur = 0.0;
float fukt = 0.0;
float gassKohm = 0.0;
float besteGass = 0.0;
float trykkHpa = 0.0;
int vocProsent = 100;
float stovVolt = 0.0;
float stovNull = 9.0; // laveste spenning vi har sett, regnes som ren luft
int stovAnslag = 0;
int side = 0;
unsigned long sisteLogg = 0;
unsigned long sisteSide = 0;
unsigned long sisteBme = 0;
unsigned long sisteStov = 0;
void leggTil(uint16_t *liste, uint16_t verdi) {
if (antallPunkter < PUNKTER) {
liste[antallPunkter] = verdi;
return;
}
memmove(liste, liste + 1, (PUNKTER - 1) * sizeof(uint16_t));
liste[PUNKTER - 1] = verdi;
}
// Regner om til y mellom 63 (min) og 27 (maks).
int tilY(long verdi, long minst, long mest) {
long y = 63L - (verdi - minst) * 36L / (mest - minst);
return constrain(y, 27, 63);
}
void tegnKurve(const uint16_t *liste, long minst, long mest) {
int start = 128 - antallPunkter;
for (int i = 1; i < antallPunkter; i++) {
skjerm.drawLine(start + i - 1, tilY(liste[i - 1], minst, mest), start + i, tilY(liste[i], minst, mest), SSD1306_WHITE);
}
}
void skrivHoyre(int y, const String &tekst) {
skjerm.setTextSize(1);
skjerm.setCursor(128 - int(tekst.length()) * 6, y);
skjerm.print(tekst);
}
void skrivStort(const String &tall, const char *enhet) {
skjerm.setTextSize(3);
skjerm.setCursor(0, 0);
skjerm.print(tall);
skjerm.setTextSize(1);
skjerm.setCursor(int(tall.length()) * 18 + 2, 17);
skjerm.print(enhet);
}
String medKomma(float verdi, int desimaler) {
String tekst(verdi, desimaler);
tekst.replace('.', ',');
return tekst;
}
void tegnCo2Side() {
skrivStort(String(co2), "ppm");
const char *status = "GOD";
if (millis() < OPPVARMING_MS) status = "VARMER";
else if (co2 > ROD_OVER) status = "LUFT UT!";
else if (co2 >= GUL_FRA) status = "SNART";
skrivHoyre(0, status);
skrivHoyre(10, medKomma(temperatur, 1) + " C");
int normY = tilY(ROD_OVER, 400, 2000);
for (int x = 8; x < 128; x += 4) {
skjerm.drawPixel(x, normY, SSD1306_WHITE);
}
tegnKurve(histCo2, 400, 2000);
}
void tegnVocSide() {
skrivStort(String(vocProsent), "%");
skrivHoyre(0, "VOC-TREND");
skrivHoyre(10, String(int(gassKohm)) + " kOhm");
tegnKurve(histVoc, 0, 100);
}
void tegnStovSide() {
skrivStort(String(stovAnslag), "ug/m3");
skrivHoyre(0, stovAnslag >= STOV_TOPP ? "TOPP!" : "STOV ca");
skrivHoyre(10, String(int(stovVolt * 1000.0)) + " mV");
tegnKurve(histStov, 0, 300);
}
void tegnSkjerm() {
skjerm.clearDisplay();
skjerm.setTextColor(SSD1306_WHITE);
if (side == 0) tegnCo2Side();
else if (side == 1) tegnVocSide();
else tegnStovSide();
skjerm.display();
}
// Én puls etter databladet: 0,32 ms på, avlesning etter 0,28 ms, periode 10 ms.
int stovPuls() {
unsigned long start = micros();
digitalWrite(STOV_LED, HIGH);
while (micros() - start < 280) {
}
int verdi = analogRead(STOV_UT);
while (micros() - start < 320) {
}
digitalWrite(STOV_LED, LOW);
while (micros() - start < 10000) {
}
return verdi;
}
void lesStov() {
long sum = 0;
for (int i = 0; i < 30; i++) {
sum += stovPuls();
}
stovVolt = (sum / 30.0) * 5.0 / 16383.0;
if (stovVolt < stovNull) {
stovNull = stovVolt;
}
// Typisk følsomhet i databladet: 0,5 V per 100 ug/m3, altså 200 ug/m3 per volt.
stovAnslag = int((stovVolt - stovNull) * 200.0);
}
void lesMiljo() {
if (!miljo.performReading()) {
return;
}
gassKohm = miljo.gas_resistance / 1000.0;
if (gassKohm <= 0.0) {
return;
}
trykkHpa = miljo.pressure / 100.0;
if (gassKohm > besteGass) {
besteGass = gassKohm;
}
vocProsent = int(gassKohm * 100.0 / besteGass);
// Riktig lufttrykk gjør CO2-målingen mer korrekt, særlig i høyden.
sensor.setAmbientPressure(uint32_t(miljo.pressure));
}
void settLys() {
bool varm = millis() >= OPPVARMING_MS;
digitalWrite(LED_GRONN, varm && co2 < GUL_FRA);
digitalWrite(LED_GUL, varm && co2 >= GUL_FRA && co2 <= ROD_OVER);
digitalWrite(LED_ROD, varm && co2 > ROD_OVER);
}
void loggLinje() {
Serial.print(millis() / 1000);
Serial.print(';');
Serial.print(co2);
Serial.print(';');
Serial.print(medKomma(temperatur, 1));
Serial.print(';');
Serial.print(medKomma(fukt, 0));
Serial.print(';');
Serial.print(medKomma(trykkHpa, 1));
Serial.print(';');
Serial.print(medKomma(gassKohm, 1));
Serial.print(';');
Serial.print(vocProsent);
Serial.print(';');
Serial.print(int(stovVolt * 1000.0));
Serial.print(';');
Serial.println(stovAnslag);
}
void setup() {
Serial.begin(115200);
pinMode(STOV_LED, OUTPUT);
digitalWrite(STOV_LED, LOW);
pinMode(LED_GRONN, OUTPUT);
pinMode(LED_GUL, OUTPUT);
pinMode(LED_ROD, OUTPUT);
analogReadResolution(14);
Wire.begin();
if (!skjerm.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println("Fant ikke OLED på 0x3C");
}
if (miljo.begin(0x77) || miljo.begin(0x76)) { // DFRobots veiledning bruker 0x77, skjemaet viser 0x76
miljo.setGasHeater(320, 150); // 320 C i 150 ms
} else {
Serial.println("Fant ikke BME688 på 0x77 eller 0x76");
}
Wire1.begin();
sensor.begin(Wire1, SCD41_I2C_ADDR_62);
sensor.wakeUp();
sensor.stopPeriodicMeasurement();
sensor.reinit();
if (sensor.startPeriodicMeasurement() != 0) {
Serial.println("Fant ikke SCD41 på Wire1");
}
Serial.println("sekunder;co2_ppm;temp_c;fukt_rf;trykk_hpa;gass_kohm;voc_prosent;stov_mv;stov_anslag");
}
void loop() {
unsigned long naa = millis();
if (naa - sisteStov >= STOV_HVERT_MS) {
sisteStov = naa;
lesStov();
}
if (naa - sisteBme >= BME_HVERT_MS) {
sisteBme = naa;
lesMiljo();
}
bool klar = false;
sensor.getDataReadyStatus(klar);
if (klar) {
uint16_t ny = 0;
float grader = 0.0;
float rf = 0.0;
if (sensor.readMeasurement(ny, grader, rf) == 0 && ny != 0) {
co2 = ny;
temperatur = grader;
fukt = rf;
}
}
if (naa >= OPPVARMING_MS && naa - sisteLogg >= LOGG_HVERT_MS) {
sisteLogg = naa;
leggTil(histCo2, co2);
leggTil(histVoc, vocProsent);
leggTil(histStov, stovAnslag);
if (antallPunkter < PUNKTER) {
antallPunkter++;
}
loggLinje();
}
if (naa - sisteSide >= SIDE_HVERT_MS) {
sisteSide = naa;
side = (side + 1) % 3;
}
settLys();
tegnSkjerm();
delay(100);
}
Tre ting er verdt å lese nøye. stovPuls() teller tiden med micros(), så avlesningen starter ved 0,28 ms og pulsen blir 0,32 ms, så sant analogRead blir ferdig innen pulsen. Det har vi ikke målt på kortet. stovNull er laveste spenning programmet har sett, så start det i ren luft. Og setAmbientPressure sender lufttrykket fra BME688 til SCD41, som bruker det til å rette CO₂-tallet. Her utfyller to av sensorene hverandre. BME688 kan svare på 0x77 eller 0x76. DFRobots veiledning bruker 0x77, men koblingsskjemaet for modulen viser 0x76, så programmet prøver begge.
sekunder;co2_ppm;temp_c;fukt_rf;trykk_hpa;gass_kohm;voc_prosent;stov_mv;stov_anslag 180;655;22,8;39;1012,4;96,3;92;891;0 240;662;22,9;39;1012,4;101,7;97;887;0 300;671;22,9;39;1012,3;104,5;100;1062;35
Loggen bruker semikolon og desimalkomma, så et norsk regneark åpner den uten omforming. Kopier linjene fra Serial Monitor til en tekstfil med endelsen .csv. Kortet har ingen klokke som er stilt, så første kolonne er sekunder siden start. Noter klokkeslettet når du starter.
Gassverdien sklir særlig etter oppstart. Boschs programvare rapporterer selv at nøyaktigheten er null i stabiliseringstiden. Vår tommelfingerregel er å lese trenden først etter minst en time.
Hvis det ikke virker
Fant ikke SCD41
Sjekk at kabelen sitter i Qwiic og ikke i en annen kontakt, og at programmet bruker Wire1. Kontroller at du har en Qwiic til Grove-kabel, ikke en vanlig Grove-kabel.
Skjermen er svart
Serial Monitor viser «Fant ikke OLED» hvis adressen er feil. Bytt 0x3C til 0x3D. Sammenlign rekkefølgen på pinnene med bildet på din egen modul: VCC og GND bytter plass på enkelte varianter.
CO₂ står fast på samme tall eller er 0
Programmet hopper over verdien 0, som betyr ugyldig måling. Står tallet helt stille, koble fra USB i ti sekunder og start på nytt, så sensoren får kald start.
Støvtallet er alltid 0
Den vanligste feilen er at IR-dioden ikke får puls. Sjekk transistorens retning, at D2 går via motstanden til basen, og at 150 Ω og 220 µF sitter på pinne 1. Hold et stykke papir inn i lufthullet: da skal millivolt-tallet hoppe, for da reflekteres mye lys.
Støvtallet er høyt hele tiden
Programmet starter med laveste spenning det ser. Startet du i en røykfylt krok, blir nullpunktet feil. Start på nytt i ren luft. Rengjør hullet med trykkluft hvis sensoren har stått i mye støv.
VOC-prosenten faller om natten uten grunn
Prosenten måles mot høyeste verdi siden start, og temperatur og fukt påvirker også gassmotstanden. Se etter en tydelig endring knyttet til en hendelse, ikke små bevegelser.
Prøv selv og neste steg
- Legg til en knapp som skriver en egen hendelseslinje i loggen: «lufter nå», «steker mat».
- Mål soverommet ditt én natt og legg resultatet oppå den beregnede kurven. Bruk formelen og finn ut hvor mye friskluft du faktisk får.
- Endre grensene for lampene og se om du lufter oftere.
- Gi Luftvakta en vifte på lav spenning. Reléprosjektet viser en trygg 5 V-kobling. Arbeid på 230 V skal gjøres av elektriker.
- Vil du vise temperatur på et enkelt tegndisplay i stedet, kan du bygge LCD 16 x 2-prosjektet først.
Liker du måling over tid og utenfor huset, er værstasjonen med LoRa og solcelle et naturlig neste prosjekt.
Hobbymåling eller sertifisert måling?
| Spørsmål | Luftvakta kan brukes | Bruk heller fagfolk eller godkjent utstyr |
|---|---|---|
| Lufter vi nok på soverommet? | Ja, CO₂ er målt direkte | Ved krav i skole eller arbeidsliv |
| Slår stekepanna ut? | Ja, som hendelse og trend | Ved mistanke om helseproblemer |
| Holder vi FHIs PM2,5-norm? | Nei | Kalibrert partikkelinstrument |
| Er det farlig gass i rommet? | Nei | Godkjent CO-alarm og røykvarsler |
| Hva skyldes lukten? | Nei, bare at noe endret seg | Luftprøve og analyse på laboratorium |
Luftvakta gir deg et godt grunnlag for å lufte oftere og finne kilder, men den er ikke en brannvarsler, et medisinsk apparat eller dokumentasjon overfor Arbeidstilsynet.
Vanlige spørsmål
Måler BME688 CO₂?
Nei. Den måler gassmotstand, temperatur, fuktighet og trykk. Eventuell eCO₂ er en beregnet størrelse.
Er TVOC én bestemt gass?
Nei. Det er en samleverdi for mange flyktige organiske forbindelser, og responsen varierer med sensoren og blandingen.
Kan jeg kalibrere CO₂-sensoren ved å ta den med ut?
SCD41 korrigerer seg selv når den ser frisk luft regelmessig. Du trenger sjelden å gjøre mer enn å lufte rommet. Tvungen kalibrering gir mening bare når du vet hva konsentrasjonen faktisk er.
Hvorfor viser sensoren under 400 ppm?
Uteluft ligger normalt rundt 420 ppm. Lavere tall betyr som regel at sensoren står i oppstart eller trenger frisk luft for å korrigere seg, ikke at lufta er bedre enn ute.
Hvordan virker en CO2-måler?
Den sender infrarødt lys gjennom lufta. CO₂ tar opp lys ved en bestemt bølgelengde, og sensoren regner dette om til ppm. En vanlig NDIR-sensor ser hvor mye lys som forsvinner, mens SCD41 lytter til trykkpulsene lyset lager i gassen.
Hvilken CO2-sensor passer til Arduino og ESP32?
CO2L med SCD41 er sensoren i dette prosjektet. Den bruker I²C og måler CO₂ direkte, og den kan brukes både med UNO R4 WiFi og med ESP32.
Kan en CO2-sensor kobles til Home Assistant?
Ja, med en ESP32 og ESPHome, som har en egen komponent for SCD4x over I²C. Da blir tallet en vanlig sensor i Home Assistant, uten programmet fra denne guiden. Start med ESP32-guiden hvis kortet er nytt for deg.
Virker dette på ESP32 også?
CO2L og BME688 passer godt på ESP32, som beskrevet i ESP32-guiden. GP2Y1010 krever 5 V, og signalet må deles ned før det går til en 3,3 V-inngang, så vi har valgt UNO R4 WiFi for hele prosjektet.
Hvor lenge varer sensorene?
Sensirion oppgir SCD41 som vedlikeholdsfri når automatisk kalibrering er på. Støvsensoren må holdes ren, og klebrige eller oljete stoffer kan tilsmusse den, så unngå å plassere den rett over komfyren.
Oppsummering og kilder
Velg sensor etter hva du vil vite, gi hvert tall riktig navn, og les trender før enkeltverdier. Tall og koblinger er kontrollert mot FHIs rapport 2015:1, Arbeidstilsynet om inneklima, forskrift om tiltaks- og grenseverdier, TEK17 § 13-2, Sensirions SCD4x-datablad, M5Stacks dokumentasjon for CO2L, Boschs BME688-datablad, DFRobots BME688-side, Sharps GP2Y1010-datablad og Arduinos datablad for UNO R4 WiFi.




