← Back to CoursesStemExpert
StemExpert
Intermediate · Handboek · 29. Microcontrollers programmeren in C++
StemExpert · Brecht Corbeel · schoolium.me
StemExpert
I29. Microcontrollers programmeren in C++
IIntermediate · deel I7 · Programmeren

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.

7× uitleg3× uitgewerkt voorbeeld1× proef1× ontleed3× naslag1× verhaal1× het geheel6× code16 opdrachten in het werkboek± 9 lestijden
De Mega in de kast van de klimaatmodule: één programma voor sensoren, pomp, lampen en dak.
De Mega in de kast van de klimaatmodule: één programma voor sensoren, pomp, lampen en dak.
Na dit hoofdstuk
Verhaal · 29.1

248 dagen: een teller in een vliegtuig

1/23

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.

Waarom dit verhaal ertoe doet

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.

Een vliegtuig aan de grondstroom: ook als het niet vliegt, draaien de computers aan boord door.
Een vliegtuig aan de grondstroom: ook als het niet vliegt, draaien de computers aan boord door.
Uitleg · 29.2

Wat je al weet, en wat er nu verandert

2/23

Je hebt al heel wat C++ geschreven, alleen nooit alles tegelijk:

wat je al kanwaarwat er nu bij komt
setup() en loop(), pinMode, digitalWrite, digitalRead, delay()F24wat er onder setup() en loop() gebeurt, en waarom delay() in een groot programma niet meer kan
een sensor lezen met analogRead en een drempelF25, hoofdstuk 22alle 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 Pythonhoofdstuk 27-28types met een vaste grootte, accolades en puntkomma's
binaire getallen, bits en byteshoofdstuk 25waarom 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.

Leesadvies

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.

SHT31 op I²C (20, 21)lucht: T en RVLDR op A1daglichtA3 en A4batterij: U en Ivlotter op D2genoeg water?knop S1 op D22kasdak open/dichtD23 en D24eindeloop dakArduino Mega 2560klimaat.ino: takenzonder delay()D5 (PWM)groeilampenD6, D7, D8dakmotor (H-brug)D9pompD10CO₂-klepD11ventilatorenD13statusledcomputer (USB)seriële monitor enplotteringangenuitgangen
Zes groepen ingangen en zes groepen uitgangen, één programma. De pijlen tonen de richting van de informatie.
Ontleed · 29.3

De Arduino Mega 2560

3/23

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.

LAREF · 13 … 2 · 1 014 … 19 · 20 2122535V · GND · VinA0 … A152560
Vereenvoudigde bovenkant van de Mega 2560 (101,6 × 53,3 mm): de pinrijen en de belangrijkste onderdelen.
Wijs een nummer aan: de figuur zoomt in. Klik om vast te zetten.
  1. 1
    USB-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.

  2. 2
    USB-chip (ATmega16U2)

    Zet USB om naar een seriële verbinding (TX/RX) voor de hoofdchip. Op de computer verschijnt de Mega als COM-poort.

  3. 3
    ATmega2560

    De microcontroller: 16 MHz, 256 kB flash voor het programma, 8 kB SRAM voor variabelen, 4 kB EEPROM. Alles wat je schrijft, draait hier.

  4. 4
    Pinnen 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.

  5. 5
    14 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.

  6. 6
    20 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).

  7. 7
    22 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).

  8. 8
    A0 tot A15: analoge ingangen

    Zestien ingangen voor de 10-bits ADC. A0 tot A6 zijn bezet: NTC, LDR, pH, batterij, stroom, druk, EC.

  9. 9
    Voedingspinnen

    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).

  10. 10
    Ledje L

    Hangt aan pin 13. De statusled van de module zit op dezelfde pin: je ziet hem ook op het bordje knipperen.

  11. 11
    Resetknop

    Start het programma opnieuw: setup() loopt weer en millis() begint bij 0. Globale variabelen krijgen hun beginwaarde terug.

Uitleg · 29.4

Hoe een schets draait: van tekst tot stroompjes

4/23

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.

Teksten in flash met F()

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.

Flash: 256 kBhet programma: klimaat.ino, de bibliotheken ende vaste teksten; 8 kB is de bootloaderblijft bij stroomuitvalSRAM: 8 kBvariabelen, de stack, de buffersvan Serial en Wireweg bij stroomuitvalEEPROM: 4 kBwat je wil onthouden: delaadtoestandblijft; ±100 000 keer schrijven per byteprogrammawerkgeheugenbewaren
Flash voor het programma, SRAM voor wat verandert, EEPROM voor wat moet blijven. De balken zijn niet op schaal.
Naslag · 29.5

Types in C++ op de Mega

5/23

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.

bereik van een geheel type met n bits
symboolbetekeniseenheid
aantal bits (8 per byte)
typebytesbereikgebruik in de module
bool1true, falsepompAan, dakOpen, genoegWater
byte (= uint8_t)10 tot 255een I²C-adres, een byte van een sensor
int2−32 768 tot 32 767ADC-waarden (0-1023), PWM (0-255), pinnummers
unsigned int20 tot 65 535tellers die niet negatief worden
long4±2 147 483 647een som van 100 ADC-waarden
unsigned long40 tot 4 294 967 295elke tijd in ms: millis(), vorige, intervallen
float (double)4±3,4 · 10³⁸, ±7 cijfersspanning, 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.

typebytes (8 bits per vakje)bereik op de Megabooltrue of falsebyte0 tot 255charéén teken, bv. 'A' (65)int−32 768 tot 32 767unsigned int0 tot 65 535long−2 147 483 648 tot 2 147 483 647unsigned long0 tot 4 294 967 295float, double±3,4 · 10³⁸, ongeveer 7 cijfers
Elk vakje is één byte (8 bits). Meer vakjes geeft een groter bereik; float gebruikt zijn 4 bytes voor een getal met een komma.
Code · 29.6

Wat past er in welk type?

6/23

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.

C++ (Arduino)types.ino45 regelsDownload
// 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.

"De compiler zou me wel waarschuwen"

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.

Uitgewerkt voorbeeld · 29.7

Een uur in milliseconden

7/23

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?

Gegeven
  • int op de Mega: 16 bit, van −32 768 tot 32 767
  • unsigned long: 32 bit, van 0 tot 4 294 967 295
Gevraagd
  • de waarde van CYCLUS_MS
  • een juiste schrijfwijze
Oplossing
  1. 1
    In 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.
  2. 2
    Stap 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: .
  3. 3
    Bewaren 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.
  4. 4
    Narekenen 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
  5. 5
    Juist schrijven. Maak het eerste getal al een unsigned long, dan rekent de Mega alles in 32 bits: 60UL * 60 * 1000 = 3 600 000.
Antwoord
CYCLUS_MS = 4 294 962 816 in plaats van 3 600 000. Schrijf 60UL * 60 * 1000.
Klopt dit? Past het antwoord? , dus in een unsigned long is er ruimte genoeg. Vuistregel: zodra een tijd in ms boven 32 767 kan komen (33 seconden), zet je UL achter het eerste getal.
Uitleg · 29.8

Functies en variabelen: waar leeft een waarde?

8/23

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 vorigeMeting en toestanden als pompAan moeten globaal zijn, want ze moeten van de ene ronde van loop() naar de volgende blijven.
  • Lokaal, binnen een functie: bestaat alleen tijdens die aanroep. som hierboven 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 vorige bijhoudt, 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.

globaal: bovenaan, buiten elke functiebestaat zolang de Mega draait; elke functie kan erbijunsigned long vorigeMeting; float soc; int stand;taakMeten(unsigned long nu)parameter nu: een kopielokaal: int n (weg na de return)loop()lokaal: unsigned long nu(elke ronde opnieuw gemaakt)taakNiveau(unsigned long nu)static unsigned long vorige = 0;static: alleen hier zichtbaar, maar de waarde blijft tussen aanroepen
Een globale variabele leeft zo lang als het programma, een lokale zo lang als één aanroep.
Uitleg · 29.9

Ingangen en uitgangen van de module

9/23

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.

pinonderdeelpinModelezen of schrijvenwaarden
D2vlotter (naar GND)INPUT_PULLUPdigitalReadLOW = genoeg water
D22knop S1 via RC en 74HC14INPUTdigitalReadHIGH = ingedrukt
D23, D24eindeloop open, dicht (verbreekcontact)INPUT_PULLUPdigitalReadHIGH = eindpunt (of draad los)
A0-A6NTC, LDR, pH, batterij, stroom, druk, EC(niet nodig)analogRead0 tot 1023
D5groeilampen (MOSFET)OUTPUTanalogWrite0 tot 255 (PWM, 490 Hz)
D6, D7dakmotor sluiten, openen (H-brug)OUTPUTanalogWrite0 tot 255
D8, D9, D10, D11H-brug aan, pomp, CO₂-klep, ventilatorenOUTPUTdigitalWriteLOW of HIGH
D13statusled (en ledje L)OUTPUTdigitalWriteLOW of HIGH
D20, D21I²C: SDA en SCL(Wire regelt het)Wirehoofdstuk 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 is geen voeding

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.

vlotter op D2in de Mega+5 V20-50 kΩD2vlotterdicht (water) = LOWknop S1 op D22+5 V10 kΩS110 kΩ1 µF74HC14D22ingedrukt = HIGH (na 11-15 ms)NTC op A0+5 VNTCA010 kΩ0 tot 1023
Links een schakelaar met de interne pull-up, midden de knop met RC-filter en Schmitt-trigger, rechts een sensor als spanningsdeler.
statusled op D13D13220 ΩgroenHIGH = led aan (±13 mA)pomp op D9 (hoofdstuk 23)D9220 Ω10 kΩIRLZ44NMpomp 12 V+12,8 V1N5819HIGH = pomp aan (1 A)
Een led kan rechtstreeks aan een pin; voor de pomp stuurt de pin de gate van een MOSFET, de diode vangt de piek bij het uitschakelen op.
Code · 29.10

Serial: meekijken in je programma

10/23

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:

C++ (Arduino)batterij_debug.ino23 regelsDownload
// 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.

Debuggen met Serial, in drie regels

(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).

Code · 29.11

De seriële plotter

11/23

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.

C++ (Arduino)plotter.ino23 regelsDownload
// 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);  }}
0,00,51,01,52,02,53,0400425450475500525550575600t (s)waardelichtaanuit
De eerste drie seconden uit de simulatie: een wolk trekt voor de zon en de waarde duikt onder 'aan'.

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.

Proef · 29.12

delay() tegenover millis(): hoeveel drukken mis je?

12/23
Onderzoeksvraag

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()?

Materiaal
  • 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)
knop ingedruktled D13 (delay)knop bekeken (delay)02468101214161820t (s)
De eerste 20 s van proef A: alleen een druk die over een kijkmoment valt, telt.
Werkwijze
  1. 1Laad knop_delay.ino op en open de seriële monitor op 115200 baud.
  2. 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.
  3. 3Laad knop_millis.ino op en herhaal met twintig drukken.
  4. 4Voorspel vooraf: welk deel van de tijd kijkt knop_delay.ino naar de knop?
Waarneming

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);
    }
  }
}
Verklaring

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.

De opstelling van de proef: een knop, een led en de seriële monitor.
De opstelling van de proef: een knop, een led en de seriële monitor.
Uitleg · 29.13

millis(): kijken hoe laat het is, in plaats van wachten

13/23

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 geeft nu door aan alle taken, zodat ze met dezelfde tijd rekenen.
  • vorige += INTERVAL, niet vorige = nu. Komt de lus 3 ms te laat, dan blijft de volgende beurt toch op het vaste ritme; met = nu schuift 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 - vorige vergelijken met het interval, nooit nu >= 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.

loop() begintnu = millis()nu − vorige ≥INTERVAL?neejavorige += INTERVALdoe de taak (meten,knipperen …)de volgende taak, zelfdevorm
Elke taak heeft dezelfde vorm: is het tijd? Zo ja, schuif de klok op en doe het werk.
Code · 29.14

Vier taken tegelijk

14/23

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().

C++ (Arduino)taken.ino76 regelsDownload
// 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.

knop D22 (ingang)flank herkendmeten (A1)statusled D13pomp D900,511,522,533,544,555,566,5t (s)
De vier taken lopen door elkaar zonder op elkaar te wachten. Getekend uit de pinwissels van de simulatie.
vlotter D2: te weinig waterpomp D9020406080100120140160180t (min)
De pomp volgt haar uurritme; tijdens het watertekort blijft ze uit en daarna gaat ze verder tot het einde van de 10 minuten.
Uitgewerkt voorbeeld · 29.15

Na 49,7 dagen: de overloop van millis()

15/23

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)?

Gegeven
  • unsigned long: 32 bit, alle bewerkingen modulo
  • vorige = 4 294 966 000, INTERVAL = 2000
Gevraagd
  • wanneer elke versie de volgende meting doet
Oplossing
  1. 1
    Wanneer zou de meting moeten komen? 2000 ms na vorige, dus 704 ms na de overloop: .
  2. 2
    Versie 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.
  3. 3
    Versie 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.
  4. 4
    Narekenen met Python. Een Python-int loopt nooit over; met % 2**32 reken 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
  5. 5
    De 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
Antwoord
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.
Klopt dit? Werkt het ook als je tijden in een 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.
0255075100125150012345tijd sinds de start (dagen)millis() (miljard ms)2³² ms = 49,7 dagen
millis() klimt 49,7 dagen lang en begint dan weer bij 0.
0306090120150−40 000−20 000020 00040 000tijd sinds de start (s)int t = millis()
Dezelfde tijd in een int: na 32,8 s negatief, na 65,5 s weer bij 0.
Uitleg · 29.16

Bibliotheken en eigen bestanden

16/23

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.

klimaat/ (de schetsmap)klimaat.inosetup(), loop() en de takenpinnen.hde pintabel (eigen bestand)sensoren.hwat de sensorfuncties doensensoren.cpphoe ze het doenArduino/libraries/Adafruit_SHT31/via de bibliotheekbeheerderAdafruit_BusIO/nodig voor Adafruit_SHT31Wire, EEPROM, SPI, SDkomen mee met de IDE#include "pinnen.h"#include <Adafruit_SHT31.h>
Eigen bestanden zitten in de schetsmap; bibliotheken in de map libraries. Tussen punthaken zoekt de compiler bij de bibliotheken.
Code · 29.17

Een eigen headerbestand: pinnen.h

17/23

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".

C++ (Arduino)pinnen.h33 regelsDownload
// 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) #endif

Drie 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 .h en een .cpp

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.

Uitleg · 29.18

Het plan: taken en intervallen van de klimaatmodule

18/23

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:

taakhoe vaakleestschrijftuit
taakMetenom de 2 sSHT31, A1, A3, A4luchtT, luchtRV, lux, uBat, iBat, soc, grens16, 22, 25, 20
taakLampenom de 200 mslux, grens, klokD5 (PWM)25
taakPompelke rondeD2, klokD923
taakDakelke rondeD22, D23, D24D6, D724, 26
taakStatusledom de 500 ms (alarm: 100 ms)genoegWater, socD13dit hoofdstuk
taakMeldenom de minuutallesSerialdit hoofdstuk
taakBewarenom het uursocEEPROMdit 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.

45 mstaakMeten bezigstatusled D1319002000210022002300240025002600t (ms)
Uit de simulatie van klimaat.ino: zolang taakMeten loopt, krijgt geen enkele andere taak een beurt.
Uitgewerkt voorbeeld · 29.19

De sturing opbouwen in vijf stappen

19/23

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.

Gegeven
  • de pintabel (pinnen.h)
  • de losse schetsen uit hoofdstuk 16 en 22 tot 26
  • het plan met zeven taken
Gevraagd
  • een volgorde van stappen, met per stap een test
Oplossing
  1. 1
    Stap 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:
    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);
    }
    Test: na het opladen blijft alles uit en de monitor toont de laadtoestand uit het EEPROM.
  2. 2
    Stap 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.
  3. 3
    Stap 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 tijdelijk CYCLUS_MS op 60 s en POMP_AAN_MS op 10 s.
  4. 4
    Stap 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.
  5. 5
    Stap 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.
Antwoord
Vijf stappen, elk met een eigen test; het volledige programma staat op het volgende blad.
Klopt dit? Waarom deze volgorde? Wat meet, kan niets kapotmaken; wat beweegt (pomp, dak), komt pas als de metingen kloppen; en het energiebeheer, dat alles aan elkaar knoopt, komt als laatste.
Code · 29.20

klimaat.ino: de volledige sturing

20/23

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.

C++ (Arduino)klimaat.ino268 regelsDownload
// 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);}
0123456789101112131415020406080100120140160180200220240260tijd na 06:00 (min)vermogen (W)pomp uit (vlotter)pomp uitzonnestroomlampenlampen zonder energiebeheer
Uit dezelfde simulatie: tot 06:08 volgen de lampen de zonnestroom min het basisverbruik; daarna begrenst het daglicht ze.

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.

Naslag · 29.21

Foutzoektabel

21/23

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.

foutwat je zietwat de compiler zegtoplossing
puntkomma vergetencompileert nietexpected ',' or ';' before 'if': op de volgende regelkijk één regel hoger
hoofdletter fout: digitalwritecompileert niet'digitalwrite' was not declared in this scopeC++ onderscheidt hoofd- en kleine letters
accolade } vergetencompileert nieta function-definition is not allowed here, expected '}' at end of inputlaat de IDE inspringen (Ctrl+T) en kijk waar het scheef loopt
variabele buiten haar functie gebruiktcompileert niet'n' was not declared in this scopemaak ze globaal of geef ze terug met return
bibliotheek niet geïnstalleerd of typfout in de naamcompileert nietAdafruit_SHT3x.h: No such file or directorybibliotheekbeheerder; naam van het .h-bestand nakijken
if (x = 5) in plaats van ==de if is altijd waar, x wordt 5alleen een waarschuwing: suggest parentheses around assignment== vergelijkt, = kent toe
puntkomma na de if: if (x > 3);het blok eronder loopt altijdwaarschuwing: empty body in an 'if' statementgeen puntkomma na if (…)
gehele deling: 5 / 9 * (t - 32)altijd 0nietsschrijf 5.0 / 9
overloop: int wacht = 60 * 1000;−5536 en rare wachttijdenwaarschuwing integer overflow in expression (als waarschuwingen aanstaan)60000UL of 60UL * 1000, en unsigned long
tijd in een int bewarenna 32,8 s gaat alles misnietstijd altijd in unsigned long
delay() in de lusknoppen en eindeloopschakelaars reageren nietnietstaken met millis()
pinMode(…, OUTPUT) vergetenled brandt zwak of niet, MOSFET half opennietsalle uitgangen instellen in setup()
ingang zonder pull-upde waarde springt willekeurignietsINPUT_PULLUP of een weerstand
baudrate verschilt van de monitorrare tekens op het schermniets115200 in Serial.begin en rechtsonder in de monitor
kommagetallen vergelijken met ==de vergelijking lukt bijna nooitnietsvergelijk met < of >, of met een marge
Naslag · 29.22

C++ voor de Arduino in één oogopslag

22/23
overloop van millis()
symboolbetekeniseenheid
aantal waarden van een unsigned long
functiewat ze doetgeeft terug
pinMode(pin, OUTPUT)INPUT, INPUT_PULLUP of OUTPUT kiezenniets
digitalWrite(pin, HIGH)pin op 5 V (HIGH) of 0 V (LOW)niets
digitalRead(pin)de spanning op de pin lezenHIGH 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 startunsigned long (ms, µs)
delay(ms)wachten, en verder niets doenniets
Serial.begin(115200)seriële poort openenniets
Serial.print(x, 2), printlntekst of getal sturen (2 decimalen), println met nieuwe regelaantal verstuurde tekens
constrain(x, a, b), map(…)begrenzen, omrekenen van bereikde 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
variabelesoc = 35.0float soc = 35.0; (type vooraf, puntkomma)
blokinspringen na :accolades { }; inspringen is voor de lezer
keuzeif a > b: … elif … else:if (a > b) { } else if (…) { } else { }
lusfor i in range(16):for (int i = 0; i < 16; i++) { }
functiedef lux(n): … returnfloat lux(int n) { … return …; }
en, of, nietand, or, not&&, ||, !
gehele deling7 // 2 (en 7 / 2 = 3.5)7 / 2 = 3; 7 / 2.0 = 3.5
overloopbestaat niet: een int groeit meeint 16 bit, long 32 bit: pas op
commentaar# …// … of /* … */
Het geheel · 29.23

Waar dit hoofdstuk in het geheel past

23/23

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.

HabitatDe klimaatmodule heeft nu één programma: klimaat.ino, versie 1, met pinnen.h. Zeven taken lopen naast elkaar: meten om de 2 s (SHT31 op I²C 0x44, licht, batterij), lampen om de 200 ms met energiebeheer, de pomp 10 minuten per uur met vlotterbeveiliging, het dak via de knop, de statusled op D13, een statusregel per minuut en de laadtoestand elk uur in het EEPROM. De langste ronde van loop() duurt ongeveer 45 ms. In hoofdstuk 30 komen er een echte klok en een logboek bij, in 31 en 32 de regelingen en de modi. Opdracht 29.14 voegt een niveaualarm voor het reservoir toe.
Samenvatting

In het kort

Begrippen

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.
Aan de slag in het werkboek16 opdrachten, van oefenen tot uitdagen