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

Oplossingen · hoofdstuk 29Microcontrollers programmeren in C++

Van losse schetsen naar één programma dat nooit stilstaat: de sturing van de klimaatmodule

Uitgewerkte oplossingen bij de 16 opdrachten van het werkboek. Voor de leerkracht, en voor wie zichzelf wil verbeteren nadat hij het eerst zelf probeerde.

5× oefenen7× toepassen4× uitdagen± 419 min
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.
Voor de leerkrachtDeze pagina bevat de antwoorden. Druk het werkboek af zonder deze pagina.
Oefenen

Oefenen

één stap, direct toepassen
29.1

Welk type past?

Oefenen3Begrijpen12 min
Kies voor elke variabele van de klimaatmodule het kleinste type dat altijd volstaat, en zeg waarom. Gebruik de tabel met types uit het handboek (Mega: int = 16 bit).
  1. a
    Vul de tabel in.
    • (a) int: 0 tot 1023 past niet in een byte (hoogstens 255).
    • (b) unsigned long: millis() is een unsigned long; in een int loopt het na 32,8 s over.
    • (c) byte (0-255 past precies) of int: een int kost één byte meer maar rekent makkelijker.
    • (d) bool: alleen waar of onwaar.
    • (e) float: een kommagetal.
    • (f) 5 jaar is ongeveer 43 830 uur: te veel voor een int (32 767), wel voor een unsigned int (65 535, genoeg voor 7,5 jaar). Wie verder wil tellen, neemt unsigned long.
    • (g) long: tot 100 × 1023 = 102 300, meer dan een int of unsigned int kan bevatten.
    • (h) byte: een I²C-adres heeft 7 bits.
  2. b
    Een klasgenoot stelt voor om gewoon alles float te maken, 'dan past alles'. Geef twee redenen waarom dat geen goed idee is.
    (1) Een float onthoudt maar ongeveer 7 cijfers: een teller of een tijd in ms is na 16 777 216 (4,7 uur in ms) niet meer exact, want +1 verandert dan niets meer. (2) Vergelijken met == werkt niet betrouwbaar door afrondingen. (3) De ATmega2560 heeft geen rekeneenheid voor kommagetallen: float-rekenen gebeurt in software en is tientallen keren trager dan rekenen met gehele getallen. En een float kost 4 bytes, waar een bool of byte er 1 kost.
29.2

Voorspel de uitvoer

Oefenen3Programmeren12 min
Wat toont de seriële monitor als dit programma op een Arduino Mega draait? Schrijf per println één regel, zonder het programma uit te voeren.
void setup() {
  Serial.begin(115200);
  int a = 10;
  int b = 4;
  Serial.println(a / b);
  Serial.println(a / (float)b);
  Serial.println(a % b);
  byte x = 200;
  x = x * 2;
  Serial.println(x);
  int t = 30000;
  t = t + t;
  Serial.println(t);
  float c = (5 / 9) * (77 - 32);
  Serial.println(c);
  float d = (5.0 / 9) * (77 - 32);
  Serial.println(d, 1);
}

void loop() {
}
  1. a
    Schrijf de zeven regels die op de monitor verschijnen.

    Uitvoer in de simulatie van de Mega:

    2
    2.50
    2
    144
    -5536
    0.00
    25.0

    10 / 4 is een gehele deling (2); met (float)b rekent C++ in float (2.50). 10 % 4 is de rest (2). Een byte gaat van 400 naar 400 − 256 = 144. 30 000 + 30 000 = 60 000 past niet in een int: 60 000 − 65 536 = −5536. (5 / 9) is een gehele deling en dus 0, en 0 maal 45 blijft 0.00. Met 5.0 wordt het 25,0 (de omrekening van 77 °F naar °C).

  2. b
    Welke twee regels zouden op je computer (int = 32 bit) anders zijn? Waarom?
    Alleen de regel met t: op een computer is een int 32 bit en past 60 000 gewoon. De byte blijft 144 (een byte is overal 8 bit), en de gehele delingen blijven gehele delingen.
29.3

Welke functie, welke pin?

Oefenen3Begrijpen10 min
Vul voor elk onderdeel van de klimaatmodule in hoe je de pin instelt en leest of stuurt. Gebruik de pintabel.
  1. a
    Vul de tabel in.
    • vlotter: D2, INPUT_PULLUP, digitalRead, HIGH = te weinig water (LOW = genoeg)
    • knop S1: D22, INPUT, digitalRead, HIGH = ingedrukt
    • eindeloop dicht: D24, INPUT_PULLUP, digitalRead, HIGH = dak dicht (of draad los)
    • lichtsensor: A1, geen pinMode nodig, analogRead, 0 tot 1023
    • groeilampen: D5, OUTPUT, analogWrite, 0 tot 255 (PWM)
    • pomp: D9, OUTPUT, digitalWrite, HIGH = aan
    • statusled: D13, OUTPUT, digitalWrite, HIGH = aan
  2. b
    Waarom staat de vlotter op INPUT_PULLUP en de knop S1 op INPUT?
    De vlotter is een gewone schakelaar naar GND: zonder pull-up zweeft de pin als de schakelaar open is. De knop gaat eerst door het RC-filter en de 74HC14, en die chip zet zelf een harde 0 of 5 V op de pin; een pull-up is daar overbodig.
29.4

Vind de vijf fouten

Oefenen4Programmeren20 min
Een leerling test de pomp met dit programma: om de 5 s aan of uit, en uit als de vlotter te weinig water meldt. Er zitten vijf fouten in. Drie hoort de compiler, twee niet.
// pomp_test.ino - de pomp om de 5 s aan of uit, en de vlotter bewaken (met vijf fouten)
const int POMP = 9;
const int VLOTTER = 2;
bool aan = false;
unsigned long vorige = 0;

void setup() {
  pinMode(VLOTTER, INPUT_PULLUP);
  Serial.begin(9600);
}

void loop() {
  unsigned long nu = millis()
  if (nu - vorige >= 5000) {
    vorige += 5000;
    aan = !aan;
  }
  if (digitalRead(VLOTTER) = HIGH) {
    aan = false;
  }
  digitalwrite(POMP, aan ? HIGH : LOW);
  Serial.println(aan);
}
  1. a
    Zoek de vijf fouten. Welke drie meldt de compiler? Op welke regel verwacht je elke melding?

    De compiler (g++, zelfde kern als in de Arduino-IDE):

    wb_fouten: In function 'void loop()':
    wb_fouten:14:3: error: expected ',' or ';' before 'if'
    wb_fouten:18:18: error: lvalue required as left operand of assignment
    wb_fouten:21:3: error: 'digitalwrite' was not declared in this scope; did you mean 'digitalWrite'?
    wb_fouten:13:17: warning: unused variable 'nu' [-Wunused-variable]

    (1) regel 13: puntkomma na millis() vergeten; de melding staat op regel 14, want pas bij if merkt de compiler dat de opdracht niet af is. (2) regel 18: = in plaats van ==; hier weigert de compiler, omdat je aan een functieresultaat niets kan toekennen. (3) regel 21: digitalwrite met kleine w.

  2. b
    Welke twee fouten merk je pas als het programma draait, en wat zie je dan?
    (4) pinMode(POMP, OUTPUT) ontbreekt: de pin blijft een ingang, digitalWrite zet dan alleen de interne pull-up aan en uit. De MOSFET krijgt nauwelijks stroom op de gate en de pomp slaat niet of maar half aan. (5) Serial.begin(9600) terwijl de monitor op 115200 staat: rare tekens. (En Serial.println(aan) in elke ronde overspoelt de monitor met duizenden regels per seconde.)
  3. c
    Schrijf de verbeterde versie. Zorg er ook voor dat de monitor niet overspoeld wordt.

    Verbeterd en getest:

    // pomp_test.ino - verbeterd
    const int POMP = 9;
    const int VLOTTER = 2;
    bool aan = false;
    unsigned long vorige = 0;
    
    void setup() {
      pinMode(POMP, OUTPUT);                        // fout 4: zonder pinMode geen stevige uitgang
      pinMode(VLOTTER, INPUT_PULLUP);
      Serial.begin(115200);                         // fout 5: dezelfde snelheid als de seriële monitor
    }
    
    void loop() {
      unsigned long nu = millis();                  // fout 1: puntkomma
      if (nu - vorige >= 5000) {
        vorige += 5000;
        aan = !aan;
        Serial.println(aan ? "pomp aan" : "pomp uit");   // alleen bij een wissel iets sturen
      }
      if (digitalRead(VLOTTER) == HIGH) {           // fout 2: == vergelijkt, = kent toe
        aan = false;
      }
      digitalWrite(POMP, aan ? HIGH : LOW);         // fout 3: hoofdletter W
    }

    In de simulatie (20 s, vlotter open van 12 tot 13 s):

    pomp aan
    pomp uit
    pomp aan
    pomp uit

    Er is geen melding 'pomp uit' door de vlotter, omdat de melding alleen bij de wissel om de 5 s komt: een uitbreiding voor wie wil.

29.5

Rekenen met tijd en geheugen

Oefenen4Rekenen15 min
Gebruik: int 16 bit met teken, unsigned int 16 bit, unsigned long 32 bit; het EEPROM verslijt na ongeveer 100 000 schrijfbeurten per byte.
  1. a
    Hoeveel ms zijn 10 minuten, 1 uur en 1 dag? Past elk getal in een int? In een unsigned long?
    , , . Geen enkele past in een int (hoogstens 32 767); alle drie passen in een unsigned long (tot 4 294 967 295).
  2. b
    Na hoeveel tijd loopt millis() over? En na hoeveel tijd wordt een int waarin je millis() bewaart negatief? Een unsigned int weer 0?
    millis(): dagen. Een int is na 32 767 ms (32,8 s) vol en springt dan naar −32 768; een unsigned int is na 65 536 ms (65,5 s) weer 0.
  3. c
    Hoeveel uurcycli van de pomp passen in één periode van millis()?
    cycli.
  4. d
    klimaat.ino bewaart de laadtoestand elk uur in het EEPROM. Hoe lang houdt het EEPROM dat vol? En als je het om de 2 s zou doen?
    Elk uur: jaar. Om de 2 s: dagen. EEPROM.put schrijft wel alleen bytes die veranderen, maar een laadtoestand verandert bijna altijd.
Toepassen

Toepassen

meerdere stappen, in een context
29.6

De seriële plotter: temperatuur en vocht

Toepassen4Programmeren20 min
De SHT31 op I²C-adres 0x44 meet de temperatuur en de relatieve vochtigheid van de kaslucht. Je wil ze in de seriële plotter zien, samen met een vaste lijn op 26 °C (boven 26 °C wordt het te warm voor sla, hoofdstuk 28).
  1. a
    Schrijf een programma dat elke seconde, zonder delay(), de drie waarden in het formaat van de plotter stuurt.

    Getest met een gesimuleerde SHT31 die opwarmt:

    // plotter_sht.ino - temperatuur en vocht van de kaslucht in de seriële plotter
    #include <Wire.h>
    #include <Adafruit_SHT31.h>
    
    Adafruit_SHT31 sht = Adafruit_SHT31();
    unsigned long vorige = 0;
    
    void setup() {
      Serial.begin(115200);
      sht.begin(0x44);
    }
    
    void loop() {
      unsigned long nu = millis();
      if (nu - vorige >= 1000) {
        vorige += 1000;
        float t, rv;
        sht.readBoth(&t, &rv);
        Serial.print("temperatuur:");
        Serial.print(t, 1);
        Serial.print(",vocht:");
        Serial.print(rv, 1);
        Serial.println(",grens_26C:26");
      }
    }
    temperatuur:24.9,vocht:70.2,grens_26C:26
    temperatuur:25.2,vocht:69.4,grens_26C:26
    temperatuur:25.5,vocht:68.6,grens_26C:26
    temperatuur:25.8,vocht:67.8,grens_26C:26
    temperatuur:26.1,vocht:67.0,grens_26C:26
  2. b
    De vochtigheid schommelt tussen 60 en 85 %, de temperatuur tussen 18 en 28 °C. Waarom is het lastig om beide in één plotter te bekijken, en hoe los je dat op?
    De plotter gebruikt één verticale as: het verschil van 3 °C valt weg naast een vochtigheid rond 70. Oplossingen: twee keer meten met telkens één grootheid, de vochtigheid delen door 3 (en dat in de naam zetten: vocht/3:), of de temperatuur tonen als afwijking van 26 °C.
29.7

Trace-tabel: wanneer meet het programma?

Toepassen5Analyseren20 min
Twee versies van een meettaak met INTERVAL = 2000. Beide beginnen met vorige = 0. Versie A doet na een meting vorige += INTERVAL;, versie B vorige = nu;. De lus komt langs op deze momenten (ms): 1500, 2150, 3900, 4020, 5990, 6300, 8100.
  1. a
    Vul de trace-tabel in.
    nuA: nu − vorigeAA: vorigeB: nu − vorigeBB: vorige
    15001500nee01500nee0
    21502150ja20002150ja2150
    39001900nee20001750nee2150
    40202020ja40001870nee2150
    59901990nee40003840ja5990
    63002300ja6000310nee5990
    81002100ja80002110ja8100
  2. b
    Hoeveel metingen doet elke versie? Wat gebeurt er met versie B op lange termijn?
    A meet 4 keer (bij 2150, 4020, 6300 en 8100), B maar 3 keer. A blijft op het ritme 2000, 4000, 6000 …: komt een meting te laat, dan komt de volgende sneller. B schuift elke vertraging door: gemiddeld ligt er meer dan 2 s tussen twee metingen, en de achterstand groeit.
  3. c
    Na 8100 blijft de lus hangen tot 15 000 ms, daarna komt ze weer vlot langs (15 000, 15 001, 15 002 …). Wat doen A en B? Welke is beter voor de coulombteller, die bij elke meting 2 s stroom optelt?
    A: bij 15 000 is nu − vorige = 7000, dus meten (vorige 10 000); bij 15 001 nog 5001 (vorige 12 000), bij 15 002 nog 3002 (vorige 14 000), bij 15 003 1003: stop. A meet drie keer kort na elkaar om de gemiste beurten in te halen. B meet één keer (vorige 15 000). Voor de coulombteller is A beter: die telt per meting 2 s stroom op. A heeft na 15 003 ms 7 metingen gedaan, 14 s geteld: klopt. B heeft er 4, dus 8 s geteld terwijl er 15 s voorbij zijn. Voor een taak die iets aanzet (een pompbeurt), wil je dat inhalen juist niet.
29.8

Water geven zonder delay()

Toepassen5Programmeren35 min
In F25 gaf de meetkas water met dit programma (Uno; SENSOR A0, VLOTTER 3, POMP 6, rode led 10, groene led 11; water geven onder 30 %):
void loop() {
  int ruw = analogRead(SENSOR);
  int vocht = map(ruw, DROOG, NAT, 0, 100);
  bool water = (digitalRead(VLOTTER) == LOW);
  if (vocht < DREMPEL && water) {
    digitalWrite(ROOD, HIGH);
    digitalWrite(GROEN, LOW);
    digitalWrite(POMP, HIGH);
    delay(30000);                             // 30 s = 240 mL
    digitalWrite(POMP, LOW);
  } else if (vocht < DREMPEL) {
    digitalWrite(ROOD, HIGH);
    digitalWrite(GROEN, LOW);
  } else {
    digitalWrite(ROOD, LOW);
    digitalWrite(GROEN, HIGH);
  }
  delay(900000UL);                            // 15 min: het water moet eerst inzakken
}
  1. a
    De grond wordt droog vlak nadat de lus aan delay(900000UL) begon. Hoe lang duurt het voor het lampje rood wordt? En wat als de vlotter opengaat terwijl de pomp 30 s draait?
    Tot 15 minuten: pas na de delay wordt de sensor opnieuw gelezen. Gaat de vlotter open tijdens de 30 s pompen, dan merkt het programma niets en de pomp draait droog.
  2. b
    Herschrijf het programma zonder delay(). Gebruik een variabele fase (KLAAR, POMPEN, WACHTEN) en millis(). Het lampje moet meteen reageren en de pomp moet stoppen zodra de vlotter opengaat.

    Getest: het bodemvocht begint op 33 % en daalt 5 procentpunt per uur (het model uit F23); een beurt geeft +16.

    // water_geven_millis.ino - water geven in de meetkas zonder delay() (Arduino Uno, pinnen uit hoofdstuk 25)
    const int SENSOR = A0;
    const int VLOTTER = 3;
    const int POMP = 6;
    const int ROOD = 10;
    const int GROEN = 11;
    const int DROOG = 600;
    const int NAT = 300;
    const int DREMPEL = 30;
    const unsigned long POMP_MS = 30000UL;       // 30 s pompen = 240 mL
    const unsigned long WACHT_MS = 900000UL;     // daarna 15 min wachten tot het water ingezakt is
    
    enum Fase { KLAAR, POMPEN, WACHTEN };
    Fase fase = KLAAR;
    unsigned long startFase = 0;
    
    void setup() {
      pinMode(VLOTTER, INPUT_PULLUP);
      pinMode(POMP, OUTPUT);
      pinMode(ROOD, OUTPUT);
      pinMode(GROEN, OUTPUT);
      Serial.begin(115200);
    }
    
    void loop() {
      unsigned long nu = millis();
      int vocht = map(analogRead(SENSOR), DROOG, NAT, 0, 100);
      bool water = (digitalRead(VLOTTER) == LOW);
    
      if (fase == KLAAR && vocht < DREMPEL && water) {
        fase = POMPEN;                           // droog en er is water: een beurt begint
        startFase = nu;
        digitalWrite(POMP, HIGH);
        Serial.print(nu / 1000);
        Serial.println(" s: pomp aan");
      } else if (fase == POMPEN && (nu - startFase >= POMP_MS || !water)) {
        fase = WACHTEN;                          // 30 s voorbij, of het reservoir is leeg
        startFase = nu;
        digitalWrite(POMP, LOW);
        Serial.print(nu / 1000);
        Serial.println(" s: pomp uit, 15 min wachten");
      } else if (fase == WACHTEN && nu - startFase >= WACHT_MS) {
        fase = KLAAR;
      }
    
      digitalWrite(ROOD, vocht < DREMPEL ? HIGH : LOW);    // het lampje reageert nu meteen
      digitalWrite(GROEN, vocht < DREMPEL ? LOW : HIGH);
    }
    2537 s: pomp aan
    2567 s: pomp uit, 15 min wachten

    In deze drie uur valt het vocht één keer onder 30 %; na de beurt zit het op ruim 45 % en duurt het meer dan drie uur voor het weer zakt. Het lampje wordt in elke ronde bijgewerkt.

  3. c
    Waarom is er nog altijd een wachttijd van 15 minuten na een beurt, ook zonder delay()?
    De wachttijd is geen wachten van het programma, maar een regel van de teelt: het water moet in de potgrond zakken voor de sensor het ziet. Zonder die pauze zou de pomp blijven geven tot de sensor natte grond meldt, en is het bakje overvol. Het programma doet in die 15 minuten wel al het andere werk.
29.9

De CO₂-klep als taak

Toepassen5Programmeren30 min
De CO₂-klep op D10 (6 W, hoofdstuk 14 en 20) moet tijdens de lichtperiode (6 tot 22 uur) om het halfuur 30 s opengaan, maar niet als het dak openstaat. klimaat.ino heeft al secondenVanDeDag(nu), START_UUR en de globale dakOpen.
  1. a
    Schrijf de functie void taakCo2(unsigned long nu) met millis(), en een melding bij elke wissel.

    Als losse test (in klimaat.ino laat je de declaraties weg die er al zijn):

    // co2_taak.ino - de CO2-klep: overdag om het halfuur 30 s open, maar niet als het dak openstaat
    #include "pinnen.h"
    
    const int START_UUR = 6;
    const unsigned long CO2_PERIODE_MS = 30UL * 60 * 1000;
    const unsigned long CO2_OPEN_MS = 30000UL;
    unsigned long startCo2 = 0;
    bool klepOpen = false;
    bool dakOpen = false;                        // in klimaat.ino houdt taakDak dit bij
    
    unsigned long secondenVanDeDag(unsigned long nu) {
      return (START_UUR * 3600UL + nu / 1000) % 86400UL;
    }
    
    void taakCo2(unsigned long nu) {
      if (nu - startCo2 >= CO2_PERIODE_MS) startCo2 += CO2_PERIODE_MS;
      unsigned long uur = secondenVanDeDag(nu) / 3600;
      bool licht = uur >= 6 && uur < 22;                  // CO2 helpt alleen als de planten licht krijgen
      bool open = licht && !dakOpen && (nu - startCo2) < CO2_OPEN_MS;
      if (open != klepOpen) {
        klepOpen = open;
        Serial.print(nu / 1000);
        Serial.println(open ? " s: CO2-klep open" : " s: CO2-klep dicht");
      }
      digitalWrite(CO2_KLEP, open ? HIGH : LOW);
    }
    
    void setup() {
      pinMode(CO2_KLEP, OUTPUT);
      Serial.begin(115200);
    }
    
    void loop() {
      taakCo2(millis());
    }

    De eerste 95 minuten na 06:00 in de simulatie:

    0 s: CO2-klep open
    30 s: CO2-klep dicht
    1800 s: CO2-klep open
    1830 s: CO2-klep dicht
    3600 s: CO2-klep open
    3630 s: CO2-klep dicht
    5400 s: CO2-klep open
    5430 s: CO2-klep dicht
  2. b
    Waarom mag de klep niet open als het dak openstaat?
    CO₂ is zwaarder dan lucht maar mengt snel; door het open dak verdwijnt het meeste naar buiten voor de planten het kunnen opnemen. De klep zou energie en CO₂ verspillen.
  3. c
    Hoeveel minuten per dag staat de klep open, en hoeveel energie kost dat? Vergelijk met het budget van hoofdstuk 20 (15 minuten, 1,5 Wh).
    16 lichturen × 2 keer × 30 s = 32 × 0,5 min = 16 min; . Dat is net iets meer dan het budget van 15 minuten en 1,5 Wh.
29.10

Debuggen: altijd 25,00 °C

Toepassen5Programmeren25 min
Dit programma meet de temperatuur van het reservoir met de NTC op A0 (hoofdstuk 22). De monitor toont 25.00, 25.00, 25.00, en als het water opwarmt ineens -273.15.
// ntc_fout.ino - de temperatuur van het reservoir: waarom altijd 25.00?
#include "pinnen.h"

float temperatuur() {
  int n = analogRead(NTC);
  long r = 10000L * (1024 - n) / n;              // weerstand van de NTC (ohm)
  float invT = 1.0 / 298.15 + log(r / 10000) / 3950;
  return 1.0 / invT - 273.15;
}

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

void loop() {
  Serial.println(temperatuur());
  delay(2000);
}
  1. a
    Welke tussenresultaten laat je afdrukken om de fout te vinden? Schrijf de extra Serial-regels.

    Druk n, r, r / 10000 en invT af:

    float temperatuur() {
      int n = analogRead(NTC);
      Serial.print("n = ");
      Serial.print(n);
      long r = 10000L * (1024 - n) / n;
      Serial.print("   r = ");
      Serial.print(r);
      Serial.print("   r / 10000 = ");
      Serial.print(r / 10000);
      float invT = 1.0 / 298.15 + log(r / 10000) / 3950;
      Serial.print("   invT = ");
      Serial.println(invT, 6);
      return 1.0 / invT - 273.15;
    }

    Uitvoer bij 18, 20, 25 en 29 °C:

    n = 431   r = 13758   r / 10000 = 1   invT = 0.003354
    25.00
    n = 454   r = 12555   r / 10000 = 1   invT = 0.003354
    25.00
    n = 512   r = 10000   r / 10000 = 1   invT = 0.003354
    25.00
    n = 560   r = 8285   r / 10000 = 0   invT = inf
    -273.15
  2. b
    Wat is de fout? Verklaar zowel de 25.00 als de −273.15.
    r / 10000 is een gehele deling (r is een long, 10000 een int). Tussen 10 000 en 19 999 Ω geeft ze 1, en ln 1 = 0: dan is invT = 1/298,15 en T precies 25,00 °C, wat de weerstand ook is. Boven 25 °C is r kleiner dan 10 000 en wordt het 0; ln 0 is min oneindig, invT ook, en 1 gedeeld door min oneindig is 0: T = 0 − 273,15. (De Arduino drukt min oneindig af als inf, zonder minteken.)
  3. c
    Verbeter de functie.

    Reken in float:

    float temperatuur() {
      int n = analogRead(NTC);
      float r = 10000.0 * (1024 - n) / n;            // 10000.0: de hele berekening in float
      float invT = 1.0 / 298.15 + log(r / 10000.0) / 3950.0;
      return 1.0 / invT - 273.15;
    }

    Uitvoer:

    17.99
    19.97
    25.00
    29.29
29.11

Labo: hoe snel draait loop()?

Toepassen5Labo45 min
Met dit programma tel je hoeveel keer per seconde loop() rondgaat. Je zet telkens één van de regels in commentaar aan.
// lusteller.ino - hoeveel keer per seconde loopt loop()?
unsigned long vorige = 0;
unsigned long rondes = 0;

void setup() {
  Serial.begin(115200);         // proef (c): 9600
}

void loop() {
  rondes++;
  // hier komt wat je wil testen, bv.:
  // analogRead(A1);            // proef (b)
  // Serial.println(123);       // proef (c) en (d)
  // delay(1);                  // proef (e)
  unsigned long nu = millis();
  if (nu - vorige >= 1000) {
    vorige += 1000;
    Serial.print("rondes per seconde: ");
    Serial.println(rondes);
    rondes = 0;
  }
}
  1. a
    Voorspel eerst het aantal rondes per seconde voor (b) met analogRead (±0,11 ms), (c) met Serial.println(123) op 9600 baud, (d) hetzelfde op 115200 baud, (e) met delay(1). Eén teken kost 10 bits.
    (b) hoogstens per seconde. (c) 123 plus een nieuwe regel (\r\n) is 5 tekens of 50 bits: , dus ongeveer 190 rondes per seconde (de eerste 64 tekens gaan meteen in de buffer, daarna wacht print()). (d) : ongeveer 2300. (e) iets minder dan 1000.
  2. b
    Meet (a) zonder extra regel en (b) tot en met (e).
    Typisch: (a) in de orde van honderdduizend per seconde (de precieze waarde hangt af van het bord en de compiler), (b) net onder 9000, (c) rond 190, (d) rond 2300, (e) net onder 1000. De Mega en de Uno geven ongeveer hetzelfde: beide lopen op 16 MHz.
  3. c
    Wat leer je hieruit voor klimaat.ino? Welke regel zou je nooit in elke ronde van loop() zetten?
    Een lege taak kost bijna niets: zeven taken die meestal 'nog niet' zeggen, laten de lus nog tienduizenden keren per seconde lopen. Maar één Serial.println in elke ronde zet de hele module op 190 rondes per seconde (bij 9600 baud), en delay() legt alles stil. Daarom meldt klimaat.ino maar één keer per minuut en alleen bij veranderingen.
Zo wordt dit beoordeeld
VoorspellingBerekend uit de tijd per bewerking, met eenheden; bij Serial het aantal tekens en 10 bits per teken.
MetingenVijf metingen, elk een paar seconden stabiel afgelezen; baudrate van de monitor aangepast bij (c).
VergelijkingVerschil tussen voorspeld en gemeten verklaard (de lus zelf kost ook tijd, de buffer van Serial).
BesluitSerial in elke ronde en delay() vertragen de hele lus; analogRead is snel genoeg voor tientallen metingen per taak.
29.12

De overloop narekenen

Toepassen6Analyseren25 min
Een unsigned long rekent modulo : een negatief resultaat krijgt er bij. Het interval is 2000 ms.
  1. a
    Bereken nu - vorige zoals de Mega het doet, en zeg of de taak loopt.
    (a) 2000: loopt. (b) : loopt, op het juiste moment. (c) : nog niet. (d) 1000: nog niet.
  2. b
    Een leerling schrijft unsigned long volgende = vorige + 2000; en if (millis() > volgende). Wat gebeurt er in geval (d)?
    volgende = 4 294 968 000 loopt over en wordt 704. In geval (d) is millis() = 4 294 967 000 groter dan 704: de taak loopt 1 s te vroeg, en daarna bij elke ronde, tot millis() zelf overloopt.
  3. c
    Je wil testen of je programma de overloop overleeft, zonder 49,7 dagen te wachten. Hoe pak je dat aan?
    Laat het programma rekenen met een eigen klok die bijna vol is: bv. unsigned long nu = millis() + 4294960000UL;. Die klok loopt na 7,3 s over. Werkt alles dan nog (metingen om de 2 s, pompcyclus), dan werkt het ook na 49,7 dagen. Zo is overloop.ino in het handboek getest.
Uitdagen

Uitdagen

transfer, open problemen, leerlijnen combineren
29.13

Debat: mag delay() nog?

Uitdagen5Argumenteren20 min
Een klasgenoot zegt: 'Na dit hoofdstuk mag je delay() nooit meer gebruiken.' Een ander antwoordt: 'Onzin, zelfs de bibliotheek van de SHT31 gebruikt delay(20), en klimaat.ino ook: in batterijstroom() wacht het programma 20,4 ms.'
  1. a
    Geef twee situaties waarin delay() (of een korte wachtlus) geen probleem is, en twee waarin het wel een probleem is. Gebruik voorbeelden uit de klimaatmodule.
    Geen probleem: in setup() (een sensor een halve seconde laten opwarmen), in een testschets die maar één ding doet (batterij_debug.ino), en voor korte wachttijden die kleiner zijn dan wat de andere taken kunnen missen (20 ms voor de SHT31, 20,4 ms voor de stroommeting: de knop wordt 20 ms ontdenderd en een druk duurt langer). Wel een probleem: water_geven.ino met delay(900000UL), de dakmotor die tijdens een delay zijn eindeloop mist, een knop die maar om de 2 s bekeken wordt (proef A).
  2. b
    Formuleer een regel van één zin die beide klasgenoten aanvaarden.
    Bijvoorbeeld: 'In loop() mag een taak nooit langer wachten dan de snelste andere taak kan verdragen; alles wat langer duurt, doe je met millis().'
29.14

Een eigen bibliotheek: sensoren.h en sensoren.cpp

Uitdagen6Programmeren40 min
klimaat.ino wordt lang. De sensorfuncties (gemiddeld, daglichtLux, batterijspanning, batterijstroom) horen in een eigen 'bibliotheek' in de schetsmap, samen met een nieuwe functie reservoirTemperatuur() voor de NTC op A0 (10 kΩ bij 25 °C, B = 3913 K na ijking, hoofdstuk 22).
  1. a
    Schrijf sensoren.h: de declaraties van de vijf functies, met een include-bewaking.
    // sensoren.h - wat de sensorfuncties van de klimaatmodule doen (de declaraties)
    #ifndef SENSOREN_H
    #define SENSOREN_H
    
    int gemiddeld(int pin);          // 16 metingen van een analoge pin, gemiddeld
    float daglichtLux();             // daglicht in lux (LDR op A1)
    float batterijspanning();        // V (deler op A3)
    float batterijstroom();          // A, positief = ontladen (ACS712 op A4)
    float reservoirTemperatuur();    // graden C (NTC op A0)
    
    #endif
  2. b
    Schrijf sensoren.cpp met de definities. Welke twee bestanden moet het zelf met #include invoegen, en waarom?

    Het moet Arduino.h invoegen (voor analogRead, micros, pow, log, NAN: in een .ino doet de IDE dat zelf, in een .cpp niet) en pinnen.h (voor LDR, BATTERIJ …), en voor de zekerheid sensoren.h, zodat de compiler controleert of declaratie en definitie overeenkomen.

    // sensoren.cpp - hoe de sensorfuncties het doen (de definities)
    #include <Arduino.h>
    #include "pinnen.h"
    #include "sensoren.h"
    
    int gemiddeld(int pin) {
      long som = 0;
      for (int i = 0; i < 16; i++) som += analogRead(pin);
      return som / 16;
    }
    
    float daglichtLux() {
      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() {
      return gemiddeld(BATTERIJ) * 5.0 / 1024.0 * (133.0 / 33.0);
    }
    
    float batterijstroom() {
      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;
    }
    
    float reservoirTemperatuur() {
      int n = gemiddeld(NTC);
      if (n < 5 || n > 1018) return NAN;                 // draad los of kortsluiting
      float r = 10000.0 * (1024.0 - n) / n;
      return 1.0 / (1.0 / 298.15 + log(r / 10000.0) / 3913.0) - 273.15;   // B na ijking (hoofdstuk 22)
    }

    Een testschets:

    // sensoren_test.ino - de eigen bibliotheek gebruiken
    #include "pinnen.h"
    #include "sensoren.h"
    
    void setup() {
      Serial.begin(115200);
      Serial.print("reservoir ");
      Serial.print(reservoirTemperatuur(), 1);
      Serial.print(" C, daglicht ");
      Serial.print(daglichtLux(), 0);
      Serial.print(" lx, batterij ");
      Serial.print(batterijspanning(), 2);
      Serial.print(" V, ");
      Serial.print(batterijstroom(), 1);
      Serial.println(" A");
    }
    
    void loop() {
    }
    reservoir 19.9 C, daglicht 4004 lx, batterij 13.19 V, 1.4 A
  3. c
    In pinnen.h staat const int LDR = A1;. Wat zou er gebeuren als daar int LDR = A1; stond, nu pinnen.h in twee .cpp-bestanden terechtkomt?
    Dan maakt elk .cpp-bestand zijn eigen variabele LDR, en de linker, die alle stukken samenvoegt, ziet twee keer dezelfde naam: fout multiple definition of 'LDR'. Een const op het hoogste niveau blijft in C++ binnen zijn eigen bestand, dus twee kopieën storen elkaar niet.
Zo wordt dit beoordeeld
sensoren.hAlleen declaraties met de juiste types; include-bewaking met een unieke naam.
sensoren.cppDefinities identiek aan klimaat.ino; Arduino.h, pinnen.h en sensoren.h ingevoegd; reservoirTemperatuur met foutcontrole.
TestEen testschets die alle vijf functies aanroept en de waarden plausibel toont.
UitlegVerschil tussen declaratie en definitie; waarom const in een headerbestand mag.
29.15

Habitat: een niveaualarm voor het reservoir

Uitdagen6Habitat50 min
Het voedingsreservoir (60 × 40 cm, vol bij 25 cm) heeft onderaan een druksensor MPX5010 op A5 (hoofdstuk 11): met p in kPa en = 5,0 V. Onder 10 cm (24 L) moet de pomp geblokkeerd worden en moet de statusled snel knipperen, met één melding op de monitor. Het alarm gaat pas weg boven 12 cm.
  1. a
    Bereken de ADC-waarde bij 10 cm en bij 12 cm water, en hoeveel millimeter één ADC-stap is (dichtheid 1000 kg/m³).
    10 cm: , , ADC . 12 cm: , , ADC . Eén stap is , of = 10,8 Pa: .
  2. b
    Schrijf float niveauCm() en void taakNiveau(unsigned long nu) (om de 2 s). Welke twee regels van klimaat.ino pas je aan?

    De nieuwe functies:

    float niveauCm() {                                    // MPX5010 onderaan het reservoir (hoofdstuk 11)
      float u = gemiddeld(DRUK) * 5.0 / 1024.0;
      float p = (u / 5.0 - 0.04) / 0.09 * 1000.0;         // overdruk in Pa
      return p / (1000.0 * 9.81) * 100.0;                 // waterhoogte in cm
    }
    void taakNiveau(unsigned long nu) {                   // om de 2 s, met twee grenzen
      static unsigned long vorige = 0;
      if (nu - vorige < MEET_MS) return;
      vorige += MEET_MS;
      niveau = niveauCm();
      if (!niveauLaag && niveau < 10.0) {
        niveauLaag = true;
        printTijd(nu);
        Serial.print("ALARM niveau: ");
        Serial.print(niveau, 2);
        Serial.println(" cm, pomp geblokkeerd");
      } else if (niveauLaag && niveau > 12.0) {           // pas weer goed boven 12 cm
        niveauLaag = false;
        printTijd(nu);
        Serial.print("niveau weer goed: ");
        Serial.print(niveau, 2);
        Serial.println(" cm");
      }
    }

    Aanpassen: in taakPomp bool aan = tijd && water && !niveauLaag;, in taakStatusled bool alarm = !genoegWater || niveauLaag || soc < 10.0;, en taakNiveau(nu); in loop(). Getest met een reservoir dat (versneld) 1 cm per minuut zakt en om 06:05 wordt bijgevuld:

    klimaatmodule v1 (hoofdstuk 29)
    laadtoestand uit het EEPROM: 50.0 %
    06:00:00  pomp aan
    06:01:00  T 21.0 C  RV 55 %  licht 2537 lx  bat 13.05 V 0.0 A  SoC 50.0 %  lampen 195/255
    06:02:00  T 21.0 C  RV 55 %  licht 2537 lx  bat 13.05 V 0.0 A  SoC 50.0 %  lampen 195/255
    06:02:26  ALARM niveau: 9.96 cm, pomp geblokkeerd
    06:02:26  pomp uit
    06:03:00  T 21.0 C  RV 55 %  licht 2537 lx  bat 13.05 V 0.0 A  SoC 50.0 %  lampen 195/255
    06:04:00  T 21.0 C  RV 55 %  licht 2537 lx  bat 13.05 V 0.0 A  SoC 50.0 %  lampen 195/255
    06:05:00  niveau weer goed: 19.91 cm
    06:05:00  pomp aan
    06:05:00  T 21.0 C  RV 55 %  licht 2537 lx  bat 13.05 V 0.0 A  SoC 50.0 %  lampen 195/255
    06:06:00  T 21.0 C  RV 55 %  licht 2537 lx  bat 13.05 V 0.0 A  SoC 50.0 %  lampen 195/255
    06:07:00  T 21.0 C  RV 55 %  licht 2537 lx  bat 13.05 V 0.0 A  SoC 50.0 %  lampen 195/255
  3. c
    Waarom twee grenzen (10 en 12 cm) en niet één?
    Het niveau schommelt: de pomp trekt water weg en het klotst, en de meting heeft ruis van een paar ADC-stappen (1 mm per stap). Met één grens van 10 cm zou het alarm rond die grens voortdurend aan- en uitgaan, en de pomp mee. Twee grenzen (hysterese) geven rust; in hoofdstuk 31 zie je dat principe bij elke aan/uit-regeling.
  4. d
    Hoe test je het alarm zonder het reservoir leeg te laten lopen?
    Hef de druksensor niet uit het water, maar test op drie manieren: (1) tijdelijk de grenzen verhogen (bv. 20 en 22 cm), zodat het alarm bij het huidige niveau al afgaat; (2) de luchtbuis van de MPX5010 even uit het water trekken (druk 0: het niveau zakt naar 0 cm); (3) de formule eerst testen met vaste ADC-waarden (131, 149) in plaats van analogRead.
Zo wordt dit beoordeeld
BerekeningDruk, spanning en ADC-waarde juist bij 10 en 12 cm; resolutie in mm per stap.
CodeniveauCm met de formule van de datasheet; taakNiveau met millis(), twee grenzen, één melding per wissel; pomp en statusled aangepast.
TestEen testplan zonder het reservoir te legen, en controle in de seriële monitor.
HabitatIn het portfolio: de alarmgrens, de nieuwe taak en het testresultaat.
29.16

Het tijdbudget van loop()

Uitdagen7Ontwerpen40 min
De meettaak van klimaat.ino doet elke 2 s: een SHT31-meting (readBoth: 20 ms wachten plus ±0,9 ms I²C-verkeer), 100 metingen van de stroom met 204 µs tussen twee metingen, en twee keer 16 gewone ADC-metingen van 0,112 ms. Eens per minuut stuurt taakMelden een regel van ongeveer 110 tekens op 115 200 baud; de zendbuffer van Serial is 64 tekens groot.
  1. a
    Hoe lang houdt taakMeten de lus bezig? Vergelijk met de 44,8 ms uit de simulatie.
    SHT31: . Stroom: de laatste meting begint na en duurt 0,11 ms: . ADC: . Samen ongeveer , precies wat de simulatie gaf.
  2. b
    Hoe lang duurt het versturen van de statusregel, en hoe lang blijft print() daarvan wachten?
    110 tekens × 10 bits / 115 200 bit/s = 9,5 ms. De eerste 64 tekens gaan meteen in de buffer; voor de laatste 46 moet print() wachten tot er plaats vrijkomt: ongeveer 4,0 ms.
  3. c
    Wat is in het slechtste geval de reactietijd op de knop (ontdenderen 20 ms)? Hoe ver schuift het dak bij 0,10 m/s voor een eindeloopschakelaar gezien wordt? En bij de kruipsnelheid van 30 %?
    Knop: in het slechtste geval begint taakMeten net als je drukt; de lus ziet de druk pas na 44,8 ms, en na 20 ms ontdenderen reageert het dak: ongeveer 65 ms, ruim onder wat je merkt. Dak: ; bij 30 % snelheid 1,3 mm. De eindeloopschakelaar heeft een paar millimeter veerweg, dus dat kan, maar het is geen ruime marge.
  4. d
    Ontwerp een verbetering waardoor de langste ronde onder 25 ms zakt, zonder minder te meten. Beschrijf de nieuwe taakverdeling.
    Bijvoorbeeld drie fasen: fase 1 stuurt het meetcommando naar de SHT31 (0,3 ms) en leest de gewone ADC-waarden; fase 2 (minstens 15 ms later, in een volgende ronde) leest de zes bytes van de SHT31; de stroommeting wordt 100 losse metingen die telkens één per ronde gebeuren, zodra er 204 µs voorbij zijn. De langste ronde wordt dan enkele ms. Nadeel: de code wordt ingewikkelder (hoe je de SHT31 zonder bibliotheek aanspreekt, zie je in hoofdstuk 30), en de waarden van één meting komen van iets verschillende momenten. Een tweede weg: een RC-filter voor A4 (hoofdstuk 25), zodat één ADC-meting volstaat.
Zo wordt dit beoordeeld
TijdbudgetElke bijdrage apart berekend en opgeteld; vergelijking met de simulatie.
GevolgenReactietijd knop en overshoot van het dak juist berekend, met eenheden.
OntwerpEen concrete opsplitsing in fasen met millis(), die de werking niet verandert.
AfwegingVoor- en nadelen genoemd (complexere code, gegevens van verschillende momenten).