Intermediate · Oplossingen · 29. Microcontrollers programmeren in C++ StemExpert · Brecht Corbeel · schoolium.me
SE
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.
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).
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.
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.
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).
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.
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
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)constintPOMP=9;constintVLOTTER=2;boolaan=false;unsignedlongvorige=0;voidsetup(){pinMode(VLOTTER,INPUT_PULLUP);Serial.begin(9600);}voidloop(){unsignedlongnu=millis()if(nu-vorige>=5000){vorige+=5000;aan=!aan;}if(digitalRead(VLOTTER)=HIGH){aan=false;}digitalwrite(POMP,aan?HIGH:LOW);Serial.println(aan);}
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.
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.)
c
Schrijf de verbeterde versie. Zorg er ook voor dat de monitor niet overspoeld wordt.
Verbeterd en getest:
// pomp_test.ino - verbeterdconstintPOMP=9;constintVLOTTER=2;boolaan=false;unsignedlongvorige=0;voidsetup(){pinMode(POMP,OUTPUT);// fout 4: zonder pinMode geen stevige uitgangpinMode(VLOTTER,INPUT_PULLUP);Serial.begin(115200);// fout 5: dezelfde snelheid als de seriële monitor}voidloop(){unsignedlongnu=millis();// fout 1: puntkommaif(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 toeaan=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.
a
Hoeveel ms zijn 10 minuten, 1 uur en 1 dag? Past elk getal in een int? In een unsigned long?
10⋅60⋅1000=600000ms, 3600000ms, 86400000ms. Geen enkele past in een int (hoogstens 32 767); alle drie passen in een unsigned long (tot 4 294 967 295).
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(): 232ms=4294967296ms=49,7 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.
c
Hoeveel uurcycli van de pomp passen in één periode van millis()?
4294967296/3600000≈1193 cycli.
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: 100000h=100000/(24⋅365,25)≈11,4 jaar. Om de 2 s: 200000s≈2,3 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).
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_SHT31sht=Adafruit_SHT31();unsignedlongvorige=0;voidsetup(){Serial.begin(115200);sht.begin(0x44);}voidloop(){unsignedlongnu=millis();if(nu-vorige>=1000){vorige+=1000;floatt,rv;sht.readBoth(&t,&rv);Serial.print("temperatuur:");Serial.print(t,1);Serial.print(",vocht:");Serial.print(rv,1);Serial.println(",grens_26C:26");}}
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.
a
Vul de trace-tabel in.
nu
A: nu − vorige
A
A: vorige
B: nu − vorige
B
B: vorige
1500
1500
nee
0
1500
nee
0
2150
2150
ja
2000
2150
ja
2150
3900
1900
nee
2000
1750
nee
2150
4020
2020
ja
4000
1870
nee
2150
5990
1990
nee
4000
3840
ja
5990
6300
2300
ja
6000
310
nee
5990
8100
2100
ja
8000
2110
ja
8100
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.
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 %):
voidloop(){intruw=analogRead(SENSOR);intvocht=map(ruw,DROOG,NAT,0,100);boolwater=(digitalRead(VLOTTER)==LOW);if(vocht<DREMPEL&&water){digitalWrite(ROOD,HIGH);digitalWrite(GROEN,LOW);digitalWrite(POMP,HIGH);delay(30000);// 30 s = 240 mLdigitalWrite(POMP,LOW);}elseif(vocht<DREMPEL){digitalWrite(ROOD,HIGH);digitalWrite(GROEN,LOW);}else{digitalWrite(ROOD,LOW);digitalWrite(GROEN,HIGH);}delay(900000UL);// 15 min: het water moet eerst inzakken}
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.
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)constintSENSOR=A0;constintVLOTTER=3;constintPOMP=6;constintROOD=10;constintGROEN=11;constintDROOG=600;constintNAT=300;constintDREMPEL=30;constunsignedlongPOMP_MS=30000UL;// 30 s pompen = 240 mLconstunsignedlongWACHT_MS=900000UL;// daarna 15 min wachten tot het water ingezakt isenumFase{KLAAR,POMPEN,WACHTEN};Fasefase=KLAAR;unsignedlongstartFase=0;voidsetup(){pinMode(VLOTTER,INPUT_PULLUP);pinMode(POMP,OUTPUT);pinMode(ROOD,OUTPUT);pinMode(GROEN,OUTPUT);Serial.begin(115200);}voidloop(){unsignedlongnu=millis();intvocht=map(analogRead(SENSOR),DROOG,NAT,0,100);boolwater=(digitalRead(VLOTTER)==LOW);if(fase==KLAAR&&vocht<DREMPEL&&water){fase=POMPEN;// droog en er is water: een beurt begintstartFase=nu;digitalWrite(POMP,HIGH);Serial.print(nu/1000);Serial.println(" s: pomp aan");}elseif(fase==POMPEN&&(nu-startFase>=POMP_MS||!water)){fase=WACHTEN;// 30 s voorbij, of het reservoir is leegstartFase=nu;digitalWrite(POMP,LOW);Serial.print(nu/1000);Serial.println(" s: pomp uit, 15 min wachten");}elseif(fase==WACHTEN&&nu-startFase>=WACHT_MS){fase=KLAAR;}digitalWrite(ROOD,vocht<DREMPEL?HIGH:LOW);// het lampje reageert nu meteendigitalWrite(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.
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.
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"constintSTART_UUR=6;constunsignedlongCO2_PERIODE_MS=30UL*60*1000;constunsignedlongCO2_OPEN_MS=30000UL;unsignedlongstartCo2=0;boolklepOpen=false;booldakOpen=false;// in klimaat.ino houdt taakDak dit bijunsignedlongsecondenVanDeDag(unsignedlongnu){return(START_UUR*3600UL+nu/1000)%86400UL;}voidtaakCo2(unsignedlongnu){if(nu-startCo2>=CO2_PERIODE_MS)startCo2+=CO2_PERIODE_MS;unsignedlonguur=secondenVanDeDag(nu)/3600;boollicht=uur>=6&&uur<22;// CO2 helpt alleen als de planten licht krijgenboolopen=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);}voidsetup(){pinMode(CO2_KLEP,OUTPUT);Serial.begin(115200);}voidloop(){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
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.
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; 6W⋅16/60h=1,6Wh. 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"floattemperatuur(){intn=analogRead(NTC);longr=10000L*(1024-n)/n;// weerstand van de NTC (ohm)floatinvT=1.0/298.15+log(r/10000)/3950;return1.0/invT-273.15;}voidsetup(){Serial.begin(115200);}voidloop(){Serial.println(temperatuur());delay(2000);}
a
Welke tussenresultaten laat je afdrukken om de fout te vinden? Schrijf de extra Serial-regels.
Druk n, r, r / 10000 en invT af:
floattemperatuur(){intn=analogRead(NTC);Serial.print("n = ");Serial.print(n);longr=10000L*(1024-n)/n;Serial.print(" r = ");Serial.print(r);Serial.print(" r / 10000 = ");Serial.print(r/10000);floatinvT=1.0/298.15+log(r/10000)/3950;Serial.print(" invT = ");Serial.println(invT,6);return1.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
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.)
c
Verbeter de functie.
Reken in float:
floattemperatuur(){intn=analogRead(NTC);floatr=10000.0*(1024-n)/n;// 10000.0: de hele berekening in floatfloatinvT=1.0/298.15+log(r/10000.0)/3950.0;return1.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()?unsignedlongvorige=0;unsignedlongrondes=0;voidsetup(){Serial.begin(115200);// proef (c): 9600}voidloop(){rondes++;// hier komt wat je wil testen, bv.:// analogRead(A1); // proef (b)// Serial.println(123); // proef (c) en (d)// delay(1); // proef (e)unsignedlongnu=millis();if(nu-vorige>=1000){vorige+=1000;Serial.print("rondes per seconde: ");Serial.println(rondes);rondes=0;}}
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 1/0,11ms≈9000 per seconde. (c) 123 plus een nieuwe regel (\r\n) is 5 tekens of 50 bits: 50/9600=5,2ms, dus ongeveer 190 rondes per seconde (de eerste 64 tekens gaan meteen in de buffer, daarna wacht print()). (d) 50/115200=0,43ms: ongeveer 2300. (e) iets minder dan 1000.
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.
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
Voorspelling
Berekend uit de tijd per bewerking, met eenheden; bij Serial het aantal tekens en 10 bits per teken.
Metingen
Vijf metingen, elk een paar seconden stabiel afgelezen; baudrate van de monitor aangepast bij (c).
Vergelijking
Verschil tussen voorspeld en gemeten verklaard (de lus zelf kost ook tijd, de buffer van Serial).
Besluit
Serial 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 232=4294967296: een negatief resultaat krijgt er 232 bij. Het interval is 2000 ms.
a
Bereken nu - vorige zoals de Mega het doet, en zeg of de taak loopt.
(a) 2000: loopt. (b) 704−4294966000+4294967296=2000: loopt, op het juiste moment. (c) 100−4294966000+4294967296=1396: nog niet. (d) 1000: nog niet.
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.
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.'
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).
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).
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_Hintgemiddeld(intpin);// 16 metingen van een analoge pin, gemiddeldfloatdaglichtLux();// daglicht in lux (LDR op A1)floatbatterijspanning();// V (deler op A3)floatbatterijstroom();// A, positief = ontladen (ACS712 op A4)floatreservoirTemperatuur();// graden C (NTC op A0)#endif
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"intgemiddeld(intpin){longsom=0;for(inti=0;i<16;i++)som+=analogRead(pin);returnsom/16;}floatdaglichtLux(){intn=gemiddeld(LDR);if(n<1)n=1;floatr=1000.0*(1024.0-n)/n;return10.0*pow(r/15000.0,-1.0/0.7)/0.10;}floatbatterijspanning(){returngemiddeld(BATTERIJ)*5.0/1024.0*(133.0/33.0);}floatbatterijstroom(){longsom=0;unsignedlongt0=micros();for(inti=0;i<100;i++){while(micros()-t0<i*204UL){}som+=analogRead(STROOM);}return(som/100.0*5.0/1024.0-2.50)/0.100;}floatreservoirTemperatuur(){intn=gemiddeld(NTC);if(n<5||n>1018)returnNAN;// draad los of kortsluitingfloatr=10000.0*(1024.0-n)/n;return1.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"voidsetup(){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");}voidloop(){}
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.h
Alleen declaraties met de juiste types; include-bewaking met een unieke naam.
sensoren.cpp
Definities identiek aan klimaat.ino; Arduino.h, pinnen.h en sensoren.h ingevoegd; reservoirTemperatuur met foutcontrole.
Test
Een testschets die alle vijf functies aanroept en de waarden plausibel toont.
Uitleg
Verschil 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): U=VS⋅(0,09⋅p+0,04) met p in kPa en VS = 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.
a
Bereken de ADC-waarde bij 10 cm en bij 12 cm water, en hoeveel millimeter één ADC-stap is (dichtheid 1000 kg/m³).
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:
floatniveauCm(){// MPX5010 onderaan het reservoir (hoofdstuk 11)floatu=gemiddeld(DRUK)*5.0/1024.0;floatp=(u/5.0-0.04)/0.09*1000.0;// overdruk in Pareturnp/(1000.0*9.81)*100.0;// waterhoogte in cm}
voidtaakNiveau(unsignedlongnu){// om de 2 s, met twee grenzenstaticunsignedlongvorige=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");}elseif(niveauLaag&&niveau>12.0){// pas weer goed boven 12 cmniveauLaag=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
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.
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
Berekening
Druk, spanning en ADC-waarde juist bij 10 en 12 cm; resolutie in mm per stap.
Code
niveauCm met de formule van de datasheet; taakNiveau met millis(), twee grenzen, één melding per wissel; pomp en statusled aangepast.
Test
Een testplan zonder het reservoir te legen, en controle in de seriële monitor.
Habitat
In 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.
a
Hoe lang houdt taakMeten de lus bezig? Vergelijk met de 44,8 ms uit de simulatie.
SHT31: 20+0,9=20,9ms. Stroom: de laatste meting begint na 99⋅0,204=20,2ms en duurt 0,11 ms: 20,3ms. ADC: 32⋅0,112=3,6ms. Samen ongeveer 44,8ms, precies wat de simulatie gaf.
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.
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: 0,10m/s⋅0,0448s=4,5mm; bij 30 % snelheid 1,3 mm. De eindeloopschakelaar heeft een paar millimeter veerweg, dus dat kan, maar het is geen ruime marge.
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
Tijdbudget
Elke bijdrage apart berekend en opgeteld; vergelijking met de simulatie.
Gevolgen
Reactietijd knop en overshoot van het dak juist berekend, met eenheden.
Ontwerp
Een concrete opsplitsing in fasen met millis(), die de werking niet verandert.
Afweging
Voor- en nadelen genoemd (complexere code, gegevens van verschillende momenten).