← Back to CoursesStemExpert
StemExpert
Intermediate · Handboek · 30. Datalogging en communicatie
StemExpert · Brecht Corbeel · schoolium.me
StemExpert
I30. Datalogging en communicatie
IIntermediate · deel I7 · Programmeren

Handboek · hoofdstuk 30Datalogging en communicatie

Een logboek dat zichzelf schrijft: van bits op een draad tot een grafiek in Python

De klimaatmodule meet om de twee seconden temperatuur, vochtigheid, licht en energie. Zonder geheugen is elke meting na twee seconden vergeten. In dit hoofdstuk bouw je de datalogger: de Mega praat met de computer over een seriële verbinding, leest sensoren over de I²C-bus, kent de datum dankzij een klokchip en schrijft elke minuut een regel op een SD-kaart, in het CSV-formaat van hoofdstuk 28. Daarna lees je een hele week in met Python, schoon je ze op en maak je er grafieken van die vertellen wat de module deed.

7× uitleg5× uitgewerkt voorbeeld1× proef1× ontleed2× naslag1× verhaal1× het geheel11× code16 opdrachten in het werkboek± 10 lestijden
Elke minuut een regel op de kaart: het logboek van de klimaatmodule.
Elke minuut een regel op de kaart: het logboek van de klimaatmodule.
Na dit hoofdstuk
Verhaal · 30.1

Zestig jaar meten op een vulkaan

1/29

In maart 1958 begon de Amerikaanse scheikundige Charles David Keeling met metingen van koolstofdioxide in de lucht, op de flank van de vulkaan Mauna Loa op Hawaï: ver van steden en bossen, zodat hij de lucht van de hele planeet meet en niet die van één fabriek. Na een paar jaar zag hij twee dingen. Elk jaar zakt het CO₂-gehalte in de lente en de zomer, als de planten op het noordelijk halfrond groeien, en stijgt het weer in de herfst. En van jaar tot jaar gaat het omhoog: van ongeveer 315 ppm in 1958 naar meer dan 420 ppm vandaag. Die grafiek heet de Keeling-curve.

Het geheim van de curve is niet één slimme meting, maar een logboek dat nooit stopt: elke meting met een datum en een uur, elke ijking genoteerd, elke onderbreking verklaard. Op 27 november 2022 barstte de Mauna Loa zelf uit; de lava sneed de weg en de stroomkabel naar het observatorium af. Vanaf 8 december werd er tijdelijk gemeten op de naburige Maunakea, en in de gegevens staat precies wanneer en waar. Zonder die notities zou niemand de meetreeksen van voor en na de uitbarsting kunnen vergelijken.

De datalogger van de klimaatmodule is veel kleiner, maar hij moet hetzelfde kunnen: meten op vaste tijden, elke meting een tijdstempel geven, alles bewaren in een vorm die een programma kan lezen, en eerlijk tonen wat er ontbreekt.

Waarom dit hoofdstuk ertoe doet

In hoofdstuk 28 kreeg je een CSV-bestand en analyseerde je het. Hier maak je het bestand zelf, van de sensor tot de kaart, en zie je waarom lege cellen, tijdstempels en een vaste vorm zo belangrijk zijn.

Een meetstation hoog op een vulkaan: ver van alles, om de lucht van de hele aarde te meten.
Een meetstation hoog op een vulkaan: ver van alles, om de lucht van de hele aarde te meten.
Uitleg · 30.2

Wat je al weet, en het plan voor de logger

2/29
wat je al kanwaarwat er nu bij komt
Serial.print voor debuggen, de seriële plotterhoofdstuk 29wat er op de draad gebeurt, en een protocol waar een programma mee overweg kan
de SHT31 met een bibliotheekhoofdstuk 29I²C zelf: adressen, pull-ups, bytes en een controlegetal
millis() en takenhoofdstuk 29een echte klok met datum
CSV inlezen met csv.DictReader, gemiddelden, matplotlibhoofdstuk 28het bestand zelf maken, en duizenden regels opschonen
binair en hexadecimaal, bits en byteshoofdstuk 25BCD, de bits van een teken, twee bytes tot één getal
RC-tijd τ = R · Choofdstuk 24waarom een lange I²C-kabel sterkere pull-ups vraagt

De logger van de klimaatmodule heeft vier soorten ingangen en drie uitgangen. De sensoren voor temperatuur, vocht, energie en tijd hangen aan de I²C-bus: twee draden voor allemaal samen. De lichtsensor blijft op A1. De Mega meet om de 2 s, telt de metingen van één minuut samen en schrijft dan één regel: op de SD-kaart, en tegelijk over de seriële poort naar de computer, waar Python ze kan lezen.

De regel heeft het formaat van hoofdstuk 28, met twee kolommen extra achteraan:

tijdstip,temperatuur_C,vocht_pct,verbruik_Wh,zon_Wh,licht_lx,batterij_V

Omdat je programma's uit hoofdstuk 28 de kolommen bij hun naam opvragen (rij["zon_Wh"]), werken ze zonder aanpassing op de nieuwe bestanden. Alleen staat er nu één regel per minuut, in plaats van één per drie uur.

SHT31 (I²C 0x44)T en RV van de kaslucht2 × INA226 (0x40, 0x41)zonnestroom en verbruikLDR op A1daglichtDS3231 (I²C 0x68)datum en tijdMega 2560om de 2 s meten,elke minuut eenregelSD-kaart (SPI)LOG/20261012.CSV, éénbestand per dagseriële poortdezelfde regel over USB,115 200 baudPython op de computerlees_serieel.py leest depoort, logboek.py de kaart
Vier sensorgroepen in, twee kopieën van elke regel uit: één op de kaart, één naar de computer.
Uitleg · 30.3

Seriële communicatie: bits op één draad

3/29

Een seriële verbinding stuurt de bits na elkaar over één draad: de uitgang TX (transmit) van de ene kant hangt aan de ingang RX (receive) van de andere, en omgekeerd; de massa (GND) is gedeeld. Er is geen klokdraad. Beide kanten spreken vooraf een snelheid af, de baudrate: bij 9600 baud duurt één bit . Het blok in de Mega dat dit doet, heet een UART. De Mega heeft er vier: Serial (pin 0 en 1, via de USB-chip naar de computer) en Serial1 tot Serial3 (pin 14 tot 19).

In rust is de lijn hoog (5 V). Een teken begint met een startbit: de lijn gaat één bittijd laag, zodat de ontvanger weet dat er iets komt en zijn klok gelijkzet. Dan volgen de acht databits, het minst belangrijke bit eerst (b0), en ten slotte een stopbit (hoog). Dat heet 8N1: 8 databits, geen pariteitsbit, 1 stopbit. Eén teken kost dus 10 bittijden.

Het teken '2' is in ASCII 50, of 0x32 = 0011 0010. Van b0 naar b7 is dat 0, 1, 0, 0, 1, 1, 0, 0: zo zie je het in het tijdsdiagram.

Alleen gelijke snelheden verstaan elkaar

Staat de seriële monitor op 9600 en de Mega op 115 200, dan meet de monitor elf keer te trage bits: je ziet onzin. Er is geen melding, alleen rare tekens. Kijk dan eerst naar de baudrate.

startb0b1b2b3b4b5b6b7stopTX (0 of 5 V)025050075010001250t (µs)
Het teken '2' zoals het op de TX-lijn staat: een startbit, b0 tot b7 en een stopbit, elk 104 µs lang.
Uitgewerkt voorbeeld · 30.4

9600 of 115 200 baud: hoeveel tijd kost een regel?

4/29

De logger stuurt elke minuut een regel van 48 tekens, plus \r\n. klimaat.ino stuurt elke minuut ook een statusregel van ongeveer 110 tekens. Hoe lang duurt het versturen bij 9600 en bij 115 200 baud? Wat doet Serial.print ondertussen?

Gegeven
  • 8N1: 10 bits per teken
  • zendbuffer van Serial: 64 tekens
Gevraagd
  • tijd per regel
  • hoe lang print() wacht
Oplossing
  1. 1
    Tekens per seconde. 9600 baud / 10 bits = 960 tekens per seconde; bij 115 200 baud 11 520.
  2. 2
    De logregel. 50 tekens: bij 9600, bij 115 200.
  3. 3
    De buffer. Serial.print zet tekens in een buffer van 64 en keert meteen terug; de UART stuurt ze op de achtergrond. Past een regel in de buffer, dan wacht print() niet. Van de statusregel van 110 tekens moeten er 46 wachten tot er plaats is: bij 9600 baud , bij 115 200 .
  4. 4
    Overzicht:
    baudratetijd per bittekens per seconderegel van 48 tekens + \r\n
    9600104,2 µs96052,08 ms
    19 20052,1 µs192026,04 ms
    57 60017,4 µs57608,68 ms
    115 2008,7 µs11 5204,34 ms
Antwoord
Bij 115 200 baud kost een regel ongeveer 4 ms; bij 9600 baud 52 ms, en dan houdt een lange regel de lus 48 ms op.
Klopt dit? Past het in een minuut? Ruimschoots: zelfs bij 9600 baud is de lijn maar 0,1 % van de tijd bezig. Het probleem is niet de hoeveelheid, maar dat print() de lus kan ophouden. Kies daarom 115 200 baud.
Code · 30.5

Een eigen protocol: één regel per meting

5/29

Een protocol is een afspraak over de vorm van de berichten. Voor de logger kiezen we het eenvoudigste dat een programma aan de andere kant zeker begrijpt:

  • elke meting is één regel, afgesloten met \r\n (println); de regelovergang is het einde van het bericht;
  • de velden staan gescheiden door komma's, met een decimale punt;
  • de eerste gewone regel is de kop met de namen van de kolommen;
  • een regel die met # begint, is commentaar of een melding: een programma slaat hem over;
  • geen geldige meting: de cel blijft leeg, nooit 0 of −999.
C++ (Arduino)serieel_csv.ino30 regelsDownload
// serieel_csv.ino - om de 2 s een meting als CSV-regel naar de computer#include <Wire.h>#include <Adafruit_SHT31.h>#include "pinnen.h" Adafruit_SHT31 sht = Adafruit_SHT31();unsigned long vorige = 0; void setup() {  Serial.begin(115200);  sht.begin(0x44);  Serial.println("# klimaatmodule: een meting om de 2 s");   // regels met # zijn commentaar  Serial.println("tijd_s,temperatuur_C,vocht_pct,licht_ruw");  // de kop: namen van de kolommen} void loop() {  unsigned long nu = millis();  if (nu - vorige >= 2000) {    vorige += 2000;    float t, rv;    bool ok = sht.readBoth(&t, &rv);   // false als de sensor niet (goed) antwoordde    Serial.print(nu / 1000);    Serial.print(',');    if (ok) Serial.print(t, 2);        // geen geldige meting: de cel blijft leeg    Serial.print(',');    if (ok) Serial.print(rv, 1);    Serial.print(',');    Serial.println(analogRead(LDR));   // println: de regel eindigt op \r\n  }}

Wat de computer ontvangt (in de simulatie zit het stekkertje van de SHT31 tussen 7 en 9 s even los):

# klimaatmodule: een meting om de 2 s
tijd_s,temperatuur_C,vocht_pct,licht_ruw
2,23.50,64.0,477
4,23.61,63.8,478
6,23.70,63.6,479
8,,,480
10,23.90,63.2,481
12,24.01,63.0,482

De regel van 8 s heeft twee lege cellen: de meting mislukte, en dat staat er eerlijk. Een 0 zou een programma als een echte temperatuur van 0 °C lezen.

Code · 30.6

De regels inlezen in Python

6/29

Aan de kant van de computer leest Python de regels met de bibliotheek pyserial (installeren met pip install pyserial; je importeert ze als serial). Welke poort de Mega heeft, zie je in de Arduino-IDE (Tools, Port) of met python -m serial.tools.list_ports. Sluit eerst de seriële monitor: een poort kan maar door één programma tegelijk open zijn.

Pythonlees_serieel.py25 regelsDownload
# lees_serieel.py - metingen van de Mega over de seriële poort inlezen en bewarenimport csvfrom datetime import datetime import serial                                        # eenmalig installeren: pip install pyserial poort = serial.Serial("COM3", 115200, timeout=5)     # Linux: "/dev/ttyACM0", macOS: "/dev/cu.usbmodem…"kop = Nonewith open("metingen.csv", "w", newline="", encoding="utf-8") as bestand:    schrijver = csv.writer(bestand)    for _ in range(9):                               # voor de test negen regels; echt meten: while True        regel = poort.readline().decode("utf-8", errors="replace").strip()        velden = regel.split(",")        if regel.startswith("#"):            print("commentaar:", regel)        elif velden[0] == "tijd_s":                  # de kop: de namen van de kolommen            kop = velden            schrijver.writerow(["pc_tijd"] + kop)        elif kop is None or len(velden) != len(kop):            print("overgeslagen:", regel)        else:            nu = datetime.now().strftime("%Y-%m-%d %H:%M:%S")   # de computer kent de datum wel            schrijver.writerow([nu] + velden)            print(nu, velden)poort.close()

Uitvoer:

overgeslagen: 63.8,478
commentaar: # klimaatmodule: een meting om de 2 s
2026-10-12 10:00:00 ['2', '23.50', '64.0', '477']
2026-10-12 10:00:02 ['4', '23.61', '63.8', '478']
2026-10-12 10:00:04 ['6', '23.70', '63.6', '479']
2026-10-12 10:00:06 ['8', '', '', '480']
2026-10-12 10:00:08 ['10', '23.90', '63.2', '481']
2026-10-12 10:00:10 ['12', '24.01', '63.0', '482']

readline() wacht tot er een hele regel binnen is (tot \n) en geeft bytes; decode maakt er tekst van, strip haalt \r\n weg. De eerste regel is een restje van vóór het openen van de poort: hij heeft te weinig velden en er is nog geen kop, dus hij wordt overgeslagen. De computer zet er zijn eigen datum en tijd voor, want serieel_csv.ino kent alleen de seconden sinds de start.

Zo draait dit programma hier

In dit boek praat het programma met een nagebootste poort die de regels teruggeeft van de simulatie op het vorige blad, en een klok die om de 2 s verspringt. Op je computer gebruik je de echte poort, precies zoals in de code. Let op: het openen van de poort herstart de Mega (de USB-chip trekt de reset even laag); de eerste 2 s komt er niets.

Uitleg · 30.7

I²C: twee draden, veel sensoren

7/29

De Mega heeft maar een handvol seriële poorten, en elke sensor een eigen poort geven lukt niet. I²C (spreek uit: i-kwadraat-c) is een bus: tot ongeveer honderd chips hangen aan dezelfde twee draden, SDA (data) en SCL (klok). De Mega is de master: hij maakt de klok en begint elk gesprek. De sensoren zijn slaves: ze antwoorden alleen als ze aangesproken worden.

Elke chip heeft een adres van 7 bits. Een gesprek gaat zo: een START (SDA zakt terwijl SCL hoog is), dan het adres met één bit erachter (0 = schrijven, 1 = lezen), dan geeft de aangesproken chip een ACK (hij trekt SDA laag bij de negende klokpuls). Daarna volgen de databytes, elk met een ACK, en een STOP (SDA stijgt terwijl SCL hoog is). Antwoordt er niemand op een adres, dan blijft SDA hoog: een NACK, en Wire.endTransmission() geeft 2 terug in plaats van 0.

De chips maken een lijn nooit actief hoog: ze kunnen ze alleen laag trekken of loslaten (open drain). Een pull-upweerstand naar 5 V trekt de losgelaten lijn weer hoog. Zo kunnen twee chips die tegelijk iets zeggen, nooit kortsluiting maken. De Mega heeft 10 kΩ pull-ups op pin 20 en 21.

adreschipwathoe
0x40INA226stroom en spanning van de zonnepanelenA0 en A1 aan GND
0x41INA226stroom en spanning naar de verbruikersA0 aan VS
0x44SHT31temperatuur en vochtigheid van de luchtADDR aan GND (0x45 aan VDD)
0x57AT24C32geheugenchipje op het RTC-moduletje (niet gebruikt)vast op het moduletje
0x68DS3231klok met datumvast
start0x44 + WACK0x24ACK0x00ACKSCL (klok)SDA (data)−104090140190240290t (µs)
Drie bytes, elk gevolgd door een ACK van de SHT31; het strookje achteraan is de STOP. De data verandert alleen als SCL laag is.
Ontleed · 30.8

De I²C-bus van de klimaatmodule

8/29

Vier chips op twee draden, en de SD-kaart apart op de SPI-bus. De INA226's en de klok zitten in de elektronicakast naast de Mega; de SHT31 zit in de sensorkast in de kweekruimte, 3 m verder.

Mega 2560SDA 20SCL 2150-53 SPIGND+5 VSDASCL2 × 2,2 kΩINA226zon0x40INA226verbruik0x41SHT31sensorkast,3 m0x44DS3231klok0x68SD-kaartCS = 53
Alle chips parallel op SDA en SCL; de adressen maken het verschil.
Wijs een nummer aan: de figuur zoomt in. Klik om vast te zetten.
  1. 1
    De Mega: master

    Maakt de klok op SCL (100 kHz) en begint elk gesprek. De bibliotheek Wire doet het werk; SDA is pin 20, SCL pin 21.

  2. 2
    Pull-ups

    Houden SDA en SCL hoog als niemand ze laag trekt. Door de lange kabel naar de sensorkast komen er 2,2 kΩ bij de 10 kΩ van de Mega (volgend voorbeeld).

  3. 3
    INA226 zon (0x40)

    Meet de spanning over een shunt in de leiding van de laadregelaar: de zonnestroom.

  4. 4
    INA226 verbruik (0x41)

    Dezelfde chip, een ander adres: pin A0 aan de voeding in plaats van aan GND.

  5. 5
    SHT31 (0x44)

    Temperatuur en vocht van de kaslucht, in de sensorkast op 3 m kabel.

  6. 6
    DS3231 (0x68)

    De klok. Op het moduletje zit ook een geheugenchipje op 0x57, dat de scanner ook vindt.

  7. 7
    SD-kaart op SPI

    Niet op I²C: de kaart praat SPI, via pin 50 tot 53. CS (53) kiest de kaart.

  8. 8
    Eén bus, vier chips

    Wat er op de bus gebeurt, horen alle chips; alleen de chip met het juiste adres antwoordt.

Code · 30.9

Wie zit er op de bus? De I²C-scanner

9/29

Het eerste programma dat je op een nieuwe I²C-bus draait: het probeert elk adres van 1 tot 126 en meldt waar er een ACK komt. Zo zie je meteen of een sensor goed aangesloten is, en welk adres hij echt heeft (dat staat niet altijd juist op het bordje).

C++ (Arduino)i2c_scanner.ino22 regelsDownload
// i2c_scanner.ino - welke adressen antwoorden op de I2C-bus?#include <Wire.h> void setup() {  Serial.begin(115200);  Wire.begin();  int gevonden = 0;  for (byte adres = 1; adres < 127; adres++) {    Wire.beginTransmission(adres);         // alleen het adres sturen, geen data    if (Wire.endTransmission() == 0) {     // 0 = er kwam een ACK: hier zit een apparaat      Serial.print("apparaat op 0x");      if (adres < 16) Serial.print('0');      Serial.println(adres, HEX);      gevonden++;    }  }  Serial.print(gevonden);  Serial.println(" apparaten gevonden");} void loop() {}

Uitvoer met de bus van de klimaatmodule:

apparaat op 0x40
apparaat op 0x41
apparaat op 0x44
apparaat op 0x57
apparaat op 0x68
5 apparaten gevonden

Vijf antwoorden voor vier moduletjes: het klokmoduletje heeft ook een geheugenchip (0x57). Vindt de scanner niets, dan zijn het bijna altijd de draden: SDA en SCL verwisseld, geen gemeenschappelijke GND, of geen voeding op het bordje. Vindt hij een chip op een onverwacht adres, pas dan je code aan of verander de adrespin.

Uitgewerkt voorbeeld · 30.10

Pull-ups voor een kabel van 3 m

10/29

De SHT31 zit in de sensorkast, 3 m van de Mega. De kabel heeft tussen twee aders een capaciteit van ongeveer 100 pF per meter; de vier chips en de Mega voegen elk ongeveer 10 pF toe. Werkt de bus op 100 kHz met alleen de 10 kΩ van de Mega? Zo niet: welke extra pull-up kies je?

0,00,51,01,52,02,53,03,54,0012345t (µs)U (V)0,3 · 5 V: nog laag0,7 · 5 V: hoog1 µs10 kΩ (alleen de Mega)1,8 kΩ (met 2,2 kΩ erbij)
Met 10 kΩ komt SDA na 1 µs niet eens tot 3,5 V; met 1,8 kΩ ruim op tijd.
Gegeven
  • I²C-norm bij 100 kHz: stijgtijd (van 30 % tot 70 % van 5 V) hoogstens 1000 ns
  • een chip moet 3 mA kunnen wegtrekken bij 0,4 V
  • 10 kΩ op de Mega
Gevraagd
  • de stijgtijd met 10 kΩ
  • een geschikte extra weerstand
Oplossing
  1. 1
    Capaciteit. ; reken met 340 pF.
  2. 2
    Stijgen is een RC-laadcurve (hoofdstuk 24): . Van 30 % naar 70 % duurt .
  3. 3
    Met 10 kΩ: , : bijna drie keer te traag. De bus haalt dan geen 100 kHz; een bit wordt verkeerd gelezen of de chip antwoordt niet.
  4. 4
    Grenzen voor R. Hoogstens . Minstens , anders kan een chip de lijn niet laag genoeg trekken.
  5. 5
    Keuze. 2,2 kΩ extra, parallel met de 10 kΩ: . Dan en .
Antwoord
Met alleen 10 kΩ is de stijgtijd 2,9 µs (te traag). Met 2,2 kΩ extra op SDA en op SCL: 1,8 kΩ en 0,52 µs.
Klopt dit? Tel alle pull-ups op de bus mee: veel breakoutbordjes (SHT31, INA226) hebben er zelf 10 kΩ op. Met twee zulke bordjes erbij zou 10 ∥ 10 ∥ 10 ∥ 2,2 kΩ = 1,4 kΩ te klein worden; dan kies je 3,3 kΩ extra of haal je de pull-ups van een bordje af. Op nog langere kabels gebruik je een I²C-busversterker.
Code · 30.11

De SHT31 zonder bibliotheek

11/29

In hoofdstuk 29 las je de SHT31 met readBoth. Zo doet de bibliotheek het, stap voor stap volgens de datasheet:

uit de datasheet (SHT3x-DIS)waarde
adres0x44 (ADDR aan GND) of 0x45 (ADDR aan VDD)
commando één meting, hoge herhaalbaarheid, zonder clock stretching0x24 0x00
duur van die metingtypisch 12,5 ms, hoogstens 15 ms
antwoord6 bytes: T (2), CRC, RV (2), CRC; eerst de hoge byte
omrekening (°C), (%)
CRCCRC-8, veelterm 0x31, beginwaarde 0xFF; CRC(0xBE 0xEF) = 0x92
nauwkeurigheid SHT31 (typisch)±0,2 °C, ±2 % RV
voeding, stroom2,15 tot 5,5 V; 0,6 mA tijdens een meting, 0,2 µA in rust

Het programma stuurt het commando, wacht 20 ms, leest zes bytes, controleert beide CRC's en rekent om. Een CRC is een controlegetal dat de sensor uit de twee databytes berekent; de Mega rekent het na. Valt er onderweg een bit om (storing op de lange kabel), dan klopt het niet meer en gooit het programma de meting weg.

C++ (Arduino)sht31_zelf.ino73 regelsDownload
// sht31_zelf.ino - de SHT31 uitlezen zonder bibliotheek, stap voor stap volgens de datasheet#include <Wire.h> const byte ADRES = 0x44;             // pin ADDR aan GND (aan VDD: 0x45)float luchtT = NAN, luchtRV = NAN; byte crc8(byte a, byte b) {          // CRC-8 over twee bytes: veelterm 0x31, beginwaarde 0xFF  byte crc = 0xFF;  byte data[2] = {a, b};  for (int i = 0; i < 2; i++) {    crc ^= data[i];    for (int bit = 0; bit < 8; bit++) {      if (crc & 0x80) crc = (crc << 1) ^ 0x31;      else crc = crc << 1;    }  }  return crc;} void printHex(byte b) {  if (b < 16) Serial.print('0');  Serial.print(b, HEX);  Serial.print(' ');} bool leesSht31() {  Wire.beginTransmission(ADRES);  Wire.write(0x24);                  // commando 0x2400: één meting, hoge herhaalbaarheid,  Wire.write(0x00);                  // zonder 'clock stretching'  if (Wire.endTransmission() != 0) { // geen ACK: sensor los of verkeerd adres    Serial.println("geen antwoord op 0x44");    return false;  }  delay(20);                         // de meting duurt tot 15 ms (datasheet)  if (Wire.requestFrom(ADRES, (byte)6) != 6) {    Serial.println("minder dan 6 bytes ontvangen");    return false;  }  byte b[6];  for (int i = 0; i < 6; i++) b[i] = Wire.read();  Serial.print("bytes: ");  for (int i = 0; i < 6; i++) printHex(b[i]);  if (crc8(b[0], b[1]) != b[2] || crc8(b[3], b[4]) != b[5]) {    Serial.println(" CRC fout: meting weggegooid");    return false;  }  unsigned int ruwT = ((unsigned int)b[0] << 8) | b[1];      // twee bytes tot één getal van 16 bit  unsigned int ruwRV = ((unsigned int)b[3] << 8) | b[4];  luchtT = -45.0 + 175.0 * ruwT / 65535.0;                   // formules uit de datasheet  luchtRV = 100.0 * ruwRV / 65535.0;  Serial.print(" -> ");  Serial.print(ruwT);  Serial.print(" en ");  Serial.print(ruwRV);  Serial.print(" -> ");  Serial.print(luchtT, 2);  Serial.print(" C, ");  Serial.print(luchtRV, 1);  Serial.println(" %");  return true;} void setup() {  Serial.begin(115200);  Wire.begin();  Serial.print("test CRC(BE EF) = ");            // het voorbeeld uit de datasheet: 92  Serial.println(crc8(0xBE, 0xEF), HEX);} void loop() {  leesSht31();  delay(2000);                                   // testschets: hier mag het}

Uitvoer: eerst de test met het voorbeeld uit de datasheet, dan drie metingen; bij de derde valt een bit om in de tweede byte, bij de vierde is de sensor losgekoppeld:

test CRC(BE EF) = 92
bytes: 64 29 59 9C ED F5  -> 25641 en 40173 -> 23.47 C, 61.3 %
bytes: 64 31 A3 9C AB 6E  -> 25649 en 40107 -> 23.49 C, 61.2 %
bytes: 64 3C 2B 9C 6A 18  CRC fout: meting weggegooid
geen antwoord op 0x44

Narekenen: 0x6429 = 25 641, en °C. Twee bytes worden één getal met (hoog << 8) | laag: de hoge byte schuift acht plaatsen naar links, zoals 0x64 · 256. Wie de fout negeert, krijgt bij een losse sensor 0xFF 0xFF (Wire.read() geeft −1): dat is precies 130,0 °C en 100,0 %. Je vindt die waarden verderop terug in echte logbestanden.

Uitleg · 30.12

Tijd: waarom millis() geen klok is

12/29

Een meting zonder tijdstip is weinig waard. millis() lijkt een klok, maar volstaat niet:

  • ze kent geen datum, alleen de tijd sinds de start;
  • na een stroomonderbreking of reset begint ze weer bij 0;
  • ze is onnauwkeurig: de 16 MHz van de Mega komt van een keramische resonator die tot ±0,5 % mag afwijken, in het slechtste geval ruim 7 minuten per dag, en ze verloopt met de temperatuur;
  • ze loopt na 49,7 dagen over.

Een RTC (real-time clock) lost dat op. De DS3231 heeft een eigen kwartskristal met temperatuurcorrectie: ±2 ppm tussen 0 en 40 °C, hoogstens ongeveer een minuut per jaar. Een knoopcel houdt hem jaren aan de gang als de module geen stroom heeft (hij verbruikt dan ongeveer een microampère). De Mega leest hem via I²C op adres 0x68. De tijd staat in registers 0x00 tot 0x06 in BCD (binair gecodeerd decimaal): elk cijfer apart in 4 bits, zodat 34 minuten als 0x34 = 0011 0100 staat.

Welke tijd? In België springt de klok twee keer per jaar een uur. In het najaar komt 2:30 dan twee keer voor, en in de lente ontbreekt een uur: een logboek met gewone kloktijd heeft dubbele en ontbrekende regels. De logger loopt daarom het hele jaar op wintertijd (UTC+1). In de zomer loopt hij dus een uur achter op je horloge: 12:00 in het logboek is 13:00 zomertijd.

Eén keer zetten, dan jaren niet meer

RTClib kan de klok zetten op het moment van compileren: rtc.adjust(DateTime(F(__DATE__), F(__TIME__))). Dat is de tijd van je computer, met zomertijd en een paar seconden te laat. logger.ino laat je de klok zetten met een opdracht vanaf de computer; opdracht 30.14 werkt dat uit.

56341201121026secondenminutenurenweekdagdagmaandjaar0x000x010x020x030x040x050x0612:34:56 op maandag 12-10-2026 in de registers van de DS3231
Elk vakje is een register van één byte; in hexadecimaal lees je de cijfers gewoon af.
Code · 30.13

De klok lezen: DS3231 en BCD

13/29

Twee manieren: met de bibliotheek RTClib (Adafruit, via de bibliotheekbeheerder), en zelf, met de registers. Het patroon 'registerwijzer zetten, dan bytes lezen' gebruiken ook de INA226 en veel andere I²C-chips.

C++ (Arduino)klok.ino69 regelsDownload
// klok.ino - de DS3231 lezen: met RTClib, en daarna de registers zelf (BCD)#include <Wire.h>#include <RTClib.h> RTC_DS3231 rtc; byte bcdNaarGetal(byte b) {            // 0x34 -> 3 * 10 + 4 = 34  return (b >> 4) * 10 + (b & 0x0F);} void printTwee(int x) {  if (x < 10) Serial.print('0');  Serial.print(x);} void setup() {  Serial.begin(115200);  Wire.begin();  if (!rtc.begin()) {    Serial.println("# geen DS3231 op adres 0x68");    return;  }  if (rtc.lostPower()) Serial.println("# de klok heeft stilgestaan: stel ze opnieuw in");   DateTime nu = rtc.now();             // 1. met de bibliotheek  Serial.print("RTClib:    ");  Serial.print(nu.year());  Serial.print('-');  printTwee(nu.month());  Serial.print('-');  printTwee(nu.day());  Serial.print(' ');  printTwee(nu.hour());  Serial.print(':');  printTwee(nu.minute());  Serial.print(':');  printTwee(nu.second());  Serial.println();   Wire.beginTransmission(0x68);        // 2. zelf: registerwijzer op 0x00 ...  Wire.write(0x00);  Wire.endTransmission();  Wire.requestFrom(0x68, 7);           // ... en zeven registers lezen  byte r[7];  Serial.print("registers:");  for (int i = 0; i < 7; i++) {    r[i] = Wire.read();    Serial.print(" ");    if (r[i] < 16) Serial.print('0');    Serial.print(r[i], HEX);  }  Serial.println();  Serial.print("zelf:      20");  printTwee(bcdNaarGetal(r[6]));                  // jaar  Serial.print('-');  printTwee(bcdNaarGetal(r[5] & 0x1F));           // maand (bit 7 is de eeuw)  Serial.print('-');  printTwee(bcdNaarGetal(r[4]));                  // dag  Serial.print(' ');  printTwee(bcdNaarGetal(r[2] & 0x3F));           // uur (24-uursmodus)  Serial.print(':');  printTwee(bcdNaarGetal(r[1]));  Serial.print(':');  printTwee(bcdNaarGetal(r[0] & 0x7F));  Serial.println();} void loop() {}

Uitvoer:

RTClib:    2026-10-12 12:34:56
registers: 56 34 12 01 12 10 26
zelf:      2026-10-12 12:34:56

bcdNaarGetal neemt de hoge helft (b >> 4, de tientallen) en de lage helft (b & 0x0F, de eenheden). In twee registers staan extra bits die je eerst wegmaskert: bit 7 van de maand is de eeuw, bit 6 van de uren kiest de 12-uursmodus. Register 3 is de dag van de week (RTClib: maandag = 1). RTClib doet dit allemaal voor je en geeft een DateTime.

Uitleg · 30.14

De SD-kaart: SPI, bestanden en flush

14/29

Een SD-kaart praat SPI: een snelle bus met een klokdraad (SCK), een draad van de Mega naar de kaart (MOSI), een terug (MISO) en een selectiedraad (CS) per apparaat. Op de Mega zijn dat pin 52, 51, 50 en 53. Een kaart werkt op 3,3 V: gebruik een moduletje met een spanningsregelaar en een niveauomzetter. SPI werk je in Expert (hoofdstuk E37) verder uit; de bibliotheek SD regelt het.

De bibliotheek leest kaarten met een FAT16- of FAT32-bestandssysteem (formatteer ze met de SD Card Formatter van de SD Association) en kent alleen korte 8.3-namen: hoogstens acht tekens, een punt en drie. 2026-10-12.CSV mislukt, 20261012.CSV lukt. De logger maakt één bestand per dag, in een map LOG: een map omdat de hoofdmap van een FAT16-kaart maar 512 namen kan bevatten, en per dag omdat een klein bestand makkelijk te kopiëren is en bij een fout niet alles meesleurt.

Een kaart schrijft in blokken van 512 bytes. println vult eerst een buffer in het SRAM; pas bij flush() of close() gaat alles echt naar de kaart en komt de nieuwe bestandslengte in de map. De logger opent het bestand elke minuut, schrijft één regel en sluit het: zo verlies je bij een stroomonderbreking hoogstens de lopende minuut.

Een kaart uittrekken terwijl de logger schrijft

Trek de kaart er alleen uit als de logger niet schrijft (vlak na een volle minuut is veilig), of schakel eerst de voeding uit. Hoofdstuk 24 berekende dat een supercondensator de Mega 3,1 s in leven houdt: genoeg om bij een stroomonderbreking het bestand netjes te sluiten.

1. schrijvenlogger.ino:f.println(regel)2. buffer512 bytes in hetSRAM van de Mega3. wegschrijvenflush() ofclose(): het bloknaar de kaart4. mapde nieuwelengte in demapValt de stroom uit tussen 1 en 3, dan staat de nieuwe bestandslengte nog niet in de map:alles sinds de vorige flush() is weg, ook wat al in volle blokken op de kaart stond.
Een regel gaat eerst naar een buffer; pas flush() of close() legt hem echt vast.
Uitgewerkt voorbeeld · 30.15

Hoeveel dagen past er op 1 GB?

15/29

De logger schrijft elke minuut een regel. Hoe groot wordt een dagbestand, hoe lang doet een kaart van 1 GB ermee, en wat gebeurt er als ze toch vol is?

Gegeven
  • een regel is 44 tot 49 tekens plus \r\n (één byte per teken)
  • de kop: 71 tekens plus \r\n
  • 1440 minuten per dag
Gevraagd
  • bytes per dag
  • aantal dagen op 1 GB
Oplossing
  1. 1
    Per dag, geschat. Met gemiddeld 47,5 bytes per regel: bytes.
  2. 2
    Nagemeten op de zeven dagbestanden van de testweek (verderop in dit hoofdstuk): gemiddeld 68 514 bytes per dag.
  3. 3
    1 GB. dagen, of ongeveer 40 jaar. Een bestand neemt op de kaart wel een geheel aantal clusters in (enkele kB tot 32 kB); per bestand gaat hoogstens één cluster verloren, dat verandert weinig.
  4. 4
    Kaart vol. Dan schrijft println minder tekens dan gevraagd. logger.ino vergelijkt het aantal met dat van dezelfde regel naar Serial, meldt het en zet de statusled aan. De simulatie met een 'kaart' van 200 bytes:
    # SD-kaart klaar
    tijdstip,temperatuur_C,vocht_pct,verbruik_Wh,zon_Wh,licht_lx,batterij_V
    2026-10-12 06:00,21.0,55.0,0.09,0.00,4004,13.12
    2026-10-12 06:01,21.0,55.0,0.09,0.00,4004,13.12
    2026-10-12 06:02,21.0,55.0,0.09,0.00,4004,13.12
    # SD: kaart vol, regel niet bewaard
Antwoord
Ongeveer 68,5 kB per dag; 1 GB volstaat voor ruim 40 jaar. De kaart is niet de beperking.
Klopt dit? Wat wel beperkt: een kaart kan stukgaan en elke schrijfbeurt slijt de cellen een beetje (de kaart spreidt dat zelf over de hele kaart). Kopieer de bestanden geregeld naar de computer, en kies voor een logger die jaren draait een kaart van een bekend merk, liefst een 'high endurance'-type.
Uitleg · 30.16

Energie meten met de INA226

16/29

Voor verbruik_Wh en zon_Wh moet de logger stroom meten. De ACS712-20A op A4 (hoofdstuk 16) meet tot 20 A, maar op een heldere middag laden de panelen de batterij met tot : daar loopt hij tegen zijn grens. De logger gebruikt daarom twee INA226-chips, elk met een shunt van 100 A / 75 mV (0,75 mΩ): één in de leiding van de laadregelaar, één in de leiding naar de verbruikers. De batterijstroom volgt uit de knooppuntwet (hoofdstuk 21).

De INA226 meet de kleine spanning over de shunt (tot 81,92 mV, in stappen van 2,5 µV) en de spanning van de rail (in stappen van 1,25 mV), en geeft ze via I²C. Twee dezelfde chips op één bus kan, omdat je met de pinnen A0 en A1 zestien adressen kan kiezen.

registernaamper stapin logger.ino
0x00configuratie0x4927: 128 metingen middelen, 1,1 ms per conversie, continu
0x01shuntspanning (met teken)2,5 µVstroom = stappen · 2,5 µV / 0,75 mΩ = stappen · 3,33 mA
0x02busspanning1,25 mVde spanning van de 12,8 V-rail
0xFEfabrikant0x5449 ('TI'): handig om te testen of je de juiste chip leest

De groeilampen worden met PWM gedimd (490 Hz, hoofdstuk 25): de stroom springt 490 keer per seconde tussen 0 en 15 A. Eén meting van 1,1 ms zou op een willekeurig moment vallen (aliasing). Daarom middelt de INA226 128 metingen van spanning en stroom: , bijna 140 PWM-perioden. De logger rekent zo: om de 2 s, het gemiddelde over een minuut, maal 1/60 h = Wh.

laadregelaar1,40 kWpshunt 0,75 mΩshunt 0,75 mΩverbruikerslampen, pomp, …2 × LiFePO4INA2260x40INA2260x41I zonI verbruikI batterijknooppunt: I zon = I verbruik + I batterij (hoofdstuk 21)
Twee metingen, en de derde stroom volgt uit de knooppuntwet. De minleidingen zijn niet getekend.
Code · 30.17

logger.ino: de datalogger van de klimaatmodule

17/29

Alles samen: om de 2 s meten, per minuut optellen, bij een nieuwe minuut één regel naar de computer en naar de kaart. De regel wordt geschreven door één functie, schrijfRegel(Print &uit), die zowel Serial als een File aankan: beide zijn een Print, iets waar je print() op kan doen. Zo zijn de twee kopieën gegarandeerd gelijk. Zet pinnen.h uit hoofdstuk 29 in dezelfde map, en installeer RTClib en Adafruit SHT31 met de bibliotheekbeheerder.

C++ (Arduino)logger.ino198 regelsDownload
// logger.ino - de datalogger van de klimaatmodule (Arduino Mega 2560, hoofdstuk 30)// Om de 2 s meten; elke minuut één CSV-regel op de SD-kaart (map LOG, één bestand per dag) en naar de computer.// I2C (SDA 20, SCL 21): INA226 zon 0x40, INA226 verbruik 0x41, SHT31 0x44, DS3231 0x68.// SD-kaart op SPI: 50 MISO, 51 MOSI, 52 SCK, 53 CS. De klok loopt het hele jaar op wintertijd (UTC+1).// De klok zetten vanaf de computer: stuur  T2026-06-01 06:00:00  en Enter.#include <Wire.h>#include <SPI.h>#include <SD.h>#include <RTClib.h>#include <Adafruit_SHT31.h>#include "pinnen.h" const unsigned long MEET_MS = 2000;const int SD_CS = 53;const byte INA_ZON = 0x40;            // INA226 in de leiding van de laadregelaar (A0 en A1 aan GND)const byte INA_VERBRUIK = 0x41;       // INA226 in de leiding naar de verbruikers (A0 aan VS)const float R_SHUNT = 0.00075;        // ohm: shunt van 100 A / 75 mVconst char KOP[] = "tijdstip,temperatuur_C,vocht_pct,verbruik_Wh,zon_Wh,licht_lx,batterij_V"; RTC_DS3231 rtc;Adafruit_SHT31 sht = Adafruit_SHT31();bool kaartOk = false;unsigned long vorigeMeting = 0;DateTime minuut;                      // de minuut waarvan we nu metingen verzamelenbool bezig = false; float somT = 0, somRV = 0, somLux = 0, somZon = 0, somVerbruik = 0, somU = 0;   // sommen over één minuutint nT = 0, nMeting = 0, nVermogen = 0; char opdracht[32];                    // wat de computer stuurtint lengte = 0; // ---------------------------------------------------------------- sensorenfloat daglichtLux() {                 // LDR onder een kapje (hoofdstuk 22 en 25)  long som = 0;  for (int i = 0; i < 16; i++) som += analogRead(LDR);  int n = som / 16;  if (n < 1) n = 1;  float r = 1000.0 * (1024.0 - n) / n;  return 10.0 * pow(r / 15000.0, -1.0 / 0.7) / 0.10;} void inaSchrijf(byte adres, byte reg, unsigned int waarde) {  Wire.beginTransmission(adres);  Wire.write(reg);                    // eerst het register ...  Wire.write(waarde >> 8);            // ... dan de hoge byte ...  Wire.write(waarde & 0xFF);          // ... en de lage byte  Wire.endTransmission();} long inaLees(byte adres, byte reg) {  // 16 bits uit een register, of -1 bij een fout  Wire.beginTransmission(adres);  Wire.write(reg);                    // de registerwijzer zetten  if (Wire.endTransmission() != 0) return -1;  if (Wire.requestFrom(adres, (byte)2) != 2) return -1;  byte hoog = Wire.read();            // eerst de hoge byte; twee aparte regels,  byte laag = Wire.read();            // zodat de volgorde vastligt  return ((long)hoog << 8) | laag;} float inaSpanning(byte adres) {       // busspanning: 1,25 mV per stap  long bus = inaLees(adres, 0x02);  if (bus < 0) return NAN;  return bus * 0.00125;} float inaVermogen(byte adres) {       // W, of NAN bij een fout  long shunt = inaLees(adres, 0x01);  // shuntspanning: 2,5 uV per stap, met teken  float u = inaSpanning(adres);  if (shunt < 0 || isnan(u)) return NAN;  float stroom = (int16_t)shunt * 0.0000025 / R_SHUNT;    // A  return u * stroom;} // ---------------------------------------------------------------- meten en schrijvenvoid meten() {  float t, rv;  if (sht.readBoth(&t, &rv)) {        // alleen geldige metingen tellen mee    somT += t;    somRV += rv;    nT++;  }  somLux += daglichtLux();  float pZon = inaVermogen(INA_ZON);  float pVerbruik = inaVermogen(INA_VERBRUIK);  float u = inaSpanning(INA_VERBRUIK);  if (!isnan(pZon) && !isnan(pVerbruik) && !isnan(u)) {    somZon += pZon;    somVerbruik += pVerbruik;    somU += u;    nVermogen++;  }  nMeting++;} void wisSommen() {  somT = somRV = somLux = somZon = somVerbruik = somU = 0;  nT = nMeting = nVermogen = 0;} size_t schrijfRegel(Print &uit) {     // één CSV-regel, naar de kaart of naar de computer  size_t n = 0;                       // hoeveel tekens er echt geschreven zijn  char tijd[17];  snprintf(tijd, sizeof(tijd), "%04d-%02d-%02d %02d:%02d", minuut.year(), minuut.month(), minuut.day(),           minuut.hour(), minuut.minute());  n += uit.print(tijd);  n += uit.print(',');  if (nT > 0) n += uit.print(somT / nT, 1);                // geen geldige meting: lege cel  n += uit.print(',');  if (nT > 0) n += uit.print(somRV / nT, 1);  n += uit.print(',');  if (nVermogen > 0) n += uit.print(somVerbruik / nVermogen / 60.0, 2);   // gemiddeld vermogen x 1/60 h = Wh  n += uit.print(',');  if (nVermogen > 0) n += uit.print(somZon / nVermogen / 60.0, 2);  n += uit.print(',');  if (nMeting > 0) n += uit.print(somLux / nMeting, 0);  n += uit.print(',');  if (nVermogen > 0) n += uit.print(somU / nVermogen, 2);  n += uit.println();  return n;} void opslaan() {  size_t lang = schrijfRegel(Serial);                       // altijd naar de computer  if (!kaartOk) return;  char naam[20];  snprintf(naam, sizeof(naam), "LOG/%04d%02d%02d.CSV", minuut.year(), minuut.month(), minuut.day());  bool nieuw = !SD.exists(naam);  File f = SD.open(naam, FILE_WRITE);                       // FILE_WRITE schrijft achteraan bij  if (!f) {    Serial.println("# SD: kan het bestand niet openen");    digitalWrite(STATUSLED, HIGH);    return;  }  if (nieuw) f.println(KOP);                                // een nieuwe dag: eerst de kop  if (schrijfRegel(f) < lang) {                             // niet alles paste nog op de kaart    Serial.println("# SD: kaart vol, regel niet bewaard");    digitalWrite(STATUSLED, HIGH);  }  f.close();                                                // close() schrijft alles echt weg} void leesOpdracht() {                 // bv. "T2026-06-01 06:00:00": de klok zetten  while (Serial.available() > 0) {    char c = Serial.read();    if (c == '\n' || c == '\r') {      opdracht[lengte] = '\0';      int j, mo, d, u, mi, s;      if (lengte > 0 && sscanf(opdracht, "T%d-%d-%d %d:%d:%d", &j, &mo, &d, &u, &mi, &s) == 6) {        rtc.adjust(DateTime(j, mo, d, u, mi, s));        Serial.println("# klok gezet");        wisSommen();                                        // de lopende minuut hoort bij de oude tijd:        bezig = false;                                      // niet bewaren, opnieuw beginnen      } else if (lengte > 0) {        Serial.println("# onbekende opdracht");      }      lengte = 0;    } else if (lengte < (int)sizeof(opdracht) - 1) {      opdracht[lengte] = c;      lengte++;    }  }} // ---------------------------------------------------------------- setup en loopvoid setup() {  Serial.begin(115200);  pinMode(STATUSLED, OUTPUT);  Wire.begin();  if (!rtc.begin()) Serial.println("# ALARM: geen klok (DS3231) op 0x68");  if (rtc.lostPower()) Serial.println("# de klok heeft stilgestaan: stuur T jjjj-mm-dd uu:mm:ss");  if (!sht.begin(0x44)) Serial.println("# ALARM: geen SHT31 op 0x44");  inaSchrijf(INA_ZON, 0x00, 0x4927);                       // 128 metingen middelen: het gemiddelde over de PWM  inaSchrijf(INA_VERBRUIK, 0x00, 0x4927);  kaartOk = SD.begin(SD_CS);  if (kaartOk && !SD.exists("LOG")) SD.mkdir("LOG");  Serial.println(kaartOk ? "# SD-kaart klaar" : "# ALARM: geen SD-kaart, alleen naar de computer");  digitalWrite(STATUSLED, kaartOk ? LOW : HIGH);           // led aan = probleem met de kaart  Serial.println(KOP);} void loop() {  unsigned long nu = millis();  leesOpdracht();  if (nu - vorigeMeting < MEET_MS) return;  vorigeMeting += MEET_MS;  DateTime t = rtc.now();  DateTime begin(t.year(), t.month(), t.day(), t.hour(), t.minute(), 0);   // het begin van deze minuut  if (!bezig) {    minuut = begin;    bezig = true;  } else if (begin.unixtime() != minuut.unixtime()) {     // een nieuwe minuut: de vorige is af    opslaan();    wisSommen();    minuut = begin;  }  meten();}

Een test in de simulatie: zondag 11 oktober 2026 van 23:58 tot 00:02, met in de minuut 23:59 een losse SHT31 en om 00:00 de pomp die aanslaat. De seriële monitor:

# SD-kaart klaar
tijdstip,temperatuur_C,vocht_pct,verbruik_Wh,zon_Wh,licht_lx,batterij_V
2026-10-11 23:58,18.1,83.8,0.09,0.00,0,13.25
2026-10-11 23:59,,,0.09,0.00,0,13.25
2026-10-12 00:00,18.0,83.9,0.29,0.00,0,13.24
2026-10-12 00:01,18.0,84.0,0.29,0.00,0,13.24

En wat er daarna op de kaart staat, twee dagbestanden met elk een kop:

== LOG/20261011.CSV (157 bytes)
tijdstip,temperatuur_C,vocht_pct,verbruik_Wh,zon_Wh,licht_lx,batterij_V
2026-10-11 23:58,18.1,83.8,0.09,0.00,0,13.25
2026-10-11 23:59,,,0.09,0.00,0,13.25
== LOG/20261012.CSV (165 bytes)
tijdstip,temperatuur_C,vocht_pct,verbruik_Wh,zon_Wh,licht_lx,batterij_V
2026-10-12 00:00,18.0,83.9,0.29,0.00,0,13.24
2026-10-12 00:01,18.0,84.0,0.29,0.00,0,13.24

Het verbruik springt om 00:00 van 0,09 naar 0,29 Wh per minuut: 5,5 W wordt 17,5 W als de pomp (12 W) aanslaat. De minuut 23:59 heeft lege cellen voor temperatuur en vocht, maar wel energie: die komt van andere chips. De eerste regel komt bij de eerste minuutwissel; start de logger midden in een minuut, dan is die eerste regel een gemiddelde over minder metingen.

De klok zetten vanaf de computer

Stuur in de seriële monitor T2026-10-11 20:00:00 (met Enter). logger.ino leest de tekens met Serial.read() tot de regelovergang en ontleedt ze met sscanf. Zonder knoopcel, of na een lege knoopcel, meldt de logger dat de klok stilstond:

# de klok heeft stilgestaan: stuur T jjjj-mm-dd uu:mm:ss
# SD-kaart klaar
tijdstip,temperatuur_C,vocht_pct,verbruik_Wh,zon_Wh,licht_lx,batterij_V
# klok gezet
# onbekende opdracht
2026-10-11 20:00,21.0,55.0,0.09,0.00,4004,13.12
loop()opdracht van de computer?(klok zetten)2 s voorbij?neejaklok lezen (DS3231)nieuweminuut?jaregel naar Serial en SD,sommen wissenneemeten en optellenvolgende ronde
loop(): elke 2 s de klok lezen; bij een nieuwe minuut eerst de vorige wegschrijven.
meten (om de 2 s)regel schrijven0102030405060708090100110120130t (s) na 23:58:00,3
De meettaak om de 2 s en het schrijven bij het begin van elke minuut.
Naslag · 30.18

Het CSV-formaat van de klimaatmodule

18/29

Het formaat van hoofdstuk 28, met twee kolommen achteraan. Deze afspraken maken het bestand bruikbaar voor elk programma, ook over tien jaar:

afspraakwaarom
de eerste regel is de kop, met de eenheid in de naam (temperatuur_C)een programma vraagt kolommen op bij naam; de eenheid gaat nooit verloren
tijdstip als jjjj-mm-dd uu:mm, in wintertijd (UTC+1)sorteert vanzelf juist; geen dubbele of ontbrekende uren bij de overgang naar zomertijd
het tijdstip is het begin van de minuutzoals in hoofdstuk 28 (een blok heet naar zijn begin)
komma tussen de velden, punt als decimaaltekenPython leest het zonder omzetten (float("23.5"))
lege cel = geen geldige metingeen 0 of −999 zou meetellen in een gemiddelde
nieuwe kolommen alleen achteraanoude programma's met csv.reader en een kolomnummer blijven werken
energie als Wh per minuut, niet als vermogenoptellen geeft meteen Wh per uur, per dag, per week
één bestand per dag: LOG/JJJJMMDD.CSV8.3-namen; kleine bestanden, makkelijk te kopiëren
2026-10-12 00:00tijdstip (begin van de minuut),18.0temperatuur_C,83.9vocht_pct,0.29verbruik_Wh,0.00zon_Wh,0licht_lx,13.24batterij_V\r\n
Zeven velden, zes komma's en een regelovergang: meer is een logregel niet.
Code · 30.19

Testdata: een week in het formaat van de logger

19/29

Om de analyse te schrijven, hoef je niet een week te wachten. Ingenieurs maken eerst testdata: een model dat bestanden maakt in precies het formaat van het echte toestel, met de fouten die je verwacht. Dit programma maakt zeven dagbestanden voor 12 tot 18 oktober 2026. Het model gebruikt wat je al weet: een zonneprofiel per dag (van helder tot grijs), de pomp 10 minuten per uur, lampen die dimmen met het daglicht en het energiebeheer van hoofdstuk 20 (onder 40 % alleen op zon, onder 20 % uit). En het zet er drie soorten fouten in, zoals ze in echte logbestanden voorkomen.

Pythonmaak_testdata.py50 regelsDownload
# maak_testdata.py - een week testdata in het formaat van de logger (een model, geen echte metingen)import mathimport osimport random rng = random.Random(2026)                        # vast zaadje: telkens dezelfde 'metingen'os.makedirs("LOG", exist_ok=True)KOP = "tijdstip,temperatuur_C,vocht_pct,verbruik_Wh,zon_Wh,licht_lx,batterij_V"WEER = [0.95, 0.55, 0.2, 0.4, 0.8, 0.65, 0.3]    # 1 = helder; maandag 12 tot zondag 18 oktober 2026soc = 0.65                                       # laadtoestand van de batterij (0 tot 1)ruis = 0.0 for dag in range(7):    w = WEER[dag]    with open(f"LOG/202610{dag + 12}.CSV", "w", newline="") as f:        f.write(KOP + "\r\n")        for m in range(1440):            u = m / 60                                                  # uur, wintertijd (UTC+1)            hoogte = max(0.0, math.sin(math.pi * (u - 7.1) / 10.7))    # zon op 7:06, onder 17:48            lux = 40000 * w * hoogte ** 1.3 * (1 + (1 - w) * (rng.random() - 0.5))            paneel = math.cos(math.pi * (u - 12.45) / 10.8) ** 1.3 if abs(u - 12.45) < 5.4 else 0.0            zon = 1000 * w * paneel * (1 + 0.6 * (1 - w) * (rng.random() - 0.5))   # W die de panelen kunnen geven            basis = 3.5 + (12 if m % 60 < 10 else 0) + (5 if 8 <= u < 20 else 2)   # sensoren, pomp, ventilatoren            wens = 190 * min(max((200 - lux / 54) / 200, 0), 1) if 6 <= u < 22 else 0.0            if soc >= 0.40:                lampen = wens                                           # energiebeheer (hoofdstuk 20)            elif soc >= 0.20:                lampen = min(wens, max(zon - basis, 0))                 # alleen op zon            else:                lampen = 0.0            verbruik = basis + lampen            if soc >= 1.0 and zon > verbruik:                zon = verbruik                                          # batterij vol: de regelaar knijpt af            stroom = (zon - verbruik) / 13.2                            # A, positief = laden            soc = min(1.0, soc + stroom * 13.2 * (0.95 if stroom > 0 else 1) / 60 / 3072)            bat = 13.0 + 0.4 * soc + 0.015 * stroom            ruis = 0.9 * ruis + rng.gauss(0, 0.05)            temp = 18.0 + (6.0 * w + 0.8) * max(0.0, math.sin(math.pi * (u - 9) / 9.5)) ** 1.2 + 1.5 * lampen / 190 + ruis            vocht = 84 - 1.6 * (temp - 18) + rng.gauss(0, 0.4)            tijd = f"2026-10-{dag + 12} {m // 60:02d}:{m % 60:02d}"            t_tekst, v_tekst = f"{temp:.1f}", f"{vocht:.1f}"            # fouten die in echte logbestanden voorkomen:            if (dag, m) in ((0, 552), (0, 553), (1, 605)):                t_tekst, v_tekst = "130.0", "100.0"                     # versie 1 las 0xFF 0xFF: sensor antwoordde niet            if dag == 1 and m == 840:                continue                                                # 14:00: nieuwe software opgeladen            if dag == 2 and 707 <= m <= 718:                t_tekst, v_tekst = "", ""                               # stekkertje los: versie 2 laat de cel leeg            f.write(f"{tijd},{t_tekst},{v_tekst},{verbruik / 60:.2f},{zon / 60:.2f},{lux:.0f},{bat:.2f}\r\n")print("zeven bestanden in de map LOG")

Uitvoer: zeven bestanden in de map LOG. Het begin van het eerste bestand:

tijdstip,temperatuur_C,vocht_pct,verbruik_Wh,zon_Wh,licht_lx,batterij_V
2026-10-12 00:00,17.9,84.1,0.29,0.00,0,13.24
2026-10-12 00:01,17.9,83.9,0.29,0.00,0,13.24
2026-10-12 00:02,17.9,83.5,0.29,0.00,0,13.24
2026-10-12 00:03,17.9,84.7,0.29,0.00,0,13.24
2026-10-12 00:04,17.8,84.5,0.29,0.00,0,13.24
…

Het zijn geen metingen, maar een model met toevalsruis. Met dezelfde startwaarde van de toevalsgenerator (2026) krijg je precies dezelfde bestanden: download het programma en je werkt met dezelfde getallen als dit boek. De fouten: op maandag om 9:12 en 9:13 en dinsdag om 10:05 staat 130,0 °C en 100,0 % (de eerste versie van de logger controleerde niet of de SHT31 antwoordde); dinsdag om 14:00 ontbreekt een regel (toen werd versie 2 opgeladen); woensdag van 11:47 tot 11:58 zijn temperatuur en vocht leeg (een losse stekker, en versie 2 schrijft dan een lege cel).

Code · 30.20

Inlezen en opschonen: logboek.py

20/29

Een analyse begint altijd met opschonen: wat is er binnengekomen, wat ontbreekt, wat kan niet? De functies staan in een eigen module logboek.py, zodat elk volgend programma ze met from logboek import lees_week kan gebruiken, zoals pinnen.h in C++. Het blok onderaan, na if __name__ == "__main__":, draait alleen als je logboek.py zelf start: dan maakt het een verslag.

Pythonlogboek.py64 regelsDownload
# logboek.py - de logbestanden van de klimaatmodule inlezen en opschonenimport csvfrom datetime import datetime, timedelta GRENZEN = {"T": (-10, 50), "RV": (0, 100)}      # wat in de module kan; daarbuiten is het een meetfout  def getal(tekst):    """Tekst naar kommagetal; een lege cel (of een ontbrekende bij een halve regel) wordt None."""    if tekst is None or tekst == "":        return None    return float(tekst)  def lees_dag(naam):    """Alle regels van één dagbestand, als woordenboeken met getallen en een echte tijd."""    rijen = []    with open(naam, newline="", encoding="utf-8") as bestand:        for r in csv.DictReader(bestand):            rijen.append({"tijd": datetime.strptime(r["tijdstip"], "%Y-%m-%d %H:%M"),                          "T": getal(r["temperatuur_C"]), "RV": getal(r["vocht_pct"]),                          "verbruik": getal(r["verbruik_Wh"]), "zon": getal(r["zon_Wh"]),                          "licht": getal(r["licht_lx"]), "bat": getal(r["batterij_V"])})    return rijen  def lees_week(eerste=12, laatste=18):    """De dagbestanden van 12 tot 18 oktober 2026 achter elkaar."""    rijen = []    for dag in range(eerste, laatste + 1):        rijen += lees_dag(f"LOG/202610{dag}.CSV")    return rijen  def buiten(waarde, grootheid):    laag, hoog = GRENZEN[grootheid]    return waarde is not None and not (laag <= waarde <= hoog)  def opschonen(rijen):    """Onmogelijke waarden worden None (T en RV samen: ze komen van dezelfde sensor). Geeft hun aantal terug."""    fout = 0    for r in rijen:        if buiten(r["T"], "T") or buiten(r["RV"], "RV"):            r["T"] = r["RV"] = None            fout += 1    return fout  def gaten(rijen):    """Paren van opeenvolgende regels die niet precies één minuut uit elkaar liggen."""    return [(a["tijd"], b["tijd"]) for a, b in zip(rijen, rijen[1:]) if b["tijd"] - a["tijd"] != timedelta(minutes=1)]  if __name__ == "__main__":    rijen = lees_week()    print("regels ingelezen:", len(rijen), "- verwacht:", 7 * 24 * 60)    print("lege temperatuur:", sum(1 for r in rijen if r["T"] is None))    for r in rijen:        if buiten(r["T"], "T") or buiten(r["RV"], "RV"):            print("  onmogelijk:", r["tijd"], r["T"], r["RV"])    print("opgeschoond:", opschonen(rijen), "regels")    for a, b in gaten(rijen):        print("gat in de tijd:", a, "->", b)

Verslag bij de testweek:

regels ingelezen: 10079 - verwacht: 10080
lege temperatuur: 12
  onmogelijk: 2026-10-12 09:12:00 130.0 100.0
  onmogelijk: 2026-10-12 09:13:00 130.0 100.0
  onmogelijk: 2026-10-13 10:05:00 130.0 100.0
opgeschoond: 3 regels
gat in de tijd: 2026-10-13 13:59:00 -> 2026-10-13 14:01:00

Drie soorten problemen, drie behandelingen. Lege cellen worden None en tellen nergens mee. Onmogelijke waarden (130 °C in een kas) worden None: ze zijn geen extreem weer maar een meetfout, en dat kan je bewijzen met de sensor (0xFF 0xFF). Ontbrekende regels vind je alleen door de tijden te vergelijken: 10 079 regels in plaats van 10 080. datetime.strptime maakt van de tekst een echte tijd, waarmee je kan rekenen.

Uitleg · 30.21

Een fout of een uitschieter?

21/29

Opschonen is niet 'alles weggooien wat vreemd is'. Een waarde die sterk afwijkt, heet een uitschieter, en er zijn twee soorten:

  • Meetfouten: fysisch onmogelijk (130 °C in een kas, 100,0 % en 130,0 °C samen), of met een bekende oorzaak (een losse draad). Die mag je vervangen door 'geen waarde', met een regel die je in je verslag vermeldt.
  • Echte gebeurtenissen: een piek in de vochtigheid als iemand de plantjes besproeit, een daling van de temperatuur als het dak opengaat. Die hoor je niet weg te halen: juist die wil je zien.

Daarom gebruikt logboek.py vaste grenzen die uit de fysica van de module komen (−10 tot 50 °C, 0 tot 100 %), niet 'alles wat meer dan x van het gemiddelde afwijkt'. En het past de ruwe bestanden nooit aan: het opschonen gebeurt in het programma, zodat iemand anders het kan nagaan en herhalen (hoofdstuk 28, het verhaal van de vijf ontbrekende landen).

"Een ontbrekende waarde vul je in met 0"

Een 0 is een meting: 0 °C trekt het daggemiddelde omlaag. Een lege waarde laat je leeg, of je vult ze in met een methode die je vermeldt (bv. het gemiddelde van de minuut ervoor en erna). Voor een daggemiddelde over 1428 in plaats van 1440 minuten is leeglaten prima.

8,59,09,510,020406080100120140uur (maandag)T (°C)
Maandag tussen 8:30 en 10:00, zoals de logger het schreef: twee punten op 130 °C.
8,59,09,510,01819202122uur (maandag)T (°C)
Hetzelfde na opschonen: een gat van twee minuten, en de echte temperatuur wordt leesbaar.
Code · 30.22

De week per dag

22/29

Nu de gegevens schoon zijn, is een samenvatting per dag een paar regels: de rijen verzamelen per datum in een woordenboek (hoofdstuk 28), dan per dag gemiddelde, minimum, maximum en de sommen van de energie.

Pythonper_dag.py18 regelsDownload
# per_dag.py - de week per dag samenvattenfrom logboek import lees_week, opschonen rijen = lees_week()opschonen(rijen)dagen = {}                                   # datum -> de regels van die dagfor r in rijen:    datum = r["tijd"].date()    if datum not in dagen:        dagen[datum] = []    dagen[datum].append(r) print("datum       T gem  T min  T max  geldig  verbruik   zon  saldo (kWh)")for datum, rs in dagen.items():    t = [r["T"] for r in rs if r["T"] is not None]    verbruik = sum(r["verbruik"] for r in rs) / 1000    zon = sum(r["zon"] for r in rs) / 1000    print(f"{datum} {sum(t) / len(t):6.1f} {min(t):6.1f} {max(t):6.1f} {len(t):7d} {verbruik:9.2f} {zon:5.2f} {zon - verbruik:+6.2f}")
0,00,51,01,52,02,53,0energie per dag (kWh)ma 12di 13wo 14do 15vr 16za 17zo 18verbruikzonnestroom
Uit dezelfde bestanden: alleen op grijze dagen is het verbruik groter dan de zonnestroom.

Uitvoer:

datum       T gem  T min  T max  geldig  verbruik   zon  saldo (kWh)
2026-10-12   20.0   17.7   24.7    1438      1.50  1.74  +0.23
2026-10-13   19.4   17.6   22.4    1438      1.67  1.71  +0.03
2026-10-14   19.1   17.6   21.0    1428      2.15  1.27  -0.88
2026-10-15   19.2   17.7   21.4    1440      1.46  2.43  +0.98
2026-10-16   19.7   17.7   23.7    1440      1.54  1.69  +0.14
2026-10-17   19.6   17.8   22.9    1440      1.61  1.66  +0.05
2026-10-18   19.2   17.7   21.2    1440      2.02  1.89  -0.13

Woensdag (grijs) is de enige dag met een groot tekort: de lampen branden langer en de zon levert weinig. Donderdag laadt de batterij dat weer bij. De warmste minuut van de week was 24,7 °C, op de heldere maandag. De kolom 'geldig' telt de minuten met een temperatuur: 1428 op woensdag door de losse stekker.

Code · 30.23

Grafieken van de week

23/29

matplotlib (hoofdstuk 28) tekent de uurgemiddelden en de energie per dag. Met plt.subplots(2, 1) komen twee grafieken onder elkaar in één figuur. Omdat de tijd een echte datetime is, zet matplotlib zelf datums op de as.

Pythongrafiek_week.py39 regelsDownload
# grafiek_week.py - de temperatuur per uur en de energie per dag, in twee grafiekenimport matplotlib.pyplot as pltfrom logboek import lees_week, opschonen rijen = lees_week()opschonen(rijen) per_uur = {}                                 # begin van het uur -> temperaturenper_dag = {}                                 # datum -> [verbruik, zon] in Whfor r in rijen:    if r["T"] is not None:        uur = r["tijd"].replace(minute=0)        if uur not in per_uur:            per_uur[uur] = []        per_uur[uur].append(r["T"])    dag = r["tijd"].date()    if dag not in per_dag:        per_dag[dag] = [0.0, 0.0]    per_dag[dag][0] += r["verbruik"]    per_dag[dag][1] += r["zon"] uren = sorted(per_uur)gemiddeld = [sum(per_uur[u]) / len(per_uur[u]) for u in uren]dagen = sorted(per_dag) figuur, (boven, onder) = plt.subplots(2, 1, figsize=(9, 7))boven.plot(uren, gemiddeld, color="tab:red")boven.set_ylabel("temperatuur (°C)")boven.set_title("Klimaatmodule, 12 tot 18 oktober 2026")boven.grid(True)x = range(len(dagen))onder.bar([i - 0.2 for i in x], [per_dag[d][0] / 1000 for d in dagen], width=0.4, label="verbruik")onder.bar([i + 0.2 for i in x], [per_dag[d][1] / 1000 for d in dagen], width=0.4, label="zonnestroom")onder.set_xticks(list(x), [d.strftime("%a %d") for d in dagen])onder.set_ylabel("energie per dag (kWh)")onder.legend()figuur.tight_layout()figuur.savefig("week_oktober.png", dpi=150)plt.show()
01234567161820222426dagen sinds maandag 12 oktober, 0 uurT (°C)
Dezelfde uurgemiddelden in de huisstijl van dit boek: elke dag een bult, het hoogst op de heldere maandag.

De code maakt de twee grafieken van dit blad en het vorige. Het uurgemiddelde van 60 minuten verbergt de korte schommelingen maar toont het dagritme: 's nachts houdt de warmte van het station de module op ongeveer 18 °C, overdag stijgt ze met de zon, en 's avonds geven de lampen nog wat warmte.

Uitgewerkt voorbeeld · 30.24

Wat vertellen de gegevens over de module?

24/29

Bekijk het verbruik op woensdagavond en de zonnestroom op maandagvoormiddag, minuut per minuut (de energie per minuut maal 60 is het gemiddelde vermogen in W). Wat gebeurde er om 20:51 op woensdag en om 10:55 op maandag?

Gegeven
  • de testweek (logboek.py)
  • energiebeheer: onder 40 % lampen alleen op zon (hoofdstuk 20)
  • batterij 2 × 120 Ah LiFePO4
Gevraagd
  • een verklaring voor beide knikken
Oplossing
  1. 1
    Woensdag 20:51. Het verbruik valt van ongeveer 200 W naar 5,5 W, ruim een uur vóór de lichtperiode eindigt (22 uur). Op een grijze dag daalt de laadtoestand; op dat moment zakt ze onder 40 %, en na zonsondergang betekent 'alleen op zon' gewoon 'uit'. Ook donderdagochtend blijven de lampen uit tot de zon genoeg levert.
  2. 2
    Maandag 10:55. De zonnestroom valt van ruim 800 W naar het verbruik van de module. De zon schijnt gewoon door, maar de batterij is vol: de laadregelaar knijpt de panelen af, en de INA226 meet alleen wat er echt door de leiding gaat. De batterijspanning springt op dat moment van 14,3 naar 13,4 V.
  3. 3
    Gevolg voor de analyse. zon_Wh is wat er geleverd werd, niet wat de panelen konden leveren. In hoofdstuk 28 was zon_Wh een model van wat de panelen gaven; een echte logger ziet de afgeknepen zon niet. Wie wil weten hoeveel zon er verloren ging, schat het uit licht_lx.
Antwoord
Woensdagavond grijpt het energiebeheer in; maandagvoormiddag is de batterij vol.
Klopt dit? Klopt het met de dagtotalen? Maandag leverde de zon maar 1,74 kWh, hoewel het de helderste dag was: na 10:55 werd de rest afgeknepen.
18192021222324020406080100120140160180200220uur (woensdag 14 oktober)vermogen (W)lampen uit
Woensdagavond: de lampen gaan uit voor de lichtperiode voorbij is.
8910111213140100200300400500600700800900uur (maandag 12 oktober)vermogen (W)batterij volzonverbruik
Maandag: na het vol raken van de batterij levert de zon alleen nog het verbruik.
Proef · 30.25

Een datalogger voor het klaslokaal

25/29
Onderzoeksvraag

Hoe veranderen de temperatuur en de luchtvochtigheid in het klaslokaal tijdens een schooldag?

Materiaal
  • Arduino Mega of Uno, USB-kabel
  • SHT31-breakout, DS3231-moduletje met knoopcel
  • microSD-moduletje en een kaart (FAT32)
  • breadboard en draadjes
  • logger.ino, i2c_scanner.ino en klok_zetten.py (werkboek) of de seriële monitor
De logger een schooldag lang op de kast: uit de zon, weg van de verwarming.
De logger een schooldag lang op de kast: uit de zon, weg van de verwarming.
Werkwijze
  1. 1Sluit de SHT31 en de DS3231 aan op 5V, GND, SDA en SCL (Mega: 20 en 21; Uno: A4 en A5), en het SD-moduletje op 5V, GND en de SPI-pinnen (Mega: 50 tot 53; Uno: 12, 11, 13 en 10; zet dan SD_CS op 10).
  2. 2Draai de I²C-scanner. Je verwacht 0x44, 0x57 en 0x68.
  3. 3Laad logger.ino op. Zonder INA226 blijven de energiekolommen leeg: dat mag. Zet de klok met de opdracht T… in de seriële monitor.
  4. 4Leg de logger op een kast op ooghoogte, uit de zon en weg van de verwarming. Laat hem een hele schooldag loggen; noteer wanneer er ramen opengaan en wanneer er leerlingen zijn.
  5. 5Kopieer het bestand van de kaart en pas logboek.py aan (de naam van het bestand en de dagen). Maak een grafiek van temperatuur en vocht.
Waarneming

Zo begint het bestand (uit de simulatie van logger.ino; bij jou staan er echte waarden):

tijdstip,temperatuur_C,vocht_pct,verbruik_Wh,zon_Wh,licht_lx,batterij_V
2026-10-12 00:00,18.0,83.9,0.29,0.00,0,13.24
2026-10-12 00:01,18.0,84.0,0.29,0.00,0,13.24

Typisch: 's morgens stijgen temperatuur en vocht zodra de klas binnenkomt (mensen geven warmte en waterdamp af); bij een open raam dalen ze in enkele minuten; tijdens de middagpauze vlakken ze af. Een regel per minuut is fijn genoeg om een open raam te zien.

Verklaring

Een klas van twintig leerlingen geeft ongeveer 2 kW warmte af (hoofdstuk 28) en elke persoon ademt waterdamp uit: daarom stijgt de relatieve vochtigheid, tenzij de temperatuur sneller stijgt (warme lucht kan meer waterdamp bevatten, dus de relatieve vochtigheid kan dan zelfs dalen). Een logger met een klok en een kaart laat zo'n verloop zien zonder dat iemand ernaast moet zitten, en de lege cellen en tijdstempels maken duidelijk wat er ontbreekt.

Uitgewerkt voorbeeld · 30.26

Habitat: geheugen en eigen verbruik van de logger

26/29

De logger komt in klimaat.ino. Past alles in het werkgeheugen van de Mega? En hoeveel energie kost de logger zelf, vergeleken met de module?

Gegeven
  • SRAM van de Mega: 8 kB
  • 5 V-omvormer met een rendement van 83 % (hoofdstuk 21)
  • basisverbruik van de module 230 Wh per dag (hoofdstuk 20); in de testweek 1,5 tot 2,2 kWh per dag
Gevraagd
  • het SRAM-gebruik
  • het eigen verbruik per dag
Oplossing
  1. 1
    Werkgeheugen. Een schatting per onderdeel:
    watSRAM (orde van grootte)
    SD-bibliotheek (waarvan 512 bytes blokbuffer)±0,6 kB
    Wire (buffers van 32 bytes)±0,2 kB
    Serial (zend- en ontvangstbuffer van 64 bytes)±0,2 kB
    variabelen van klimaat.ino en de logger, KOP, opdracht±0,4 kB
    stack: lokale variabelen, snprintf-buffers±0,3 kB
    samen±1,7 kB van de 8 kB
    Samen ongeveer een vijfde van de 8 kB. De IDE toont na het compileren het exacte getal voor de globale variabelen; laat minstens een paar honderd bytes vrij voor de stack. Op een Uno (2 kB) zou het krap worden: een van de redenen voor de Mega.
  2. 2
    Stroom. Per onderdeel:
    onderdeel van de loggerstroom op 5 V (gemiddeld)waarop gebaseerd
    SD-kaartmodule±10 mA (pieken ±100 mA bij het schrijven)orde van grootte, hoofdstuk 21
    DS3231≤ 0,3 mAdatasheet (en een ledje op het moduletje: vaak enkele mA extra)
    2 × INA2262 × 0,33 mAdatasheet, continu meten
    SHT31, om de 2 s 12,5 ms meten bij 0,6 mA±0,004 mAdatasheet
    samen±11 mA = ±55 mWde Mega zelf telt niet: die draait toch
  3. 3
    Energie per dag. aan de 12 V-kant; maal 24 h is dat per dag. Met een ledje op het klokmoduletje (±3 mA) wordt het ±2,0 Wh.
  4. 4
    Vergelijken. 1,6 Wh is 0,7 % van het basisverbruik van 230 Wh, en ongeveer 0,1 % van wat de module in de testweek per dag verbruikte. De SD-kaart is de grootste post; de sensoren tellen nauwelijks.
Antwoord
±1,7 kB van 8 kB SRAM; ±1,6 Wh per dag, minder dan 1 % van het basisverbruik.
Klopt dit? Orde van grootte: een ledje van 3 mA verbruikt al een vijfde van de hele logger. Wie zuiniger wil, knipt het ledje los en laat de kaart slapen tussen twee regels (Expert, hoofdstuk E37).
Code · 30.27

klimaat.ino versie 2: de logger als taak

27/29

De logger wordt geen apart programma maar een deel van de sturing: de meettaak van hoofdstuk 29 meet al om de 2 s, dus telt ze nu ook op voor het logboek (loggen()). Drie dingen veranderen verder: de tijd van de dag komt uit de DS3231 in plaats van uit millis() met een startuur, de pomp draait in de eerste tien minuten van elk kloksuur (ook na een herstart), en alle meldingen beginnen met #, zodat een Python-programma op de computer ze van de logregels kan scheiden.

C++ (Arduino)klimaat.ino421 regelsDownload
// klimaat.ino - de sturing van de klimaatmodule, versie 2 (Arduino Mega 2560, hoofdstuk 30)// Nieuw tegenover versie 1: de klok komt van de DS3231, en de meettaak houdt een logboek bij: elke minuut een// CSV-regel op de SD-kaart (map LOG, één bestand per dag) en naar de computer. Meldingen beginnen nu met #.// I2C: INA226 zon 0x40, INA226 verbruik 0x41, SHT31 0x44, DS3231 0x68. SD op SPI (CS = 53). Klok: wintertijd.#include <Wire.h>#include <SPI.h>#include <SD.h>#include <RTClib.h>#include <EEPROM.h>#include <Adafruit_SHT31.h>#include "pinnen.h" // ---------------------------------------------------------------- instellingenconst unsigned long MEET_MS = 2000;const unsigned long LAMP_MS = 200;const unsigned long MELD_MS = 60000UL;const unsigned long BEWAAR_MS = 3600000UL;            // de laadtoestand elk uur in het EEPROMconst unsigned long DENDER_MS = 20;const float CAPACITEIT_AH = 240.0;                    // twee LiFePO4-batterijen van 120 Ah (hoofdstuk 20)const int ADRES_SOC = 0;                              // waar de laadtoestand in het EEPROM staatconst float DOEL = 200.0;                             // umol/(m2 s) op het kweekbed (hoofdstuk 25)const float LAMPEN_VOL = 200.0;                       // wat de lampen op 100 % gevenconst float LUX_PER_UMOL = 54.0;const float U_NODIG = 11.63, U_START = 2.21;          // dakmotor (hoofdstuk 26)const unsigned long AANLOOP_MS = 1000, REMMEN_VANAF_MS = 6000, TIMEOUT_MS = 12000;const int UITGANGEN[] = {LAMPEN, PWM_DICHT, PWM_OPEN, DAK_ENABLE, POMP, CO2_KLEP, VENTILATOREN, STATUSLED};const int AANTAL_UIT = sizeof(UITGANGEN) / sizeof(UITGANGEN[0]);const int SD_CS = 53;                                 // logger (hoofdstuk 30)const byte INA_ZON = 0x40, INA_VERBRUIK = 0x41;const float R_SHUNT = 0.00075;                        // shunt 100 A / 75 mVconst char KOP[] = "tijdstip,temperatuur_C,vocht_pct,verbruik_Wh,zon_Wh,licht_lx,batterij_V"; // ---------------------------------------------------------------- toestand (globaal: alle taken gebruiken ze)Adafruit_SHT31 sht = Adafruit_SHT31();RTC_DS3231 rtc;DateTime klok;                                        // de tijd van de laatste meting (om de 2 s bijgewerkt)bool kaartOk = false;DateTime minuut;                                      // logger: de minuut die we verzamelenbool bezig = false;float somT = 0, somRV = 0, somLux = 0, somZon = 0, somVerbruik = 0, somU = 0;int nT = 0, nMeting = 0, nVermogen = 0;char opdracht[32];int lengte = 0;float luchtT = NAN, luchtRV = NAN;                    // kaslucht, van de SHT31 op I2Cfloat lux = 0, uBat = 0, iBat = 0, soc = 50.0;int grens = 0;                                        // energiebeheer: de hoogste PWM die de lampen mogenint stand = 0;                                        // de PWM die de lampen nu krijgenbool pompAan = false, genoegWater = true;enum Dak { STIL, SLUITEN, OPENEN };Dak dak = STIL;bool dakOpen = false;unsigned long vorigeMeting = 0, vorigeLamp = 0, vorigeMelding = 0, vorigeBewaring = 0;unsigned long startDak = 0, laatsteWissel = 0, vorigeKnipper = 0;int vorigeLezing = LOW, stabiel = LOW;bool ledAan = false; // ---------------------------------------------------------------- sensorenint gemiddeld(int pin) {                              // 16 metingen middelen tegen ruis (hoofdstuk 22)  long som = 0;  for (int i = 0; i < 16; i++) som += analogRead(pin);  return som / 16;} float daglichtLux() {                                 // LDR onder een kapje (hoofdstuk 22 en 25)  int n = gemiddeld(LDR);  if (n < 1) n = 1;  float r = 1000.0 * (1024.0 - n) / n;  return 10.0 * pow(r / 15000.0, -1.0 / 0.7) / 0.10;} float batterijspanning() {                            // deler 100 kOhm / 33 kOhm (hoofdstuk 22)  return gemiddeld(BATTERIJ) * 5.0 / 1024.0 * (133.0 / 33.0);} float batterijstroom() {                              // ACS712-20A: 100 metingen in 20,4 ms (hoofdstuk 16 en 25)  long som = 0;  unsigned long t0 = micros();  for (int i = 0; i < 100; i++) {    while (micros() - t0 < i * 204UL) {    }    som += analogRead(STROOM);  }  return (som / 100.0 * 5.0 / 1024.0 - 2.50) / 0.100;  // A, positief = ontladen} long inaLees(byte adres, byte reg) {                  // INA226: 16 bits uit een register, of -1 (hoofdstuk 30)  Wire.beginTransmission(adres);  Wire.write(reg);  if (Wire.endTransmission() != 0) return -1;  if (Wire.requestFrom(adres, (byte)2) != 2) return -1;  byte hoog = Wire.read();  byte laag = Wire.read();  return ((long)hoog << 8) | laag;} float inaSpanning(byte adres) {  long bus = inaLees(adres, 0x02);  if (bus < 0) return NAN;  return bus * 0.00125;} float inaVermogen(byte adres) {  long shunt = inaLees(adres, 0x01);  float u = inaSpanning(adres);  if (shunt < 0 || isnan(u)) return NAN;  return u * (int16_t)shunt * 0.0000025 / R_SHUNT;} // ---------------------------------------------------------------- tijdunsigned long secondenVanDeDag() {                    // uit de klok, niet meer uit millis()  return klok.hour() * 3600UL + klok.minute() * 60UL + klok.second();} void printTwee(unsigned long x) {                     // altijd twee cijfers: 7 -> 07  if (x < 10) Serial.print('0');  Serial.print(x);} void printTijd() {                                    // meldingen beginnen met #: de logger-regels niet  Serial.print("# ");  unsigned long s = secondenVanDeDag();  printTwee(s / 3600);  Serial.print(':');  printTwee(s / 60 % 60);  Serial.print(':');  printTwee(s % 60);  Serial.print("  ");} // ---------------------------------------------------------------- logger (hoofdstuk 30)void wisSommen() {  somT = somRV = somLux = somZon = somVerbruik = somU = 0;  nT = nMeting = nVermogen = 0;} size_t schrijfRegel(Print &uit) {  size_t n = 0;  char tijd[17];  snprintf(tijd, sizeof(tijd), "%04d-%02d-%02d %02d:%02d", minuut.year(), minuut.month(), minuut.day(),           minuut.hour(), minuut.minute());  n += uit.print(tijd);  n += uit.print(',');  if (nT > 0) n += uit.print(somT / nT, 1);  n += uit.print(',');  if (nT > 0) n += uit.print(somRV / nT, 1);  n += uit.print(',');  if (nVermogen > 0) n += uit.print(somVerbruik / nVermogen / 60.0, 2);  n += uit.print(',');  if (nVermogen > 0) n += uit.print(somZon / nVermogen / 60.0, 2);  n += uit.print(',');  if (nMeting > 0) n += uit.print(somLux / nMeting, 0);  n += uit.print(',');  if (nVermogen > 0) n += uit.print(somU / nVermogen, 2);  n += uit.println();  return n;} void opslaan() {  size_t lang = schrijfRegel(Serial);  if (!kaartOk) return;  char naam[20];  snprintf(naam, sizeof(naam), "LOG/%04d%02d%02d.CSV", minuut.year(), minuut.month(), minuut.day());  bool nieuw = !SD.exists(naam);  File f = SD.open(naam, FILE_WRITE);  if (!f) {    Serial.println("# SD: kan het bestand niet openen");    kaartOk = false;                                  // de statusled knippert snel    return;  }  if (nieuw) f.println(KOP);  if (schrijfRegel(f) < lang) {    Serial.println("# SD: kaart vol");    kaartOk = false;  }  f.close();} void loggen() {                                       // om de 2 s, na de metingen  DateTime begin(klok.year(), klok.month(), klok.day(), klok.hour(), klok.minute(), 0);  if (!bezig) {    minuut = begin;    bezig = true;  } else if (begin.unixtime() != minuut.unixtime()) {    opslaan();    wisSommen();    minuut = begin;  }  if (!isnan(luchtT)) {    somT += luchtT;    somRV += luchtRV;    nT++;  }  somLux += lux;  float pZon = inaVermogen(INA_ZON);  float pVerbruik = inaVermogen(INA_VERBRUIK);  float u = inaSpanning(INA_VERBRUIK);  if (!isnan(pZon) && !isnan(pVerbruik) && !isnan(u)) {    somZon += pZon;    somVerbruik += pVerbruik;    somU += u;    nVermogen++;  }  nMeting++;} void leesOpdracht() {                                 // T2026-10-11 20:00:00 + Enter: de klok zetten  while (Serial.available() > 0) {    char c = Serial.read();    if (c == '\n' || c == '\r') {      opdracht[lengte] = '\0';      int j, mo, d, u, mi, s;      if (lengte > 0 && sscanf(opdracht, "T%d-%d-%d %d:%d:%d", &j, &mo, &d, &u, &mi, &s) == 6) {        rtc.adjust(DateTime(j, mo, d, u, mi, s));        Serial.println("# klok gezet");        wisSommen();        bezig = false;      } else if (lengte > 0) {        Serial.println("# onbekende opdracht");      }      lengte = 0;    } else if (lengte < (int)sizeof(opdracht) - 1) {      opdracht[lengte] = c;      lengte++;    }  }} // ---------------------------------------------------------------- takenvoid taakMeten(unsigned long nu) {  if (nu - vorigeMeting < MEET_MS) return;  vorigeMeting += MEET_MS;  klok = rtc.now();  sht.readBoth(&luchtT, &luchtRV);                    // nan als de sensor niet antwoordt  lux = daglichtLux();  uBat = batterijspanning();  iBat = batterijstroom();  soc -= iBat * (MEET_MS / 3600000.0) / CAPACITEIT_AH * 100.0;    // lading tellen (hoofdstuk 16)  if (uBat >= 14.2 && iBat < 0.0 && iBat > -2.0) soc = 100.0;     // vol: opnieuw ijken  soc = constrain(soc, 0.0, 100.0);  // energiebeheer (hoofdstuk 20): hoeveel licht mag er hoogstens branden?  if (soc >= 40.0) grens = 255;  else if (soc < 20.0) grens = 0;  else if (iBat < 0.0) grens = min(grens + 5, 255);   // de batterij laadt: er is zon over  else grens = max(grens - 5, 0);                     // de batterij levert: minder licht  loggen();} void taakLampen(unsigned long nu) {                   // dimmen met het daglicht (hoofdstuk 25)  if (nu - vorigeLamp < LAMP_MS) return;  vorigeLamp += LAMP_MS;  int wens = 0;  unsigned long uur = secondenVanDeDag() / 3600;  if (uur >= 6 && uur < 22) {                         // lichtperiode van 16 uur    float f = constrain((DOEL - lux / LUX_PER_UMOL) / LAMPEN_VOL, 0.0, 1.0);    wens = (int)(f * 255 + 0.5);  }  int doelStand = min(wens, grens);  if (doelStand > stand) stand++;                     // zacht: hoogstens 1 stap per 200 ms  else if (doelStand < stand) stand--;  analogWrite(LAMPEN, stand);} void taakPomp(unsigned long nu) {                     // 10 min per uur, nooit zonder water (hoofdstuk 23)  bool tijd = klok.minute() < 10;                     // de eerste 10 minuten van elk uur, volgens de klok  bool water = (digitalRead(VLOTTER) == LOW);  bool aan = tijd && water;  if (water != genoegWater) {                         // alleen melden als er iets verandert    printTijd();    Serial.println(water ? "vlotter: weer genoeg water" : "ALARM vlotter: te weinig water");    genoegWater = water;  }  if (aan != pompAan) {    printTijd();    Serial.println(aan ? "pomp aan" : "pomp uit");    pompAan = aan;  }  digitalWrite(POMP, aan ? HIGH : LOW);} bool knopGedrukt(unsigned long nu) {                  // true bij de flank 'net ingedrukt' (hoofdstuk 24)  int lezing = digitalRead(KNOP);  if (lezing != vorigeLezing) {    laatsteWissel = nu;    vorigeLezing = lezing;  }  if (nu - laatsteWissel >= DENDER_MS && lezing != stabiel) {    stabiel = lezing;    return stabiel == HIGH;  }  return false;} void motor(int richting, int waarde) {                // +1 sluiten, -1 openen, 0 stil (hoofdstuk 26)  analogWrite(PWM_DICHT, richting > 0 ? waarde : 0);  analogWrite(PWM_OPEN, richting < 0 ? waarde : 0);   // nooit beide tegelijk} void stopDak(unsigned long nu, const char* reden) {  motor(0, 0);  dak = STIL;  printTijd();  Serial.println(reden);} void taakDak(unsigned long nu) {  if (knopGedrukt(nu)) {    if (dak != STIL) {      stopDak(nu, "dak gestopt met de knop");         // een druk tijdens de beweging = stoppen    } else {      dak = dakOpen ? SLUITEN : OPENEN;      startDak = nu;      printTijd();      Serial.println(dak == SLUITEN ? "dak sluiten" : "dak openen");    }  }  if (dak == STIL) return;  unsigned long t = nu - startDak;  if (dak == SLUITEN && digitalRead(EIND_DICHT) == HIGH) {    dakOpen = false;    stopDak(nu, "dak dicht");  } else if (dak == OPENEN && digitalRead(EIND_OPEN) == HIGH) {    dakOpen = true;    stopDak(nu, "dak open");  } else if (t > TIMEOUT_MS) {    stopDak(nu, "ALARM dak: eindpunt niet bereikt");  } else {                                            // aanlopen, constant, afremmen tot 30 %    float s = 1.0;    if (t < AANLOOP_MS) s = t / (float)AANLOOP_MS;    else if (t > REMMEN_VANAF_MS) s = max(0.3, 1.0 - (t - REMMEN_VANAF_MS) / (float)AANLOOP_MS);    float u = U_START + (U_NODIG - U_START) * s;    motor(dak == SLUITEN ? +1 : -1, constrain((int)(u / uBat * 255 + 0.5), 0, 255));  }} void taakStatusled(unsigned long nu) {                // rustig knipperen = alles goed, snel = alarm  bool alarm = !genoegWater || soc < 10.0 || !kaartOk;  unsigned long periode = alarm ? 100 : 500;  if (nu - vorigeKnipper < periode) return;  vorigeKnipper = nu;                                 // hier mag het: een led mag wat verlopen  ledAan = !ledAan;  digitalWrite(STATUSLED, ledAan ? HIGH : LOW);} void taakMelden(unsigned long nu) {                   // elke minuut één regel voor de seriële monitor  if (nu - vorigeMelding < MELD_MS) return;  vorigeMelding += MELD_MS;  printTijd();  Serial.print("T ");  Serial.print(luchtT, 1);  Serial.print(" C  RV ");  Serial.print(luchtRV, 0);  Serial.print(" %  licht ");  Serial.print(lux, 0);  Serial.print(" lx  bat ");  Serial.print(uBat, 2);  Serial.print(" V ");  Serial.print(iBat, 1);  Serial.print(" A  SoC ");  Serial.print(soc, 1);  Serial.print(" %  lampen ");  Serial.print(stand);  Serial.print("/");  Serial.println(grens);} void taakBewaren(unsigned long nu) {                  // de laadtoestand overleeft een stroomonderbreking  if (nu - vorigeBewaring < BEWAAR_MS) return;  vorigeBewaring += BEWAAR_MS;  EEPROM.put(ADRES_SOC, soc);                         // schrijft alleen de bytes die veranderen} // ---------------------------------------------------------------- setup en loopvoid setup() {  for (int i = 0; i < AANTAL_UIT; i++) {    pinMode(UITGANGEN[i], OUTPUT);    digitalWrite(UITGANGEN[i], LOW);                  // veilige begintoestand: alles uit  }  digitalWrite(DAK_ENABLE, HIGH);  pinMode(VLOTTER, INPUT_PULLUP);  pinMode(KNOP, INPUT);  pinMode(EIND_OPEN, INPUT_PULLUP);  pinMode(EIND_DICHT, INPUT_PULLUP);  Serial.begin(115200);  Serial.println("# klimaatmodule v2 (hoofdstuk 30)");  Wire.begin();  if (!sht.begin(0x44)) Serial.println("# ALARM: geen SHT31 op adres 0x44");  if (!rtc.begin()) Serial.println("# ALARM: geen klok (DS3231) op 0x68");  if (rtc.lostPower()) Serial.println("# de klok heeft stilgestaan: stuur T jjjj-mm-dd uu:mm:ss");  klok = rtc.now();  byte ina[] = {INA_ZON, INA_VERBRUIK};  for (int i = 0; i < 2; i++) {                       // INA226: 128 metingen middelen (282 ms)    Wire.beginTransmission(ina[i]);    Wire.write(0x00);    Wire.write(0x49);    Wire.write(0x27);    Wire.endTransmission();  }  kaartOk = SD.begin(SD_CS);  if (kaartOk && !SD.exists("LOG")) SD.mkdir("LOG");  if (!kaartOk) Serial.println("# ALARM: geen SD-kaart");  EEPROM.get(ADRES_SOC, soc);  if (isnan(soc) || soc < 0.0 || soc > 100.0) soc = 50.0;   // leeg EEPROM: een ruwe schatting  Serial.print("# laadtoestand uit het EEPROM: ");  Serial.print(soc, 1);  Serial.println(" %");  uBat = batterijspanning();  dakOpen = (digitalRead(EIND_OPEN) == HIGH);  Serial.println(KOP);} void loop() {  unsigned long nu = millis();                        // millis() blijft de klok voor de intervallen  leesOpdracht();  taakMeten(nu);  taakLampen(nu);  taakPomp(nu);  taakDak(nu);  taakStatusled(nu);  taakMelden(nu);  taakBewaren(nu);}

Een test in de simulatie op maandag 12 oktober vanaf 07:59, met de laadtoestand op 35 % en een druk op de knop van het dak om 08:00:40:

# klimaatmodule v2 (hoofdstuk 30)
# laadtoestand uit het EEPROM: 35.0 %
tijdstip,temperatuur_C,vocht_pct,verbruik_Wh,zon_Wh,licht_lx,batterij_V
2026-10-12 07:59,17.9,84.0,0.85,1.11,1578,13.07
# 08:00:00  pomp aan
# 08:00:00  T 18.0 C  RV 84 %  licht 1666 lx  bat 13.05 V -0.1 A  SoC 35.0 %  lampen 86/90
# 08:00:40  dak openen
# 08:00:44  dak open
2026-10-12 08:00,18.0,83.9,1.33,1.31,1742,13.05
# 08:01:00  T 18.0 C  RV 84 %  licht 1831 lx  bat 13.05 V -0.1 A  SoC 35.0 %  lampen 86/90
2026-10-12 08:01,18.0,83.8,1.52,1.52,1908,13.05
# 08:02:00  T 18.1 C  RV 84 %  licht 1997 lx  bat 13.05 V 0.0 A  SoC 35.0 %  lampen 104/100
2026-10-12 08:02,18.1,83.7,1.73,1.74,2075,13.05
# 08:03:00  T 18.1 C  RV 84 %  licht 2161 lx  bat 13.03 V 0.2 A  SoC 35.0 %  lampen 124/120

De logregels staan tussen de meldingen; lees_serieel.py kan de #-regels overslaan. millis() is niet weg: ze blijft de klok voor de intervallen, want die moeten ook doorlopen als iemand de RTC verzet. De RTC geeft de datum en het uur.

Naslag · 30.28

UART, I²C en SPI naast elkaar

28/29
stijgtijd van een I²C-lijn (30 % tot 70 %)
symboolbetekeniseenheid
pull-upweerstandΩ
capaciteit van de busF
seriële verbinding 8N1
UART (Serial)I²C (Wire)SPI (SD-kaart)
dradenTX, RX (+ GND)SDA, SCL (+ GND)MOSI, MISO, SCK, CS per apparaat (+ GND)
klokgeen: beide kanten kennen de baudrateSCL, van de masterSCK, van de master
apparatentweetot ±100, elk een eigen adresmeerdere, elk een eigen CS-draad
snelheid (Mega)tot 115 200 baud en meer100 of 400 kHzenkele MHz
afstandmeters (tot tientallen met RS-485)centimeters tot een paar metercentimeters
in de moduleMega ↔ computerSHT31, DS3231, 2 × INA226SD-kaart
pyserialwat het doet
pip install pyserialeenmalig installeren (je importeert het als serial)
python -m serial.tools.list_portswelke poorten er zijn (COM3, /dev/ttyACM0 …)
poort = serial.Serial("COM3", 115200, timeout=5)openen; de Mega herstart, wacht 2 s
poort.readline()bytes tot en met \n, of b"" na de timeout
.decode("utf-8").strip()bytes naar tekst, zonder \r\n
poort.write(b"T2026-10-11 20:00:00\n")tekst naar de Mega sturen (als bytes)
poort.close()sluiten: pas dan kan de seriële monitor de poort weer openen
Het geheel · 30.29

Waar dit hoofdstuk in het geheel past

29/29

Datalogging is waar de leerlijnen elkaar ontmoeten: elektronica (pull-ups, shunts), programmeren in twee talen, onderzoeken (tijdstempels, lege cellen, eerlijk rapporteren) en data (opschonen, samenvatten, grafieken). De logger is ook het begin van de digitale tweeling van het station in Expert: een model dat je alleen kan bouwen en controleren met goede metingen.

Het CSV-formaat, DictReader, woordenboeken per dag en matplotlib: hier op echte logbestanden.

Taken met millis(), Serial, bibliotheken en klimaat.ino, waar de logger een taak in wordt.

De RC-tijd achter de stijgtijd van een I²C-lijn, en de supercondensator die het bestand laat sluiten.

Bits, bytes, hexadecimaal en aliasing met de PWM van de lampen.

De knooppuntwet: batterijstroom = zonnestroom − verbruik.

De laadtoestand: met twee INA226's nauwkeuriger dan met de ACS712 alleen.

Het energiebeheer dat je in de gegevens van woensdagavond terugziet.

Een regeling beoordelen met een meting: daar is het logboek voor.

Grafieken kiezen en een beslisboom trainen op de logbestanden.

Meetonzekerheid en standaardfout op minuutgemiddelden.

SPI en I²C in detail, interrupts, slaapstanden en de gegevens draadloos naar de centrale computer (MQTT).

Het verbruik van morgen voorspellen uit een logboek van maanden.

HabitatDe klimaatmodule houdt nu een logboek bij: logger.ino (of klimaat.ino versie 2) meet om de 2 s en schrijft elke minuut een regel met tijdstip, temperatuur, vocht, verbruik, zonnestroom, licht en batterijspanning, op een SD-kaart in LOG/JJJJMMDD.CSV en naar de computer. De sensoren hangen aan één I²C-bus: SHT31 (0x44, in de sensorkast op 3 m, met pull-ups van 2,2 kΩ extra), DS3231 (0x68, wintertijd) en twee INA226 (0x40 zon, 0x41 verbruik). Een dag is ±68,5 kB (1 GB is ±40 jaar); de logger zelf kost ±1,6 Wh per dag. Opdracht 30.13 is het ontwerp voor je portfolio; in hoofdstuk 39 en 40 analyseer je maanden aan gegevens.
Samenvatting

In het kort

Begrippen

Wat je nu kent

datalogger
Toestel of programma dat metingen automatisch met een tijdstempel bewaart.
UART
Seriële verbinding zonder klokdraad: elk teken gaat als startbit, databits en stopbit over één draad (TX naar RX).
baudrate
Aantal bits per seconde op een seriële verbinding; beide kanten moeten dezelfde gebruiken.
protocol
Afspraak over hoe twee toestellen gegevens uitwisselen: vorm, volgorde en betekenis.
I²C
Bus met twee draden (SDA voor data, SCL voor de klok) waarop veel chips hangen, elk met een eigen adres.
pull-upweerstand
Weerstand naar de voedingsspanning die een lijn hoog houdt als niemand ze laag trekt.
CRC
Controlegetal dat uit de data berekend wordt: klopt het niet, dan is er onderweg iets misgegaan.
RTC (real-time clock)
Klokchip die datum en tijd bijhoudt, ook zonder voeding van de microcontroller (knoopcel).
BCD
Binair gecodeerd decimaal: elk decimaal cijfer apart in 4 bits, zoals 0x34 voor 34.
SPI
Snelle bus met een klokdraad, twee datadraden en een selectiedraad per apparaat; hier voor de SD-kaart.
tijdstempel
Datum en tijd die bij een meting horen.
opschonen (data)
Fouten in data opsporen en behandelen: lege cellen, onmogelijke waarden, dubbele of ontbrekende regels.
shunt
Weerstand van enkele milliohm in een stroomleiding; de spanning erover is evenredig met de stroom.
Aan de slag in het werkboek16 opdrachten, van oefenen tot uitdagen