Handboek · hoofdstuk 29Microcontrollers programmeren in C++
Van losse schetsen naar één programma dat nooit stilstaat: de sturing van de klimaatmodule
In Fundamental liet je een led knipperen en gaf de meetkas water met een paar regels code. In hoofdstuk 22 tot 26 schreef je voor elk onderdeel van de klimaatmodule een eigen schets: de pomp, de knop, de lampen, het dak. Nu moeten ze samen op één Arduino Mega draaien, dag en nacht, maandenlang. Daarvoor moet je weten hoe groot een getal op een microcontroller mag worden, hoe je meerdere taken tegelijk laat lopen zonder delay(), hoe je met de seriële monitor in je programma kijkt, en hoe je code over bestanden en bibliotheken verdeelt. Aan het eind staat de volledige sturing van de klimaatmodule, stap voor stap opgebouwd en getest.

- Je kan uitleggen wat setup() en loop() doen, wat er bij het compileren en uploaden gebeurt en welk geheugen (flash, SRAM, EEPROM) waarvoor dient.
- Je kan voor elke grootheid het juiste C++-type kiezen (bool, byte, int, unsigned int, long, unsigned long, float) en weet dat een int op de Mega 16 bit is.
- Je kan overloop, gehele deling en te kleine types herkennen, voorspellen wat er gebeurt en de code verbeteren (L, UL, 5.0).
- Je kan functies schrijven met parameters en een returnwaarde, en het verschil uitleggen tussen globale, lokale en static variabelen.
- Je kan de in- en uitgangen van de klimaatmodule instellen en gebruiken met pinMode, digitalRead, digitalWrite, analogRead en analogWrite.
- Je kan met Serial.print en de seriële plotter tussenresultaten zichtbaar maken en zo een fout vinden.
- Je kan meerdere taken tegelijk laten lopen met millis() in plaats van delay(), en uitleggen waarom nu − vorige ≥ interval ook na de overloop van 49,7 dagen werkt.
- Je kan een bibliotheek installeren en gebruiken, en een eigen .h-bestand maken.
- Je kan een groter programma in stappen opbouwen en elke stap testen.
248 dagen: een teller in een vliegtuig
Op 1 mei 2015 publiceerde de Amerikaanse luchtvaartautoriteit FAA een dwingend voorschrift voor de Boeing 787. Een toestel dat 248 dagen onafgebroken onder stroom stond, kon alle wisselstroom verliezen: de regeleenheden van de generatoren zouden allemaal tegelijk in hun veilige stand springen. De oorzaak volgens het voorschrift: een softwareteller in die regelaars die na 248 dagen overloopt. Tot er nieuwe software was, moesten maatschappijen elk toestel geregeld volledig uitschakelen.
248 dagen is een verdacht getal. Een teller van 32 bits met een teken telt tot . Telt hij honderdsten van een seconde, dan is hij vol na , en dat is 248,55 dagen. De FAA gaf geen details over de code, maar het patroon herkent elke programmeur: een getal in een vak dat te klein is voor de tijd dat het systeem draait.
De klimaatmodule heeft precies zo'n teller. De functie millis() telt de milliseconden sinds de Mega aanging, in een vak van 32 bits zonder teken. Na , ongeveer 49,7 dagen, begint hij weer bij 0. Een kasmodule draait veel langer dan 49,7 dagen. In dit hoofdstuk leer je waarom de code van de module die overgang toch zonder haperen doorstaat, en waarom een kleine vergissing met types hetzelfde probleem al na 33 seconden zou geven.
Een microcontroller is geen computer met een scherm die je af en toe herstart. Hij draait maandenlang alleen, en een fout die pas na dagen opduikt, vind je niet met één test. Daarom leer je hier rekenen met de grenzen van elk type, vóór je het programma schrijft.

Wat je al weet, en wat er nu verandert
Je hebt al heel wat C++ geschreven, alleen nooit alles tegelijk:
| wat je al kan | waar | wat er nu bij komt |
|---|---|---|
| setup() en loop(), pinMode, digitalWrite, digitalRead, delay() | F24 | wat er onder setup() en loop() gebeurt, en waarom delay() in een groot programma niet meer kan |
| een sensor lezen met analogRead en een drempel | F25, hoofdstuk 22 | alle sensoren van de module in één meettaak |
| losse schetsen: pomp (23), knop (24), lampen (25), dak (26), laadtoestand (16) | hoofdstuk 16, 22-26 | één programma waarin ze tegelijk werken |
| variabelen, types, functies en lijsten in Python | hoofdstuk 27-28 | types met een vaste grootte, accolades en puntkomma's |
| binaire getallen, bits en bytes | hoofdstuk 25 | waarom een int op de Mega na 32 767 omslaat |
Elke schets uit hoofdstuk 22 tot 26 werkte op zich. Maar zet je ze achter elkaar in één loop(), dan gaat het mis: water_geven.ino uit F25 wacht met delay(900000UL) een kwartier. In dat kwartier leest de Mega geen knop, geen vlotter en geen eindeloopschakelaar. Het dak zou tegen zijn eindpunt blijven duwen tot de zekering smelt.
Het blokschema toont wat de Mega van de klimaatmodule allemaal tegelijk moet doen. Links de ingangen (sensoren en schakelaars), rechts de uitgangen (actuatoren). In het midden draait één programma dat elke taak op tijd een beurt geeft. Onderaan de USB-kabel naar de computer: daarlangs laad je het programma op en kijk je mee met de seriële monitor.
Heb je hoofdstuk 22 tot 26 niet gedaan, dan volstaat de pintabel verderop. De natuurkunde achter elke sensor en actuator staat in die hoofdstukken; hier gaat het om het programma.
De Arduino Mega 2560
De meetkas heeft genoeg aan een Arduino Uno. De klimaatmodule heeft meer pinnen nodig: zeven analoge sensoren, een knop, eindeloopschakelaars, zes actuatoren en een I²C-bus. De Mega 2560 heeft er 70 (54 digitale, waarvan 15 met PWM, en 16 analoge), vier seriële poorten en vier keer zoveel werkgeheugen als de Uno. Programmeren gaat precies zoals op de Uno.
- 1USB-aansluiting
Voeding (5 V) en de verbinding met de computer: hierlangs laad je programma's op en praat de Mega met de seriële monitor.
- 2USB-chip (ATmega16U2)
Zet USB om naar een seriële verbinding (TX/RX) voor de hoofdchip. Op de computer verschijnt de Mega als COM-poort.
- 3ATmega2560
De microcontroller: 16 MHz, 256 kB flash voor het programma, 8 kB SRAM voor variabelen, 4 kB EEPROM. Alles wat je schrijft, draait hier.
- 4Pinnen 2 tot 13: PWM
Digitale pinnen die ook analogWrite kunnen. De module gebruikt D2 (vlotter), D5 (lampen), D6 tot D11 (dak, pomp, CO₂, ventilatoren) en D13.
- 514 tot 19: seriële poorten 1 tot 3
Drie extra seriële poorten (TX/RX), naast poort 0 op pin 0 en 1 die met de USB-chip verbonden is.
- 620 en 21: I²C
SDA en SCL voor de I²C-bus, met 10 kΩ pull-ups op het bordje. Hier hangt de SHT31 voor temperatuur en vocht (hoofdstuk 30).
- 722 tot 53
Extra digitale pinnen: D22 (knop S1), D23 en D24 (eindeloop). Pin 50 tot 53 zijn ook de SPI-bus voor de SD-kaart (hoofdstuk 30).
- 8A0 tot A15: analoge ingangen
Zestien ingangen voor de 10-bits ADC. A0 tot A6 zijn bezet: NTC, LDR, pH, batterij, stroom, druk, EC.
- 9Voedingspinnen
5V en GND voor de sensoren, Vin voor een voeding van 7 tot 12 V. In de module komt de 5 V van een omvormer (hoofdstuk 21).
- 10Ledje L
Hangt aan pin 13. De statusled van de module zit op dezelfde pin: je ziet hem ook op het bordje knipperen.
- 11Resetknop
Start het programma opnieuw: setup() loopt weer en millis() begint bij 0. Globale variabelen krijgen hun beginwaarde terug.
Hoe een schets draait: van tekst tot stroompjes
Wat je in de Arduino-IDE typt, is C++: tekst voor mensen. Bij Verifiëren vertaalt de compiler (avr-gcc) die tekst naar machinecode voor de ATmega2560. Bij Uploaden gaat die code via USB naar het flashgeheugen. Daar zorgt een klein programmaatje, de bootloader, dat de nieuwe code binnenkomt; daarna springt de Mega naar jouw programma.
Een C++-programma begint altijd in een functie main(). Die schrijf je bij Arduino niet zelf: de IDE voegt ze toe, en ze doet ongeveer dit:
int main() {
init(); // klokken, timers en de ADC klaarzetten
setup(); // jouw setup(): één keer
for (;;) { // een lus zonder einde
loop(); // jouw loop(): opnieuw en opnieuw
}
}Na loop() komt dus meteen de volgende loop(), duizenden keren per seconde als er niets te doen is. Wat tussen twee rondes bewaard moet blijven, zet je in een globale variabele; een variabele die je binnen loop() maakt, is na elke ronde weg.
De ATmega2560 heeft drie soorten geheugen. Het programma staat in flash en blijft daar zonder stroom. De variabelen staan in het SRAM, 8 kB, en zijn weg bij een stroomonderbreking of een reset. Wat de module moet onthouden, zoals de laadtoestand van de batterij, gaat in het EEPROM: dat houdt zijn inhoud, maar elke byte verslijt na ongeveer 100 000 keer schrijven. Na het compileren meldt de IDE hoeveel flash en SRAM je programma gebruikt; hou minstens een paar honderd bytes SRAM vrij voor de stack, waar de lokale variabelen van functies komen.
Elke tekst tussen aanhalingstekens, zoals "pomp aan", wordt bij de start naar het SRAM gekopieerd. Met Serial.println(F("pomp aan")) blijft ze in het flashgeheugen. Op de Mega met 8 kB valt dat in dit programma nog mee; op een Uno met 2 kB is het vaak het verschil tussen werken en vastlopen.
Types in C++ op de Mega
In Python maakt een variabele zelf uit hoe groot ze wordt. In C++ kies jij het type, en dat legt vast hoeveel bytes ze krijgt. Met bits kan je verschillende waarden maken (hoofdstuk 25). Een type met teken gebruikt de helft voor negatieve getallen. Let op: een int is op de Mega 16 bit, op je computer 32 bit, en double is op de Mega gewoon een float.
| symbool | betekenis | eenheid |
|---|---|---|
| aantal bits (8 per byte) |
| type | bytes | bereik | gebruik in de module |
|---|---|---|---|
bool | 1 | true, false | pompAan, dakOpen, genoegWater |
byte (= uint8_t) | 1 | 0 tot 255 | een I²C-adres, een byte van een sensor |
int | 2 | −32 768 tot 32 767 | ADC-waarden (0-1023), PWM (0-255), pinnummers |
unsigned int | 2 | 0 tot 65 535 | tellers die niet negatief worden |
long | 4 | ±2 147 483 647 | een som van 100 ADC-waarden |
unsigned long | 4 | 0 tot 4 294 967 295 | elke tijd in ms: millis(), vorige, intervallen |
float (double) | 4 | ±3,4 · 10³⁸, ±7 cijfers | spanning, temperatuur, laadtoestand |
Constanten. const unsigned long MEET_MS = 2000; is een variabele die niet mag veranderen: de compiler kent haar type en controleert het. #define MEET_MS 2000 is een tekstvervanging vóór het compileren, zonder type. Kies const, behalve voor dingen die alleen met #define kunnen (zoals de beveiliging van een headerbestand). Getallen kunnen een achtervoegsel krijgen: 60000UL is een unsigned long, 100L een long, 5.0 een kommagetal.
Wat past er in welk type?
Dit programma draait één keer in setup() en toont wat er gebeurt aan de grenzen van de types. sizeof geeft het aantal bytes van een type.
// types.ino - wat past er in welk type? Draait één keer in setup().void setup() { Serial.begin(115200); Serial.print("bytes in int: "); Serial.println(sizeof(int)); // 2 op de Mega (4 op je computer) Serial.print("bytes in long: "); Serial.println(sizeof(long)); Serial.print("bytes in float: "); Serial.println(sizeof(float)); Serial.print("bytes in double: "); Serial.println(sizeof(double)); // op de Mega is double gewoon een float int a = 32767; // de grootste int a = a + 1; Serial.print("32767 + 1 = "); Serial.println(a); // overloop: de teller springt naar de kleinste int byte b = 250; b = b + 10; Serial.print("byte 250 + 10 = "); Serial.println(b); // 260 past niet in 8 bits: 260 - 256 int wacht = 60 * 1000; // bedoeld: een minuut in ms Serial.print("int wacht = "); Serial.println(wacht); long goed = 60L * 1000; // L: reken in long Serial.print("long goed = "); Serial.println(goed); Serial.print("7 / 2 = "); Serial.println(7 / 2); // twee gehele getallen: gehele deling Serial.print("7 / 2.0 = "); Serial.println(7 / 2.0); Serial.print("7 % 2 = "); Serial.println(7 % 2); // de rest float f = 16777216.0; // 2 tot de 24e f = f + 1.0; Serial.print("float 16777216 + 1 = "); Serial.println(f, 1); // een float onthoudt maar ongeveer 7 cijfers} void loop() {}Uitvoer op een Mega (seriële monitor op 115200 baud):
bytes in int: 2 bytes in long: 4 bytes in float: 4 bytes in double: 4 32767 + 1 = -32768 byte 250 + 10 = 4 int wacht = -5536 long goed = 60000 7 / 2 = 3 7 / 2.0 = 3.50 7 % 2 = 1 float 16777216 + 1 = 16777216.0
Regel voor regel: 32767 + 1 past niet meer in 16 bits met teken; de bits lopen over en het getal springt naar de kleinste int, −32 768. Een byte gaat van 255 naar 0: . 60 * 1000 rekent de Mega in int, want beide getallen zijn int: . Met 60L rekent hij in long. 7 / 2 is een gehele deling: de rest (7 % 2 = 1) valt weg. En een float onthoudt ongeveer zeven cijfers: bij 16 777 216 () is een stap van 1 al te fijn.
Voor 60 * 1000 geeft de compiler de waarschuwing integer overflow in expression, maar de Arduino-IDE toont standaard geen waarschuwingen. Zet in de voorkeuren Compilerwaarschuwingen op Alle. Voor a = a + 1 komt er nooit een waarschuwing: de compiler weet niet welke waarde a later krijgt.
Een uur in milliseconden
De pomp draait 10 minuten per uur. Een leerling schrijft const unsigned long CYCLUS_MS = 60 * 60 * 1000; en ziet de pomp nooit meer aanslaan na de eerste keer. Wat staat er in CYCLUS_MS, en hoe schrijf je het juist?
intop de Mega: 16 bit, van −32 768 tot 32 767unsigned long: 32 bit, van 0 tot 4 294 967 295
- de waarde van CYCLUS_MS
- een juiste schrijfwijze
- 1In welk type rekent de Mega? Het type links van
=telt pas bij het bewaren. De berekening zelf gebeurt in het type van de getallen: 60, 60 en 1000 zijn allemaal int, dus ook de tussenresultaten. - 2Stap voor stap. past in een int. niet: van dat getal blijven alleen de laagste 16 bits over, , en die bits lees je als int met teken: .
- 3Bewaren in unsigned long. −4480 zonder teken in 32 bits wordt . Dat is 49,7 dagen: de pomp wacht dus zeven weken op haar volgende beurt.
- 4Narekenen met Python (dat zelf nooit overloopt, dus je bootst de 16 bits na):
# uur_in_ms.py - 60 * 60 * 1000 met gehele getallen van 16 bit, zoals een int op de Mega def als_int16(x): """Houd alleen de laagste 16 bits over en lees ze als getal met teken.""" x = x % 2**16 return x - 2**16 if x >= 2**15 else x stap1 = als_int16(60 * 60) # 3600: past nog stap2 = als_int16(stap1 * 1000) # 3 600 000 past niet in 16 bits print(stap1, stap2) print("in unsigned long:", stap2 % 2**32)3600 -4480 in unsigned long: 4294962816
- 5Juist schrijven. Maak het eerste getal al een unsigned long, dan rekent de Mega alles in 32 bits:
60UL * 60 * 1000= 3 600 000.
60UL * 60 * 1000.Functies en variabelen: waar leeft een waarde?
Een groot programma schrijf je als een reeks functies, elk met één taak. Een functie in C++ vermeldt eerst het type van wat ze teruggeeft, dan haar naam en haar parameters met hun type:
float batterijspanning() { // geeft een float terug, geen parameters
return gemiddeld(BATTERIJ) * 5.0 / 1024.0 * (133.0 / 33.0);
}
int gemiddeld(int pin) { // krijgt een pin, geeft een int terug
long som = 0;
for (int i = 0; i < 16; i++) som += analogRead(pin);
return som / 16;
}
void printTwee(unsigned long x) { // void: geeft niets terug
if (x < 10) Serial.print('0');
Serial.print(x);
}Een parameter is een kopie: verandert de functie pin, dan merkt de aanroeper daar niets van. Waar een variabele gemaakt wordt, bepaalt hoe lang ze bestaat:
- Globaal, bovenaan buiten elke functie: bestaat zolang de Mega draait, elke functie kan erbij. Tijden als
vorigeMetingen toestanden alspompAanmoeten globaal zijn, want ze moeten van de ene ronde vanloop()naar de volgende blijven. - Lokaal, binnen een functie: bestaat alleen tijdens die aanroep.
somhierboven begint elke keer opnieuw bij 0, en dat is precies wat je wil. - static lokaal: alleen in die functie zichtbaar, maar de waarde blijft bewaard. Handig voor een taak die haar eigen
vorigebijhoudt, zonder dat de rest van het programma eraan kan.
Globale variabelen zijn handig, maar gevaarlijk in een groot programma: elke functie kan ze per ongeluk veranderen. Hou ze beperkt tot wat echt gedeeld moet worden, en geef ze duidelijke namen.
Ingangen en uitgangen van de module
Elke pin stel je in setup() in met pinMode. Een uitgang (OUTPUT) kan stroom leveren; een ingang (INPUT) meet alleen. INPUT_PULLUP zet binnen in de Mega een weerstand van 20 tot 50 kΩ naar 5 V: een schakelaar naar GND geeft dan LOW als hij dicht is en HIGH als hij open is. Zonder pull-up 'zweeft' een open ingang en leest ze willekeurig HIGH of LOW.
| pin | onderdeel | pinMode | lezen of schrijven | waarden |
|---|---|---|---|---|
| D2 | vlotter (naar GND) | INPUT_PULLUP | digitalRead | LOW = genoeg water |
| D22 | knop S1 via RC en 74HC14 | INPUT | digitalRead | HIGH = ingedrukt |
| D23, D24 | eindeloop open, dicht (verbreekcontact) | INPUT_PULLUP | digitalRead | HIGH = eindpunt (of draad los) |
| A0-A6 | NTC, LDR, pH, batterij, stroom, druk, EC | (niet nodig) | analogRead | 0 tot 1023 |
| D5 | groeilampen (MOSFET) | OUTPUT | analogWrite | 0 tot 255 (PWM, 490 Hz) |
| D6, D7 | dakmotor sluiten, openen (H-brug) | OUTPUT | analogWrite | 0 tot 255 |
| D8, D9, D10, D11 | H-brug aan, pomp, CO₂-klep, ventilatoren | OUTPUT | digitalWrite | LOW of HIGH |
| D13 | statusled (en ledje L) | OUTPUT | digitalWrite | LOW of HIGH |
| D20, D21 | I²C: SDA en SCL | (Wire regelt het) | Wire | hoofdstuk 30 |
analogRead duurt ongeveer 0,1 ms en geeft 0 tot 1023 voor 0 tot 5 V (hoofdstuk 25). analogWrite werkt alleen op PWM-pinnen en maakt geen echte spanning, maar een blokgolf: op D5 490 keer per seconde, met een aandeel 'aan' van waarde/255. Voor de knop D22 staat de pin op INPUT zonder pull-up: de 74HC14 levert zelf een harde 0 of 5 V (hoofdstuk 24).
Een pin van de Mega mag hoogstens 40 mA leveren (20 mA is een veilige werkwaarde). Een led met 220 Ω lukt; een pomp van 1 A niet: die schakel je met een MOSFET (hoofdstuk 23), en de pin stuurt alleen de gate.
Serial: meekijken in je programma
Een microcontroller heeft geen scherm. Met Serial.begin(115200) opent hij een verbinding over de USB-kabel; alles wat je met Serial.print stuurt, verschijnt in de seriële monitor van de IDE. println zet er een nieuwe regel achter, print(x, 2) toont een kommagetal met 2 decimalen. Zet de monitor rechtsonder op dezelfde snelheid (115200 baud), anders zie je rare tekens.
Het nuttigste gebruik: debuggen, fouten zoeken. Een leerling ziet op de monitor altijd 12.00 V voor de batterij, ook als die volgeladen is. De code ziet er juist uit. Druk dan de tussenresultaten af:
// batterij_debug.ino - waarom toont de batterij altijd 12.00 V? Tussenresultaten afdrukken.#include "pinnen.h" void setup() { Serial.begin(115200);} void loop() { int n = analogRead(BATTERIJ); float fout = n * 5 / 1024 * 133 / 33; // de oorspronkelijke regel float goed = n * 5.0 / 1024.0 * 133.0 / 33.0; // met kommagetallen Serial.print("n = "); Serial.print(n); Serial.print(" n * 5 / 1024 = "); Serial.print(n * 5 / 1024); // debug: het tussenresultaat Serial.print(" fout: "); Serial.print(fout); Serial.print(" V goed: "); Serial.print(goed); Serial.println(" V"); delay(1000); // in een testschets mag dat}Uitvoer bij vijf batterijspanningen tussen 12,05 en 14,10 V:
n = 612 n * 5 / 1024 = 2 fout: 8.00 V goed: 12.04 V n = 650 n * 5 / 1024 = 3 fout: 12.00 V goed: 12.79 V n = 670 n * 5 / 1024 = 3 fout: 12.00 V goed: 13.19 V n = 693 n * 5 / 1024 = 3 fout: 12.00 V goed: 13.64 V n = 716 n * 5 / 1024 = 3 fout: 12.00 V goed: 14.09 V
Het tussenresultaat verraadt de fout: n * 5 / 1024 is 3, terwijl ongeveer 3,27 is. Alle getallen in die berekening zijn int, dus elke deling is een gehele deling: 3,27 wordt 3, en . Pas daarna wordt het resultaat een float. Een enkele 5.0 maakt de hele berekening een kommaberekening. Deze fout is verraderlijk omdat 12,00 V plausibel lijkt: alleen doordat je de tussenstap ziet, valt ze op.
(1) Druk de invoer af (hier n), (2) druk elk tussenresultaat af, (3) reken één geval met de hand na. Waar hand en monitor verschillen, zit de fout. Haal de extra regels daarna weg: Serial kost tijd (hoofdstuk 30 rekent uit hoeveel).
De seriële plotter
Een lange kolom getallen is moeilijk te lezen. De seriële plotter (menu Tools) tekent ze als grafiek. Stuur per regel een of meer waarden als naam:waarde, gescheiden door komma's; de plotter maakt voor elke naam een lijn. Hier de lichtsensor samen met de twee drempels van de lampen (hoofdstuk 22): je ziet meteen of een wolk de lampen zou aanzetten.
// plotter.ino - de lichtsensor en zijn twee drempels in de seriële plotter (Tools > Serial Plotter)#include "pinnen.h" const int AAN = 479; // lampen aan onder 4000 lx (hoofdstuk 22)const int UIT = 552; // lampen uit boven 6000 lxunsigned long vorige = 0; void setup() { Serial.begin(115200);} void loop() { unsigned long nu = millis(); if (nu - vorige >= 200) { // vijf punten per seconde vorige += 200; Serial.print("licht:"); // naam:waarde, gescheiden door komma's Serial.print(analogRead(LDR)); Serial.print(",aan:"); Serial.print(AAN); Serial.print(",uit:"); Serial.println(UIT); }}Wat de monitor ontvangt (de plotter maakt er de grafiek hiernaast van):
licht:558,aan:479,uit:552 licht:553,aan:479,uit:552 licht:541,aan:479,uit:552 licht:519,aan:479,uit:552 licht:489,aan:479,uit:552 licht:461,aan:479,uit:552 …
Voor de monitor zijn het gewoon regels tekst; het formaat maakt er een grafiek van. Dat idee, een afgesproken formaat per regel, is de basis van de datalogger in hoofdstuk 30.
delay() tegenover millis(): hoeveel drukken mis je?
Een led knippert (1 s aan, 1 s uit) en het programma telt hoe vaak je op een knop drukt. Hoeveel van twintig korte drukken worden geteld als het knipperen met delay() gebeurt, en hoeveel met millis()?
- Arduino (Mega of Uno) met USB-kabel
- drukknop tussen pin 22 en GND (op een Uno: pin 2)
- led op pin 13 (of het ledje L op het bordje)
- de schetsen knop_delay.ino en knop_millis.ino (hieronder)
- 1Laad knop_delay.ino op en open de seriële monitor op 115200 baud.
- 2Druk twintig keer kort op de knop (ongeveer 0,2 s), op willekeurige momenten, niet in het ritme van de led. Noteer hoeveel drukken de monitor telt.
- 3Laad knop_millis.ino op en herhaal met twintig drukken.
- 4Voorspel vooraf: welk deel van de tijd kijkt knop_delay.ino naar de knop?
In een simulatie met twintig drukken van 0,2 s, om de 2,87 s, telt knop_delay.ino er 2, knop_millis.ino alle 20:
// knop_delay.ino - proef A: knipperen met delay() en ondertussen drukken tellen
const int KNOP = 22; // drukknop naar GND (proefopstelling: interne pull-up)
const int LED = 13;
int teller = 0;
int vorige = HIGH;
void setup() {
pinMode(KNOP, INPUT_PULLUP);
pinMode(LED, OUTPUT);
Serial.begin(115200);
}
void loop() {
digitalWrite(LED, HIGH);
delay(1000);
digitalWrite(LED, LOW);
delay(1000);
int lezing = digitalRead(KNOP); // de knop wordt maar om de 2 s bekeken
if (lezing == LOW && vorige == HIGH) {
teller++;
Serial.print("gedrukt: ");
Serial.println(teller);
}
vorige = lezing;
}gedrukt: 1 gedrukt: 2 -- 20 keer gedrukt
void loop() {
unsigned long nu = millis();
if (nu - vorigeKnipper >= 1000) { // 1 s aan, 1 s uit
vorigeKnipper += 1000;
ledAan = !ledAan;
digitalWrite(LED, ledAan ? HIGH : LOW);
}
int lezing = digitalRead(KNOP); // elke ronde, duizenden keren per seconde
if (lezing != vorigeLezing) {
laatsteWissel = nu;
vorigeLezing = lezing;
}
if (nu - laatsteWissel >= 20 && lezing != stabiel) {
stabiel = lezing;
if (stabiel == LOW) {
teller++;
Serial.print("gedrukt: ");
Serial.println(teller);
}
}
}knop_delay.ino kijkt maar één keer per 2 s naar de knop, na de tweede delay(1000). Een druk van 0,2 s wordt alleen geteld als hij net over dat ene moment valt: de kans is , dus gemiddeld 2 van de 20. knop_millis.ino kijkt duizenden keren per seconde en telt elke druk. Het knipperen gebeurt er tussendoor, alleen als de tijd om is. Het tijdsdiagram toont waarom proef A zo weinig ziet: de korte streepjes zijn de enige momenten waarop de knop gelezen wordt.

millis(): kijken hoe laat het is, in plaats van wachten
millis() geeft het aantal milliseconden sinds de start, als unsigned long. Het idee is eenvoudig: wacht niet, maar kijk telkens of het tijd is. Voor elke taak onthoud je in een globale variabele wanneer ze het laatst liep:
const unsigned long MEET_MS = 2000;
unsigned long vorigeMeting = 0;
void taakMeten(unsigned long nu) {
if (nu - vorigeMeting < MEET_MS) return; // nog niet aan de beurt: meteen terug
vorigeMeting += MEET_MS; // de volgende beurt is precies 2 s later
// ... meten ...
}Een taak die niet aan de beurt is, kost maar een paar microseconden. Zo krijgt elke taak duizenden keren per seconde de kans, en merkt de knop een druk van een fractie van een seconde. Drie details maken het verschil:
- Eén keer
millis()per ronde.loop()leest de klok en geeftnudoor aan alle taken, zodat ze met dezelfde tijd rekenen. vorige += INTERVAL, nietvorige = nu. Komt de lus 3 ms te laat, dan blijft de volgende beurt toch op het vaste ritme; met= nuschuift elke kleine vertraging mee en loopt de taak langzaam achter. Voor een led maakt dat niets uit, voor een meting om de 2 s wel.- Altijd
nu - vorigevergelijken met het interval, nooitnu >= vorige + INTERVAL. Waarom, zie je bij de overloop.
Dit heet niet-blokkerende code: geen enkele taak houdt de lus lang bezig. Het is geen echt multitasking, want de Mega doet nog altijd één ding tegelijk, maar zo snel na elkaar dat het lijkt of alles tegelijk gebeurt.
Vier taken tegelijk
Vier taken uit de module in één programma: meten om de 2 s, de statusled die knippert, de knop die ontdenderd wordt (hoofdstuk 24), en de pomp die 10 minuten per uur draait als er water is (hoofdstuk 23). Geen enkele delay().
// taken.ino - vier taken tegelijk, zonder delay(): meten, statusled, knop en pomp#include "pinnen.h" const unsigned long MEET_MS = 2000; // elke 2 s metenconst unsigned long KNIPPER_MS = 500; // statusled: 0,5 s aan, 0,5 s uitconst unsigned long DENDER_MS = 20; // zo lang moet de knop stil blijvenconst unsigned long POMP_AAN_MS = 10UL * 60 * 1000; // 10 minuten pompen ...const unsigned long CYCLUS_MS = 60UL * 60 * 1000; // ... per uur unsigned long vorigeMeting = 0; // globale variabelen: ze onthouden hun waardeunsigned long vorigeKnipper = 0; // tussen twee rondes van loop()unsigned long startCyclus = 0;unsigned long laatsteWissel = 0;int vorigeLezing = LOW;int stabiel = LOW;bool ledAan = false;int keerGedrukt = 0; void taakMeten(unsigned long nu) { if (nu - vorigeMeting < MEET_MS) return; // nog niet aan de beurt: meteen terug vorigeMeting += MEET_MS; int n = analogRead(LDR); // lokale variabele: bestaat alleen hier Serial.print(nu); Serial.print(" ms LDR "); Serial.println(n);} void taakStatusled(unsigned long nu) { if (nu - vorigeKnipper < KNIPPER_MS) return; vorigeKnipper += KNIPPER_MS; ledAan = !ledAan; digitalWrite(STATUSLED, ledAan ? HIGH : LOW);} void taakKnop(unsigned long nu) { // ontdenderen in software (hoofdstuk 24) int lezing = digitalRead(KNOP); if (lezing != vorigeLezing) { // er verandert iets: echt of dender laatsteWissel = nu; vorigeLezing = lezing; } if (nu - laatsteWissel >= DENDER_MS && lezing != stabiel) { stabiel = lezing; if (stabiel == HIGH) { // de flank 'net ingedrukt' keerGedrukt++; Serial.print(nu); Serial.print(" ms knop ingedrukt, keer "); Serial.println(keerGedrukt); } }} void taakPomp(unsigned long nu) { // 10 min per uur, niet zonder water (hoofdstuk 23) if (nu - startCyclus >= CYCLUS_MS) { startCyclus += CYCLUS_MS; // een nieuw uur begint } bool tijdOmTePompen = (nu - startCyclus) < POMP_AAN_MS; bool genoegWater = (digitalRead(VLOTTER) == LOW); digitalWrite(POMP, (tijdOmTePompen && genoegWater) ? HIGH : LOW);} void setup() { pinMode(KNOP, INPUT); // de 74HC14 levert zelf een harde 0 of 5 V pinMode(VLOTTER, INPUT_PULLUP); pinMode(STATUSLED, OUTPUT); pinMode(POMP, OUTPUT); Serial.begin(115200); Serial.println("taken.ino gestart");} void loop() { unsigned long nu = millis(); // één keer de klok lezen per ronde taakMeten(nu); taakStatusled(nu); taakKnop(nu); taakPomp(nu);}Uitvoer van de eerste 6,6 s, met twee drukken op de knop (van 3,30 tot 3,55 s en van 5,12 tot 5,40 s):
taken.ino gestart 2000 ms LDR 421 3320 ms knop ingedrukt, keer 1 4000 ms LDR 413 5140 ms knop ingedrukt, keer 2 6000 ms LDR 403
De metingen komen precies om de 2000 ms, de knop wordt telkens 20 ms na het indrukken herkend (de ontdenderingstijd), en ondertussen knippert de led zonder haperen. Het tijdsdiagram toont de vier taken naast elkaar. Over een langere tijd zie je de pomptaak: 10 minuten aan per uur, en uit zodra de vlotter te weinig water meldt.
Na 49,7 dagen: de overloop van millis()
Na = 4 294 967 296 ms, of 49,7 dagen, springt millis() van 4 294 967 295 terug naar 0. Stel: de laatste meting was bij vorige = 4 294 966 000, het interval is 2000 ms. Werkt if (nu - vorige >= INTERVAL) nog? En if (nu >= vorige + INTERVAL)?
unsigned long: 32 bit, alle bewerkingen modulo- vorige = 4 294 966 000, INTERVAL = 2000
- wanneer elke versie de volgende meting doet
- 1Wanneer zou de meting moeten komen? 2000 ms na vorige, dus 704 ms na de overloop: .
- 2Versie 1:
nu - vorige. Bij nu = 704 is negatief, maar een unsigned long kent geen negatieve getallen: het resultaat is modulo , . Precies het verstreken aantal ms, dus de meting komt op tijd. Het verschil van twee tijden is altijd juist, zolang er minder dan 49,7 dagen tussen liggen. - 3Versie 2:
vorige + INTERVAL. loopt zelf over en wordt 704. Vlak vóór de overloop is nu = 4 294 967 000, en is waar: de meting komt 1,3 s te vroeg. Erger: vorige wordt 704, 2704, 4704 …, allemaal kleiner dan nu, dus de taak loopt bij élke ronde opnieuw, tot vorige na ruim twee miljoen rondes weer bij nu is. - 4Narekenen met Python. Een Python-int loopt nooit over; met
% 2**32reken je zoals de Mega:# overloop.py - de Mega nabootsen: unsigned long rekent modulo 2**32 M = 2**32 INTERVAL = 2000 vorige = 4_294_966_000 # de vorige meting, 1296 ms voor de overloop for nu in (4_294_967_000, 4_294_967_295, 0, 704): verschil = (nu - vorige) % M # zoals nu - vorige in unsigned long fout = nu >= (vorige + INTERVAL) % M # zoals nu >= vorige + INTERVAL print(f"nu = {nu:>10} nu - vorige = {verschil:>10} goed: {verschil >= INTERVAL!s:5} fout: {fout}")nu = 4294967000 nu - vorige = 1000 goed: False fout: True nu = 4294967295 nu - vorige = 1295 goed: False fout: True nu = 0 nu - vorige = 1296 goed: False fout: False nu = 704 nu - vorige = 2000 goed: True fout: True
- 5De test op de 'Mega'. In de simulatie start de klok 7,3 s voor de overloop.
overloop.ino(hieronder) meet gewoon door, telkens na 2000 ms:// overloop.ino - elke 2 s een regel, ook op het moment dat millis() weer bij 0 begint const unsigned long INTERVAL = 2000; unsigned long vorige = 0; void setup() { Serial.begin(115200); vorige = millis(); } void loop() { unsigned long nu = millis(); if (nu - vorige >= INTERVAL) { Serial.print("nu = "); Serial.print(nu); Serial.print(" vorige = "); Serial.print(vorige); Serial.print(" nu - vorige = "); Serial.println(nu - vorige); vorige += INTERVAL; } }nu = 4294962000 vorige = 4294960000 nu - vorige = 2000 nu = 4294964000 vorige = 4294962000 nu - vorige = 2000 nu = 4294966000 vorige = 4294964000 nu - vorige = 2000 nu = 704 vorige = 4294966000 nu - vorige = 2000 nu = 2704 vorige = 704 nu - vorige = 2000 nu = 4704 vorige = 2704 nu - vorige = 2000
nu - vorige >= INTERVAL meet op het juiste moment (nu = 704), ook over de overloop heen. nu >= vorige + INTERVAL meet te vroeg en daarna bij elke ronde.int bewaart? Nee: een int loopt al na 32,8 s over (grafiek rechts), en dan is ook het verschil fout, omdat millis() en de int een ander bereik hebben. Tijden altijd in unsigned long.Bibliotheken en eigen bestanden
Niemand schrijft alles zelf. Een bibliotheek is code die iemand anders geschreven en getest heeft, voor een onderdeel (een sensor, een display) of een taak (I²C, bestanden op een SD-kaart). Je haalt ze in je programma met #include. Er zijn drie soorten:
- Meegeleverd met de IDE:
Wire(I²C),EEPROM,SPI,SD,Servo(F25). - Te installeren via de bibliotheekbeheerder (Tools, Manage Libraries). De kaslucht van de module krijgt een SHT31: een sensor van een paar millimeter die temperatuur (±0,2 °C) en relatieve vochtigheid (±2 %) meet en ze digitaal doorgeeft over de I²C-bus (in hoofdstuk 21 rekenden we nog met een DHT22; de SHT31 is nauwkeuriger en deelt de bus met de klok van hoofdstuk 30). Zoek 'SHT31' en installeer Adafruit SHT31 Library; de beheerder stelt voor om ook Adafruit BusIO te installeren, die ze nodig heeft.
- Eigen bestanden in de map van je schets, bv.
pinnen.h. De IDE toont ze als tabbladen.
#include <naam.h> zoekt bij de bibliotheken, #include "naam.h" eerst in de map van je schets. Een bibliotheek gebruik je bijna altijd in drie stappen: een object maken, het in setup() starten, en dan zijn functies aanroepen:
#include <Wire.h>
#include <Adafruit_SHT31.h>
Adafruit_SHT31 sht = Adafruit_SHT31(); // 1. het object
void setup() {
if (!sht.begin(0x44)) { // 2. starten op I2C-adres 0x44
Serial.println("geen SHT31 gevonden");
}
}
void loop() {
float t, rv;
sht.readBoth(&t, &rv); // 3. gebruiken: meet beide in één keer
}Wat er achter begin(0x44) en readBoth gebeurt (een adres, een commando, zes bytes en een controlegetal over twee draadjes), leer je in hoofdstuk 30. Dat is precies het nut van een bibliotheek: je kan de sensor gebruiken vóór je het protocol kent. Het & voor t en rv geeft het adres van de variabelen door, zodat de functie er twee waarden in kan zetten.
Een eigen headerbestand: pinnen.h
Elke schets van de module begon met dezelfde rij const int's. Die horen in één bestand, dan pas je een pin maar op één plaats aan. Maak in de IDE een nieuw tabblad (het pijltje rechtsboven) met de naam pinnen.h, en zet bovenaan elke schets #include "pinnen.h".
// pinnen.h - de pintabel van de klimaatmodule (Arduino Mega 2560)// Eén bestand voor alle schetsen van de module: verandert er een pin, dan pas je het maar op één plaats aan.#ifndef PINNEN_H#define PINNEN_H // analoge ingangen (0 tot 5 V, 10 bit)const int NTC = A0; // NTC 10 kOhm in het voedingsreservoir (hoofdstuk 22)const int LDR = A1; // lichtsensor onder een kapje, 1,0 kOhm naar GND (hoofdstuk 22)const int PH = A2; // pH-module (hoofdstuk 15)const int BATTERIJ = A3; // deler 100 kOhm / 33 kOhm (hoofdstuk 22)const int STROOM = A4; // ACS712-20A in de batterijleiding (hoofdstuk 16)const int DRUK = A5; // MPX5010: het niveau van het reservoir (hoofdstuk 11)const int EC = A6; // geleidbaarheid (hoofdstuk 13) // digitale ingangenconst int VLOTTER = 2; // naar GND, INPUT_PULLUP: LOW = genoeg water (hoofdstuk 23)const int KNOP = 22; // S1 'kasdak' via RC + 74HC14, INPUT: HIGH = ingedrukt (hoofdstuk 24)const int EIND_OPEN = 23; // verbreekcontact, INPUT_PULLUP: HIGH = dak open (hoofdstuk 26)const int EIND_DICHT = 24; // verbreekcontact, INPUT_PULLUP: HIGH = dak dicht // uitgangenconst int LAMPEN = 5; // PWM (490 Hz) naar de IRLZ44N van de groeilampen (hoofdstuk 25)const int PWM_DICHT = 6; // H-brug: sluiten (hoofdstuk 26)const int PWM_OPEN = 7; // H-brug: openenconst int DAK_ENABLE = 8; // H-brug: beide helften vrijgevenconst int POMP = 9; // IRLZ44N van de pomp (hoofdstuk 23)const int CO2_KLEP = 10; // IRLZ44N van de CO2-klepconst int VENTILATOREN = 11; // IRLZ44N van de ventilatorenconst int STATUSLED = 13; // groene statusled (en het ledje L op het bordje) // I2C: SDA = 20, SCL = 21 (vast op de Mega) #endifDrie dingen vallen op. De regels #ifndef PINNEN_H, #define PINNEN_H en #endif zijn een include-bewaking: wordt het bestand twee keer ingevoegd (via twee andere bestanden), dan slaat de compiler het de tweede keer over. De pinnen zijn const int: een const mag je in elk bestand invoegen, een gewone int LDR = A1; zou bij twee .cpp-bestanden een fout multiple definition geven. En de commentaren vermelden het hoofdstuk waar elk onderdeel uitgelegd is: zo blijft het bestand ook over een jaar leesbaar.
Een echte bibliotheek bestaat uit twee bestanden: in het .h-bestand staat wat er is (de declaraties, zoals float daglichtLux();), in het .cpp-bestand hoe het werkt. In opdracht 29.13 splits je zo de sensorfuncties af.
Het plan: taken en intervallen van de klimaatmodule
Voor je het volledige programma schrijft, maak je een plan: welke taken, hoe vaak, welke pinnen, en wat ze met elkaar delen. Dat plan is de klimaatmodule in tabelvorm:
| taak | hoe vaak | leest | schrijft | uit |
|---|---|---|---|---|
| taakMeten | om de 2 s | SHT31, A1, A3, A4 | luchtT, luchtRV, lux, uBat, iBat, soc, grens | 16, 22, 25, 20 |
| taakLampen | om de 200 ms | lux, grens, klok | D5 (PWM) | 25 |
| taakPomp | elke ronde | D2, klok | D9 | 23 |
| taakDak | elke ronde | D22, D23, D24 | D6, D7 | 24, 26 |
| taakStatusled | om de 500 ms (alarm: 100 ms) | genoegWater, soc | D13 | dit hoofdstuk |
| taakMelden | om de minuut | alles | Serial | dit hoofdstuk |
| taakBewaren | om het uur | soc | EEPROM | dit hoofdstuk |
De taken praten niet rechtstreeks met elkaar, maar via globale variabelen: taakMeten schrijft lux en grens, taakLampen leest ze. Zo kan je elke taak apart testen.
Het energiebeheer komt uit hoofdstuk 20: boven 40 % laadtoestand mogen de lampen volgens het schema branden, onder 20 % blijven ze uit, en daartussen alleen 'op zon'. Hoe weet de Mega of de zon ze kan voeden? Aan de batterijstroom: laadt de batterij (stroom negatief), dan is er zonne-overschot en mag de grens voor de lampen 5 stappen omhoog; levert de batterij stroom, dan 5 stappen omlaag. Zo krijgen de lampen precies het overschot. In hoofdstuk 32 wordt dit een toestandsmachine met de modi van de module.
Het tijdbudget. Een taak mag de lus niet lang ophouden. De meettaak is de traagste: de SHT31 heeft 20 ms nodig voor een meting, de stroommeting neemt 100 metingen in 20,4 ms (tien PWM-perioden, hoofdstuk 25), en er zijn nog 32 gewone ADC-metingen van 0,11 ms. In de simulatie duurde de langste ronde van loop() 44,8 ms. Het tijdsdiagram toont het gevolg: de statusled moest om 2000 ms uitgaan, maar de meettaak was toen net begonnen. Voor een led is dat onzichtbaar; voor de knop betekent het hoogstens 45 ms extra reactietijd, en de eindeloopschakelaar van het dak wordt in die tijd bij volle snelheid (0,10 m/s) 4,5 mm te laat gezien. Aanvaardbaar, maar het is de grens: een taak die langer blokkeert, moet je opsplitsen.
De sturing opbouwen in vijf stappen
Schrijf de sturing van de klimaatmodule zo dat je na elke stap iets kan testen, en dat de module nooit in een gevaarlijke toestand komt als een stap nog niet af is.
- de pintabel (pinnen.h)
- de losse schetsen uit hoofdstuk 16 en 22 tot 26
- het plan met zeven taken
- een volgorde van stappen, met per stap een test
- 1Stap 1: pinnen en een veilige start. Alle uitgangen eerst op OUTPUT en LOW: lampen, pomp en motor uit, wat er ook gebeurt. Een lijst van pinnen met een for-lus is korter en je vergeet er geen:
Test: na het opladen blijft alles uit en de monitor toont de laadtoestand uit het EEPROM.void 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 v1 (hoofdstuk 29)"); Wire.begin(); if (!sht.begin(0x44)) Serial.println("ALARM: geen SHT31 op adres 0x44"); 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); } - 2Stap 2: meten en melden. taakMeten en taakMelden, nog zonder actuatoren. Test: houd je hand op de SHT31 (T en RV stijgen), dek de LDR af (licht daalt), vergelijk de batterijspanning met een multimeter.
- 3Stap 3: de pomp. taakPomp uit
taken.ino, met een melding bij elke wissel. Test: til de vlotter op (de pomp stopt en het alarm verschijnt). Om niet een uur te wachten, zet je tijdelijkCYCLUS_MSop 60 s enPOMP_AAN_MSop 10 s. - 4Stap 4: knop, dak en statusled. De dakcode uit hoofdstuk 26, nu als taak met
nu. Test: eerst met de motor los van het dak: druk en kijk of de juiste PWM-pin reageert; houd een eindeloopschakelaar met de hand in; laat de time-out van 12 s eens aflopen. - 5Stap 5: lampen en energiebeheer. taakLampen met de grens uit taakMeten, en taakBewaren. Test: zet in het EEPROM tijdelijk een laadtoestand van 35 % en kijk of de lampen het zonne-overschot volgen.
klimaat.ino: de volledige sturing
Het volledige programma, met pinnen.h in dezelfde map en de bibliotheek Adafruit SHT31 geïnstalleerd. Het is lang, maar elke taak is een korte functie die je herkent uit een eerder hoofdstuk. Lees eerst loop() onderaan: zeven regels, zeven taken.
// klimaat.ino - de sturing van de klimaatmodule, versie 1 (Arduino Mega 2560, hoofdstuk 29)// Zeven taken zonder delay(): meten (2 s), lampen (200 ms), pomp, kasdak, statusled, melden (1 min) en bewaren (1 h).// Ventilatoren en CO2-klep krijgen hun regeling in hoofdstuk 31; de echte klok en de logger komen in hoofdstuk 30.#include <Wire.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 POMP_AAN_MS = 10UL * 60 * 1000; // 10 minuten pompen ...const unsigned long CYCLUS_MS = 60UL * 60 * 1000; // ... per uurconst unsigned long DENDER_MS = 20;const int START_UUR = 6; // de Mega wordt om 6 uur aangezet (echte klok: hoofdstuk 30)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]); // ---------------------------------------------------------------- toestand (globaal: alle taken gebruiken ze)Adafruit_SHT31 sht = Adafruit_SHT31();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 startCyclus = 0, 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} // ---------------------------------------------------------------- tijdunsigned long secondenVanDeDag(unsigned long nu) { return (START_UUR * 3600UL + nu / 1000) % 86400UL;} void printTwee(unsigned long x) { // altijd twee cijfers: 7 -> 07 if (x < 10) Serial.print('0'); Serial.print(x);} void printTijd(unsigned long nu) { unsigned long s = secondenVanDeDag(nu); printTwee(s / 3600); Serial.print(':'); printTwee(s / 60 % 60); Serial.print(':'); printTwee(s % 60); Serial.print(" ");} // ---------------------------------------------------------------- takenvoid taakMeten(unsigned long nu) { if (nu - vorigeMeting < MEET_MS) return; vorigeMeting += MEET_MS; 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} 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(nu) / 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) if (nu - startCyclus >= CYCLUS_MS) startCyclus += CYCLUS_MS; bool tijd = (nu - startCyclus) < POMP_AAN_MS; bool water = (digitalRead(VLOTTER) == LOW); bool aan = tijd && water; if (water != genoegWater) { // alleen melden als er iets verandert printTijd(nu); Serial.println(water ? "vlotter: weer genoeg water" : "ALARM vlotter: te weinig water"); genoegWater = water; } if (aan != pompAan) { printTijd(nu); 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(nu); 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(nu); 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; 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(nu); 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 v1 (hoofdstuk 29)"); Wire.begin(); if (!sht.begin(0x44)) Serial.println("ALARM: geen SHT31 op adres 0x44"); 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);} void loop() { unsigned long nu = millis(); // één keer de klok lezen per ronde taakMeten(nu); taakLampen(nu); taakPomp(nu); taakDak(nu); taakStatusled(nu); taakMelden(nu); taakBewaren(nu);}Een test in de simulatie: een lentemorgen vanaf 06:00, laadtoestand 35 % in het EEPROM, het daglicht stijgt van 1500 naar 4000 lx en de zonnepanelen leveren van 60 naar 250 W. Om 06:03:10 drukt iemand op de knop van het dak, van 06:05:00 tot 06:06:30 meldt de vlotter te weinig water. De seriële monitor:
klimaatmodule v1 (hoofdstuk 29) laadtoestand uit het EEPROM: 35.0 % 06:00:00 pomp aan 06:01:00 T 17.9 C RV 84 % licht 1666 lx bat 13.05 V -0.1 A SoC 35.0 % lampen 76/80 06:02:00 T 18.0 C RV 84 % licht 1831 lx bat 13.05 V 0.0 A SoC 35.0 % lampen 94/90 06:03:00 T 18.0 C RV 83 % licht 1997 lx bat 13.03 V 0.2 A SoC 35.0 % lampen 114/110 06:03:10 dak openen 06:03:15 dak open 06:04:00 T 18.1 C RV 83 % licht 2161 lx bat 13.05 V -0.2 A SoC 35.0 % lampen 126/130 06:05:00 ALARM vlotter: te weinig water 06:05:00 pomp uit 06:05:00 T 18.2 C RV 83 % licht 2322 lx bat 13.05 V 0.0 A SoC 35.0 % lampen 144/140 06:06:00 T 18.3 C RV 83 % licht 2492 lx bat 13.05 V -0.2 A SoC 35.0 % lampen 176/180 06:06:30 vlotter: weer genoeg water 06:06:30 pomp aan 06:07:00 T 18.3 C RV 83 % licht 2658 lx bat 13.05 V -0.2 A SoC 35.0 % lampen 176/180 06:08:00 T 18.4 C RV 82 % licht 2833 lx bat 13.05 V -0.5 A SoC 35.0 % lampen 188/240 06:09:00 T 18.5 C RV 82 % licht 2983 lx bat 13.07 V -1.6 A SoC 35.0 % lampen 185/255 06:10:00 pomp uit 06:10:00 T 18.6 C RV 82 % licht 3159 lx bat 13.11 V -2.8 A SoC 35.0 % lampen 180/255 06:11:00 T 18.7 C RV 82 % licht 3325 lx bat 13.15 V -4.9 A SoC 35.1 % lampen 177/255 06:12:00 T 18.7 C RV 82 % licht 3498 lx bat 13.17 V -6.1 A SoC 35.1 % lampen 172/255 06:13:00 T 18.8 C RV 81 % licht 3660 lx bat 13.19 V -7.2 A SoC 35.1 % lampen 169/255 06:14:00 T 18.9 C RV 81 % licht 3828 lx bat 13.20 V -8.4 A SoC 35.2 % lampen 165/255 06:15:00 T 19.0 C RV 81 % licht 3981 lx bat 13.22 V -9.6 A SoC 35.3 % lampen 161/255
Wat je ziet: bij 35 % zit de module tussen 20 en 40 %, dus de lampen krijgen alleen het zonne-overschot. De batterijstroom blijft rond 0 A, en de laadtoestand blijft 35,0 %. Als de vlotter alarm slaat, stopt de pomp en gaat de statusled snel knipperen; er komen 12 W vrij, en de lampen nemen dat over. Vanaf ongeveer 06:08 is er meer zon dan de lampen willen (bij meer daglicht dimmen ze, hoofdstuk 25): de grens klimt naar 255 en het overschot laadt de batterij, 35,3 % om 06:15. Het dak opent in 5 s. Het EEPROM is in deze 15 minuten niet beschreven: dat gebeurt maar één keer per uur.
Foutzoektabel
De fouten die iedereen maakt, met wat je ziet en wat de compiler erover zegt. De eerste vijf houdt de compiler tegen; de rest compileert zonder klagen en moet je zelf vinden, met Serial en een tijdsdiagram in je hoofd. Een compilerfout staat vaak op de regel na de echte fout.
| fout | wat je ziet | wat de compiler zegt | oplossing |
|---|---|---|---|
| puntkomma vergeten | compileert niet | expected ',' or ';' before 'if': op de volgende regel | kijk één regel hoger |
hoofdletter fout: digitalwrite | compileert niet | 'digitalwrite' was not declared in this scope | C++ onderscheidt hoofd- en kleine letters |
accolade } vergeten | compileert niet | a function-definition is not allowed here, expected '}' at end of input | laat de IDE inspringen (Ctrl+T) en kijk waar het scheef loopt |
| variabele buiten haar functie gebruikt | compileert niet | 'n' was not declared in this scope | maak ze globaal of geef ze terug met return |
| bibliotheek niet geïnstalleerd of typfout in de naam | compileert niet | Adafruit_SHT3x.h: No such file or directory | bibliotheekbeheerder; naam van het .h-bestand nakijken |
if (x = 5) in plaats van == | de if is altijd waar, x wordt 5 | alleen een waarschuwing: suggest parentheses around assignment | == vergelijkt, = kent toe |
puntkomma na de if: if (x > 3); | het blok eronder loopt altijd | waarschuwing: empty body in an 'if' statement | geen puntkomma na if (…) |
gehele deling: 5 / 9 * (t - 32) | altijd 0 | niets | schrijf 5.0 / 9 |
overloop: int wacht = 60 * 1000; | −5536 en rare wachttijden | waarschuwing integer overflow in expression (als waarschuwingen aanstaan) | 60000UL of 60UL * 1000, en unsigned long |
tijd in een int bewaren | na 32,8 s gaat alles mis | niets | tijd altijd in unsigned long |
delay() in de lus | knoppen en eindeloopschakelaars reageren niet | niets | taken met millis() |
| pinMode(…, OUTPUT) vergeten | led brandt zwak of niet, MOSFET half open | niets | alle uitgangen instellen in setup() |
| ingang zonder pull-up | de waarde springt willekeurig | niets | INPUT_PULLUP of een weerstand |
| baudrate verschilt van de monitor | rare tekens op het scherm | niets | 115200 in Serial.begin en rechtsonder in de monitor |
kommagetallen vergelijken met == | de vergelijking lukt bijna nooit | niets | vergelijk met < of >, of met een marge |
C++ voor de Arduino in één oogopslag
| symbool | betekenis | eenheid |
|---|---|---|
| aantal waarden van een unsigned long |
| functie | wat ze doet | geeft terug |
|---|---|---|
pinMode(pin, OUTPUT) | INPUT, INPUT_PULLUP of OUTPUT kiezen | niets |
digitalWrite(pin, HIGH) | pin op 5 V (HIGH) of 0 V (LOW) | niets |
digitalRead(pin) | de spanning op de pin lezen | HIGH of LOW |
analogRead(A1) | spanning 0-5 V meten (±0,1 ms) | int 0 tot 1023 |
analogWrite(5, 128) | PWM op een PWM-pin (2-13, 44-46) | niets |
millis(), micros() | tijd sinds de start | unsigned long (ms, µs) |
delay(ms) | wachten, en verder niets doen | niets |
Serial.begin(115200) | seriële poort openen | niets |
Serial.print(x, 2), println | tekst of getal sturen (2 decimalen), println met nieuwe regel | aantal verstuurde tekens |
constrain(x, a, b), map(…) | begrenzen, omrekenen van bereik | de nieuwe waarde |
EEPROM.put(adres, x), EEPROM.get(adres, x) | een variabele bewaren of terughalen | (get vult x) |
| Python (hoofdstuk 27-28) | C++ op de Mega | |
|---|---|---|
| variabele | soc = 35.0 | float soc = 35.0; (type vooraf, puntkomma) |
| blok | inspringen na : | accolades { }; inspringen is voor de lezer |
| keuze | if a > b: … elif … else: | if (a > b) { } else if (…) { } else { } |
| lus | for i in range(16): | for (int i = 0; i < 16; i++) { } |
| functie | def lux(n): … return | float lux(int n) { … return …; } |
| en, of, niet | and, or, not | &&, ||, ! |
| gehele deling | 7 // 2 (en 7 / 2 = 3.5) | 7 / 2 = 3; 7 / 2.0 = 3.5 |
| overloop | bestaat niet: een int groeit mee | int 16 bit, long 32 bit: pas op |
| commentaar | # … | // … of /* … */ |
Waar dit hoofdstuk in het geheel past
Dit hoofdstuk rijgt de losse stukken van deel I6 aan elkaar tot één programma. Het is ook een brug tussen twee manieren van programmeren: Python op een computer die rekent en tekent (hoofdstuk 27-28), en C++ op een microcontroller die meet en schakelt, zonder scherm en zonder pauze.
setup(), loop(), pinMode en digitalWrite: de eerste stappen, op de Uno van de meetkas.
water_geven.ino met delay(): het programma dat je hier zonder delay() herschrijft.
De laadtoestand tellen met de stroom op A4: het hart van het energiebeheer.
De drempels van 40 en 20 % voor de lampen.
De knop ontdenderen: in hardware en in software, met millis().
Bits, bereik en PWM: waarom een int overloopt en hoe analogWrite dimt.
De dakmotor met H-brug, eindeloop en time-out, hier een taak.
Dezelfde bouwstenen (variabelen, keuzes, lussen) in een andere taal.
Wat er achter Serial en de SHT31-bibliotheek zit, een echte klok en een logboek op een SD-kaart.
Ventilatoren en verwarming met hysterese, als nieuwe taken in klimaat.ino.
De modi dag, nacht, alarm en onderhoud met switch-case; het dak en het energiebeheer worden toestanden.
Interrupts en timers: taken die de hardware zelf op tijd start, en slaapstanden.
Een regelaar die met millis() op een vaste bemonsteringstijd rekent.
In het kort
- Een schets heeft setup() (één keer) en loop() (eindeloos); de IDE voegt main() toe. Het programma staat in flash, de variabelen in SRAM, wat moet blijven in EEPROM.
- Op de Mega is een int 16 bit (−32 768 tot 32 767), een long en unsigned long 32 bit, en double = float (4 bytes, ±7 cijfers).
- Rekenen gebeurt in het type van de getallen: 60 * 60 * 1000 loopt over in int. Schrijf 60UL * 60 * 1000; schrijf 5.0 om een gehele deling te vermijden.
- Globale variabelen onthouden iets tussen twee rondes van loop(); lokale bestaan alleen tijdens één aanroep; static lokaal combineert beide.
- pinMode kiest INPUT, INPUT_PULLUP of OUTPUT; digitalRead/digitalWrite werken met HIGH en LOW, analogRead geeft 0-1023, analogWrite 0-255 op een PWM-pin.
- Serial.print en de seriële plotter laten zien wat je programma doet: druk tussenresultaten af om een fout te vinden.
- delay() legt alles stil. Met millis() kijkt elke taak of ze aan de beurt is: if (nu − vorige ≥ INTERVAL) { vorige += INTERVAL; … }.
- millis() loopt na 49,7 dagen over; het verschil nu − vorige in unsigned long blijft toch juist. Vergelijk nooit nu ≥ vorige + INTERVAL.
- Bibliotheken haal je met #include <…>, eigen bestanden met #include "…"; een .h-bestand krijgt een include-bewaking.
- Bouw een groot programma in stappen en test elke stap; begin met een veilige toestand van alle uitgangen.
Wat je nu kent
- compileren
- Een programma in C++ vertalen naar machinecode die de microcontroller kan uitvoeren.
- setup() en loop()
- De twee functies van elke Arduino-schets: setup() loopt één keer na de start, loop() daarna eindeloos.
- type (C++)
- Wat voor waarde een variabele bevat en hoeveel geheugen ze krijgt, bv. int (2 bytes op de Mega) of float (4 bytes).
- overloop (overflow)
- Een getal dat groter wordt dan zijn type kan bevatten, springt naar het andere uiterste: 32 767 + 1 wordt −32 768 in een int.
- gehele deling
- Deling van twee gehele getallen in C++: de rest valt weg, 7 / 2 = 3.
- globale variabele
- Variabele buiten elke functie: elke functie kan ze gebruiken en ze houdt haar waarde zolang het programma draait.
- lokale variabele
- Variabele binnen een functie: ze bestaat alleen tijdens die aanroep.
- millis()
- Het aantal milliseconden sinds de start van de microcontroller, als unsigned long; na 49,7 dagen begint het weer bij 0.
- niet-blokkerende code
- Code die nooit wacht, maar telkens kijkt of het tijd is voor een taak, zodat meerdere taken tegelijk kunnen lopen.
- bibliotheek (library)
- Kant-en-klare code voor een taak of een onderdeel, die je met #include in je programma haalt.
- headerbestand (.h)
- Bestand met declaraties en constanten dat je met #include in een ander bestand invoegt.
- EEPROM
- Klein geheugen in de microcontroller dat zijn inhoud houdt zonder stroom; per byte maar ±100 000 keer te beschrijven.
- seriële plotter
- Venster in de Arduino-IDE dat getallen van de seriële poort als grafiek tekent.