← Back to CoursesStemExpert
StemExpert
Intermediate · Handboek · 32. Toestandsmachines
StemExpert · Brecht Corbeel · schoolium.me
StemExpert
I32. Toestandsmachines
IIntermediate · deel I8 · Regelen en robotica

Handboek · hoofdstuk 32Toestandsmachines

Een programma dat onthoudt waar het is: van verkeerslicht tot de modi van de klimaatmodule

Een druk op dezelfde knop opent het kasdak, sluit het, of stopt het, naargelang wat het dak op dat ogenblik doet. Wie dat met losse als-dan-regels programmeert, verdwaalt snel. Een toestandsmachine ordent het: in elke toestand staat wat elke gebeurtenis doet. Je leert toestandsdiagrammen lezen en tekenen, programmeert ze met switch-case, en ontwerpt wat er gebeurt als er iets misloopt: de veilige toestand, time-outs en de waakhond. De klimaatmodule krijgt haar modi: dag, nacht, alarm en onderhoud.

6× uitleg2× uitgewerkt voorbeeld1× ontleed1× naslag1× verhaal1× het geheel5× code16 opdrachten in het werkboek± 8 lestijden
Een knop, een paar lampen en een klok: het klassieke voorbeeld van een toestandsmachine.
Een knop, een paar lampen en een klok: het klassieke voorbeeld van een toestandsmachine.
Na dit hoofdstuk
Uitleg · 32.1

Een programma dat onthoudt waar het is

1/17

Wat je al weet: in Fundamental reageerde een blokprogramma op gebeurtenissen ('wanneer op de knop gedrukt', hoofdstuk 23) en liet je een verkeerslicht branden met delay() (hoofdstuk 24). In hoofdstuk 26 en 29 kreeg het kasdak een variabele met drie waarden: STIL, SLUITEN en OPENEN. Dat was al een kleine toestandsmachine.

Een toestandsmachine bestaat uit vier soorten dingen:

  • Toestanden: situaties waarin het systeem kan blijven, met een vast gedrag. Het dak is DICHT, OPENEN, OPEN, SLUITEN, GESTOPT of DAK_FOUT: altijd precies één daarvan.
  • Gebeurtenissen: iets dat gebeurt. Een knopdruk, een eindeloopschakelaar die schakelt, een tijd die om is, een temperatuur die een grens passeert.
  • Overgangen: van de ene toestand naar een andere, als een bepaalde gebeurtenis komt.
  • Acties: wat het systeem doet bij een overgang of bij het binnenkomen van een toestand: de motor aanzetten, een lamp doven, een melding sturen.

Waarom zo? Omdat dezelfde gebeurtenis iets anders betekent in een andere toestand. Een druk op S1 opent het dak als het dicht is, sluit het als het open is, en stopt het als het beweegt. Met losse als-dan-regels verlies je dat snel uit het oog; met een toestandsmachine staat per toestand wat elke gebeurtenis doet, en wat ze niet doet.

Zes toestanden in plaats van drie

In hoofdstuk 29 onthield het programma de toestand in twee variabelen: dak (STIL, SLUITEN, OPENEN) en dakOpen (ja of nee). Twee variabelen maken samen zes combinaties, waarvan sommige geen zin hebben. Hier zit alles in één variabele: DICHT, OPEN en GESTOPT zijn drie verschillende soorten 'stil', en DAK_FOUT is nieuw.

DICHTOPENENmotor: openenOPENGESTOPTmotor stilSLUITENmotor: dichtDAK_FOUTmotor stilwens openeindeloop openwens dichteindeloop dichtknop[t > 12 s]knopknop / wens = dicht[t > 12 s]herstel
Het kasdak: 'wens open' en 'wens dicht' komen van de knop of van de temperatuurregeling (hoofdstuk 31).
Uitleg · 32.2

Het toestandsdiagram

2/17

Een toestandsdiagram tekent een toestandsmachine. De afspraken:

  • Een toestand is een afgeronde rechthoek met een naam die een situatie beschrijft: DICHT, WACHTEN, OPENEN. Een actie die gebeurt telkens je de toestand binnenkomt, schrijf je erin: 'bij binnenkomen: motor aan'.
  • Het beginpunt, een zwart bolletje, wijst naar de toestand waarin de machine start.
  • Een overgang is een pijl met het label gebeurtenis [voorwaarde] / actie. Elk deel mag ontbreken. 'eindeloop open' is alleen een gebeurtenis; '[t > 12 s]' is alleen een voorwaarde op de tijd in de toestand; 'knop / wens = dicht' is een gebeurtenis met een actie.
  • Een zelflus is een overgang naar dezelfde toestand: er gebeurt iets, maar de toestand blijft.
  • Een gebeurtenis die in een toestand geen pijl heeft, wordt daar genegeerd. Dat moet een bewuste keuze zijn, geen vergetelheid.

Voor dat laatste is de toestandstabel handig: de toestanden als rijen, de gebeurtenissen als kolommen. Elk vakje zegt wat er gebeurt: een nieuwe toestand, een actie, of niets (–). Een diagram toont het verhaal, een tabel dwingt je om over elke combinatie na te denken. Die van het dak:

toestandknopwens openwens dichteindeloop openeindeloop dichtt > 12 s
DICHT– (wens = open)OPENEN––––
OPENENGESTOPT––OPEN–DAK_FOUT
OPEN– (wens = dicht)–SLUITEN–––
SLUITENGESTOPT–––DICHTDAK_FOUT
GESTOPTSLUITEN (wens = dicht)–––––
DAK_FOUT––––––

DAK_FOUT verlaat je alleen met dakHerstel(), na een bevestigd alarm.

ABbij binnenkomen: motor aangebeurtenis [voorwaarde] / actiebegintoestandovergangtoestand met een actie bij binnenkomenVoorbeeld: knop [dak dicht] / wens = openals de knop ingedrukt wordt, en alleen als het dak dicht is: onthoud dat het dak open moet.
Alles wat je nodig hebt om een toestandsdiagram te lezen.
Uitgewerkt voorbeeld · 32.3

Een voetgangersoversteek ontwerpen

3/17

Ontwerp de toestandsmachine van een voetgangersoversteek met drukknop. De auto's hebben groen tot er een voetganger drukt, maar altijd minstens 20 s. Na een druk: 3 s oranje, 2 s alles rood (het kruispunt leeg laten rijden), 8 s groen voor de voetgangers, 5 s alles rood (de laatste voetgangers halen de overkant), en dan weer groen voor de auto's. Een lampje 'wacht' toont dat de aanvraag genoteerd is. Een druk terwijl de voetgangers al groen hebben, telt niet.

AUTO_GROENauto groen, voetg. roodAUTO_ORANJEauto oranjeALLES_ROOD_1auto roodVOETGANGER_GROENaanvraag = neeALLES_ROOD_2voetg. roodknop / aanvraag = ja[aanvraag en t ≥ 20 s][t ≥ 3 s][t ≥ 2 s][t ≥ 8 s][t ≥ 5 s]
De knop kan in elke toestand behalve VOETGANGER_GROEN een aanvraag noteren; de zelflus staat hier bij AUTO_GROEN.
Gegeven
  • tijden: groen minstens 20 s, oranje 3 s, alles rood 2 s, voetgangers 8 s, alles rood 5 s
  • een drukknop en een lampje 'wacht'
Gevraagd
  • toestanden, overgangen en acties
  • een toestandsdiagram
Oplossing
  1. 1
    Toestanden zijn situaties met een ander gedrag. Hier: de lampen. AUTO_GROEN, AUTO_ORANJE, ALLES_ROOD_1, VOETGANGER_GROEN en ALLES_ROOD_2. Alles rood komt twee keer voor met dezelfde lampen, maar wat erna komt, verschilt: dus twee toestanden.
  2. 2
    Gebeurtenissen: de knop, en de tijd in een toestand. De tijd schrijf je als voorwaarde: [t ≥ 3 s].
  3. 3
    Een druk moet onthouden worden, ook als de auto's nog 15 s groen hebben. Daarvoor dient een extra variabele aanvraag (ja of nee): een zelflus 'knop / aanvraag = ja'. In VOETGANGER_GROEN heeft de knop geen pijl: genegeerd.
  4. 4
    De overgang uit AUTO_GROEN heeft een samengestelde voorwaarde: [aanvraag en t ≥ 20 s]. Alle andere overgangen hangen alleen van de tijd af.
  5. 5
    Acties bij binnenkomen: de lampen van elke toestand. In VOETGANGER_GROEN ook: aanvraag = nee, lampje 'wacht' uit.
Antwoord
Vijf toestanden in een kring, met één zelflus voor de knop (zie het diagram).
Klopt dit? Loop elke toestand na: wat als de knop komt? In AUTO_GROEN, AUTO_ORANJE en de twee rode toestanden wordt hij onthouden, in VOETGANGER_GROEN genegeerd. De langste wachttijd voor een voetganger: net na het begin van AUTO_GROEN drukken geeft 20 + 3 + 2 = 25 s tot groen.
Code · 32.4

Het voetgangerslicht in C++

4/17

Het diagram wordt code in vier stappen: een enum met de toestanden, één variabele toestand, een functie naar() die de overgang doet en de acties bij binnenkomen uitvoert, en een switch in loop() die per toestand de gebeurtenissen bekijkt. Nergens delay(): de tijd in een toestand is nu - sinds (hoofdstuk 29).

C++ (Arduino)verkeerslicht.ino81 regelsDownload
// verkeerslicht.ino - een voetgangersoversteek met drukknop als toestandsmachine (hoofdstuk 32)// Auto's: rood D4, oranje D5, groen D6. Voetgangers: rood D7, groen D8. Lampje 'wacht' D9. Knop D2 naar GND. const int KNOP = 2;const int A_ROOD = 4, A_ORANJE = 5, A_GROEN = 6, V_ROOD = 7, V_GROEN = 8, WACHT = 9; const unsigned long GROEN_MIN_MS = 20000;   // auto's hebben minstens 20 s groenconst unsigned long ORANJE_MS = 3000;const unsigned long ONTRUIM_MS = 2000;      // alles rood: het kruispunt leeg laten rijdenconst unsigned long OVERSTEEK_MS = 8000;    // voetgangers groenconst unsigned long NALOOP_MS = 5000;       // alles rood: de laatste voetgangers halen de overkant enum Toestand { AUTO_GROEN, AUTO_ORANJE, ALLES_ROOD_1, VOETGANGER_GROEN, ALLES_ROOD_2 };const char* NAAM[] = {"AUTO_GROEN", "AUTO_ORANJE", "ALLES_ROOD_1", "VOETGANGER_GROEN", "ALLES_ROOD_2"}; Toestand toestand = AUTO_GROEN;unsigned long sinds = 0;                    // wanneer de huidige toestand begonbool aanvraag = false;                      // heeft een voetganger op de knop gedrukt? // bij binnenkomen van een toestand: de lampen die erbij horenvoid lampen(bool ar, bool ao, bool ag, bool vr, bool vg) {  digitalWrite(A_ROOD, ar);  digitalWrite(A_ORANJE, ao);  digitalWrite(A_GROEN, ag);  digitalWrite(V_ROOD, vr);  digitalWrite(V_GROEN, vg);} void naar(Toestand nieuw, unsigned long nu) {  toestand = nieuw;  sinds = nu;  switch (nieuw) {    case AUTO_GROEN:       lampen(0, 0, 1, 1, 0); break;    case AUTO_ORANJE:      lampen(0, 1, 0, 1, 0); break;    case ALLES_ROOD_1:     lampen(1, 0, 0, 1, 0); break;    case VOETGANGER_GROEN: lampen(1, 0, 0, 0, 1);                           aanvraag = false;          // de aanvraag is beantwoord                           digitalWrite(WACHT, LOW);                           break;    case ALLES_ROOD_2:     lampen(1, 0, 0, 1, 0); break;  }  Serial.print(nu / 1000.0, 1);  Serial.print(" s  ");  Serial.println(NAAM[nieuw]);} void setup() {  pinMode(KNOP, INPUT_PULLUP);  for (int p = A_ROOD; p <= WACHT; p++) pinMode(p, OUTPUT);  Serial.begin(115200);  naar(AUTO_GROEN, millis());} void loop() {  unsigned long nu = millis();  // gebeurtenis: knop ingedrukt (onthouden, behalve als de voetgangers al groen hebben)  if (digitalRead(KNOP) == LOW && !aanvraag && toestand != VOETGANGER_GROEN) {    aanvraag = true;    digitalWrite(WACHT, HIGH);    Serial.print(nu / 1000.0, 1);    Serial.println(" s  knop: aanvraag");  }  unsigned long t = nu - sinds;               // hoe lang zijn we al in deze toestand?  switch (toestand) {    case AUTO_GROEN:      if (aanvraag && t >= GROEN_MIN_MS) naar(AUTO_ORANJE, nu);      break;    case AUTO_ORANJE:      if (t >= ORANJE_MS) naar(ALLES_ROOD_1, nu);      break;    case ALLES_ROOD_1:      if (t >= ONTRUIM_MS) naar(VOETGANGER_GROEN, nu);      break;    case VOETGANGER_GROEN:      if (t >= OVERSTEEK_MS) naar(ALLES_ROOD_2, nu);      break;    case ALLES_ROOD_2:      if (t >= NALOOP_MS) naar(AUTO_GROEN, nu);      break;  }}
knoplampje 'wacht'auto groenauto oranjeauto roodvoetg. groenvoetg. rood0102030405060708090t (s)
De pinnen die het programma in de test schakelde: nooit twee groene lampen tegelijk.

Getest met drie knopdrukken: op 5 s, op 29 s en op 41 s. De seriële monitor:

0.0 s  AUTO_GROEN
5.0 s  knop: aanvraag
20.0 s  AUTO_ORANJE
23.0 s  ALLES_ROOD_1
25.0 s  VOETGANGER_GROEN
33.0 s  ALLES_ROOD_2
38.0 s  AUTO_GROEN
41.0 s  knop: aanvraag
58.0 s  AUTO_ORANJE
61.0 s  ALLES_ROOD_1
63.0 s  VOETGANGER_GROEN
71.0 s  ALLES_ROOD_2
76.0 s  AUTO_GROEN

De druk op 29 s valt in VOETGANGER_GROEN en wordt genegeerd. De druk op 41 s komt 3 s nadat de auto's groen kregen: de aanvraag wacht tot de 20 s om zijn (58 s). De knop hoeft hier niet ontdenderd te worden: een tweede of derde 'druk' van dender zet aanvraag opnieuw op ja, en dat verandert niets.

Uitgewerkt voorbeeld · 32.5

De automatische deur: van diagram naar tabel

5/17

De ingang van het station heeft een automatische schuifdeur. Een radar ziet beweging, twee eindeloopschakelaars melden 'open' en 'dicht', een fotocel tussen de deurstijlen ziet of er iemand in de opening staat. De deur blijft open zolang er beweging is, en sluit 5 s nadat niemand meer beweegt. Stel de toestandstabel op en zoek de overgangen die over veiligheid gaan.

DICHTOPENENmotor: openOPENtimer startSLUITENmotor: dichtbewegingeindeloop open[5 s niemand]eindeloop dichtbeweging of fotocelbeweging offotocel /timer opnieuw
De pijl van SLUITEN naar OPENEN is de veiligheidsovergang.
Gegeven
  • gebeurtenissen: beweging, eindeloop open, eindeloop dicht, 5 s niemand, fotocel onderbroken
Gevraagd
  • toestanden
  • toestandstabel
  • veiligheid
Oplossing
  1. 1
    Toestanden waarin de deur anders doet: DICHT, OPENEN, OPEN (timer loopt), SLUITEN.
  2. 2
    Vul voor elke toestand elke kolom in. In DICHT doet alleen beweging iets; in OPENEN alleen 'eindeloop open'.
  3. 3
    In OPEN start 'beweging' de timer opnieuw: een zelflus. Ook de fotocel: wie stilstaat in de opening, beweegt misschien niet voor de radar.
  4. 4
    Veiligheid: in SLUITEN zijn 'beweging' en 'fotocel' de belangrijkste vakjes. De deur keert om naar OPENEN: niemand raakt gekneld.
  5. 5
    Lege vakjes nakijken. DICHT + fotocel: negeren kan. Maar OPENEN + 'eindeloop dicht' kan niet gebeuren als alles werkt: dat is een onmogelijke combinatie, en dus een sensorfout. Een goed ontwerp gaat dan naar een toestand FOUT.
Antwoord
toestandbewegingeindeloop openeindeloop dicht5 s niemandfotocel onderbroken
DICHTOPENEN / motor open––––
OPENEN–OPEN / motor stil, timer–––
OPENOPEN / timer opnieuw––SLUITEN / motor dichtOPEN / timer opnieuw
SLUITENOPENEN / motor open–DICHT / motor stil–OPENEN / motor open
Klopt dit? De tabel heeft 4 × 5 = 20 vakjes, waarvan 8 ingevuld; het diagram toont ze met 5 pijlen (twee met twee gebeurtenissen) en een zelflus. Wie alleen tekent, vergeet makkelijk de lege vakjes; de tabel laat ze zien. Opdracht 32.12 maakt van de tabel een programma.
Uitleg · 32.6

De wasmachine: een programma in stappen

6/17

Een wasmachine doorloopt een vaste reeks toestanden. Sommige overgangen komen van een sensor (het niveau van het water, de temperatuur), andere van de tijd. Drie dingen maken dit voorbeeld leerrijk:

  • Een regeling binnen een toestand. In VERWARMEN warmt het water op tot 40 °C, en tijdens het wassen houdt een aan/uit-regeling met hysterese het daar (hoofdstuk 31); in VULLEN staat de klep open tot de niveauschakelaar schakelt. Toestandsmachines en regelingen werken samen: de machine kiest wat er geregeld wordt, de regeling doet het.
  • Time-outs vangen fouten op. Staat de kraan dicht, dan komt 'niveau bereikt' nooit. Zonder time-out wacht de machine eeuwig met een open klep. Met [t > 5 min] gaat ze naar FOUT. Elke toestand die op een sensor wacht, hoort een time-out te hebben.
  • De veilige toestand is niet 'alles uit en alles open'. In FOUT staan klep, verwarming en trommel stil, maar de deur blijft op slot zolang er water in de trommel staat: anders loopt de badkamer onder.

Echte wasmachines spoelen twee of drie keer. Dat teken je niet met drie keer dezelfde toestanden, maar met een teller en een voorwaarde: uit POMPEN naar SPOELEN [spoelbeurt < 2], anders naar CENTRIFUGEREN.

KLAARdeur losVULLENklep openVERWARMEN40 °C (hysterese)WASSENtrommel draaitPOMPENpomp aanSPOELENklep open, draaienCENTRIFUGEREN1200 omw/minFOUTalles uit, deur op slotstartniveau40 °C30 minleeg10 min8 min /deur los[t > 5 min][t > 3 min]
Rechte lijn door het programma, en twee time-outs die naar FOUT leiden.
Code · 32.7

Een toestandsmachine als tabel

7/17

Een toestandsmachine hoeft geen switch te zijn. In Python kan je de toestandstabel letterlijk als woordenboek schrijven (hoofdstuk 28): de sleutel is het paar (toestand, gebeurtenis), de waarde het paar (nieuwe toestand, actie). Het programma zoekt alleen nog op.

Pythonwasmachine.py29 regelsDownload
# wasmachine.py - een toestandsmachine als tabel: (toestand, gebeurtenis) -> (nieuwe toestand, actie)OVERGANGEN = {    ("KLAAR", "start"): ("VULLEN", "deur vergrendelen, waterklep open"),    ("VULLEN", "niveau bereikt"): ("VERWARMEN", "waterklep dicht, verwarming aan"),    ("VULLEN", "5 min voorbij"): ("FOUT", "waterklep dicht: komt er geen water?"),    ("VERWARMEN", "40 graden"): ("WASSEN", "verwarming uit, trommel draait"),    ("WASSEN", "30 min voorbij"): ("POMPEN", "trommel stil, pomp aan"),    ("POMPEN", "leeg"): ("SPOELEN", "pomp uit, waterklep open, trommel draait"),    ("POMPEN", "3 min voorbij"): ("FOUT", "pomp uit: filter verstopt?"),    ("SPOELEN", "10 min voorbij"): ("CENTRIFUGEREN", "waterklep dicht, pomp aan, 1200 omw/min"),    ("CENTRIFUGEREN", "8 min voorbij"): ("KLAAR", "trommel stil, pomp uit, deur ontgrendelen"),}  def verwerk(toestand, gebeurtenis):    """Zoekt de overgang op. Staat de combinatie niet in de tabel, dan gebeurt er niets."""    if (toestand, gebeurtenis) in OVERGANGEN:        nieuw, actie = OVERGANGEN[(toestand, gebeurtenis)]        print(f"{toestand:13} --{gebeurtenis}--> {nieuw:13} {actie}")        return nieuw    print(f"{toestand:13} --{gebeurtenis}--> (genegeerd)")    return toestand  toestand = "KLAAR"for g in ["deur open", "start", "niveau bereikt", "start", "40 graden", "deur open", "30 min voorbij",          "3 min voorbij"]:    toestand = verwerk(toestand, g)print("eindtoestand:", toestand)

Uitvoer:

KLAAR         --deur open--> (genegeerd)
KLAAR         --start--> VULLEN        deur vergrendelen, waterklep open
VULLEN        --niveau bereikt--> VERWARMEN     waterklep dicht, verwarming aan
VERWARMEN     --start--> (genegeerd)
VERWARMEN     --40 graden--> WASSEN        verwarming uit, trommel draait
WASSEN        --deur open--> (genegeerd)
WASSEN        --30 min voorbij--> POMPEN        trommel stil, pomp aan
POMPEN        --3 min voorbij--> FOUT          pomp uit: filter verstopt?
eindtoestand: FOUT

Combinaties die niet in de tabel staan, worden genegeerd: 'deur open' tijdens het wassen, een tweede druk op start. Het voordeel van een tabel: de machine is data, die je kan nakijken, afdrukken of uit een bestand laden. Het nadeel: voorwaarden zoals [spoelbeurt < 2] en acties bij binnenkomen passen er minder vlot in. Op een microcontroller kiest men daarom meestal de switch.

Uitleg · 32.8

Van diagram naar code: het patroon

8/17

Elke toestandsmachine in dit hoofdstuk volgt hetzelfde patroon:

enum Toestand { A, B, C };           // 1. de toestanden krijgen een naam
Toestand toestand = A;               // 2. één variabele onthoudt waar we zijn
unsigned long sinds = 0;             //    en sinds wanneer

void naar(Toestand nieuw, unsigned long nu) {
  toestand = nieuw;                  // 3. een overgang: toestand en tijd bijwerken,
  sinds = nu;
  // ... acties bij binnenkomen (motor aan, lamp uit, melding)
}

void taak(unsigned long nu) {        // 4. elke ronde van loop(): alleen kijken, nooit wachten
  unsigned long t = nu - sinds;      //    hoe lang al in deze toestand?
  switch (toestand) {
    case A:
      if (gebeurtenis1) naar(B, nu);
      break;                         // 5. break: anders loopt de code door in de volgende case
    case B:
      if (t > TIMEOUT_MS) naar(C, nu);
      break;
    case C:
      break;
  }
}
in het diagramin de code
toestandeen waarde van de enum
beginpuntde beginwaarde van toestand, of naar() in setup()
overgangnaar(...) in de case van de vertrektoestand
gebeurteniseen test: een pin, een vlag, t > ...
[voorwaarde]een extra test met &&
actie bij binnenkomenin naar()
zelflus met een actiede actie, zonder naar()

Vier fouten komen vaak voor:

  • break vergeten. Dan loopt de code door in de volgende case, en test ze gebeurtenissen van een toestand waarin je niet bent.
  • De toestand elke ronde opnieuw zetten. Wie in de case schrijft 'motor aan' in plaats van dat bij de overgang te doen, voert de actie duizenden keren per seconde uit, en sinds schuift mee: een time-out loopt dan nooit af.
  • Wachten in een toestand. Een delay() of een while die op een sensor wacht, legt alle andere taken stil. Een toestand wacht door niets te doen tot de gebeurtenis er is.
  • De toestand in meerdere variabelen. dak en dakOpen samen geven combinaties die niet kunnen bestaan. Eén variabele, één waarheid.
Code · 32.9

Het kasdak als toestandsmachine

9/17

Het dak krijgt zijn eigen bestand, zoals sensoren.cpp in hoofdstuk 29. Het headerbestand zegt wat de rest van het programma mag vragen:

// dak.h - het kasdak als toestandsmachine (hoofdstuk 32)
#ifndef DAK_H
#define DAK_H

enum DakToestand { DICHT, OPENEN, OPEN, SLUITEN, GESTOPT, DAK_FOUT };

extern DakToestand dak;
void dakStart(unsigned long nu);                  // in setup(): waar staat het dak?
void dakVraag(bool open);                         // de klimaatregeling vraagt: open of dicht
void dakKnop(unsigned long nu);                   // korte druk op S1 (met de hand)
void dakHerstel(unsigned long nu);                // na een alarm: opnieuw proberen te sluiten
void dakBlokkeer(bool aan, unsigned long nu);     // onderhoud: de motor mag niet bewegen
void taakDak(unsigned long nu);
const char* dakNaam(DakToestand t);

#endif

De temperatuurregeling van hoofdstuk 31 stuurt de motor niet meer zelf aan: ze vraagt alleen dakVraag(true) als de lucht boven 27 °C komt en dakVraag(false) onder 25 °C. De hysterese maakt van een temperatuur een gebeurtenis; de toestandsmachine beslist of en hoe het dak beweegt. Zo zit elke verantwoordelijkheid op één plaats.

C++ (Arduino)dak.cpp87 regelsDownload
// dak.cpp - het kasdak als toestandsmachine (hoofdstuk 32; motor en eindeloop: hoofdstuk 26)#include <Arduino.h>#include "pinnen.h"#include "dak.h" void melding(const char* tekst);                  // staat in de schets zelf (met de tijd van de klok) const unsigned long TIMEOUT_MS = 12000;           // langer dan 12 s bewegen: er klopt iets nietconst int PWM_DAK = 232;                          // 0,10 m/s bij 12,8 V (hoofdstuk 26)const char* DAK_NAAM[] = {"DICHT", "OPENEN", "OPEN", "SLUITEN", "GESTOPT", "DAK_FOUT"}; DakToestand dak = DICHT;unsigned long dakSinds = 0;bool wensOpen = false, geblokkeerd = false; const char* dakNaam(DakToestand t) { return DAK_NAAM[t]; } void motor(int richting) {                        // +1 sluiten, -1 openen, 0 stil: nooit beide tegelijk  analogWrite(PWM_DICHT, richting > 0 ? PWM_DAK : 0);  analogWrite(PWM_OPEN, richting < 0 ? PWM_DAK : 0);} void dakNaar(DakToestand nieuw, unsigned long nu) {  dak = nieuw;  dakSinds = nu;  if (nieuw == OPENEN) motor(-1);                 // bij binnenkomen: de motor  else if (nieuw == SLUITEN) motor(+1);  else motor(0);  char tekst[24] = "dak: ";  strcat(tekst, DAK_NAAM[nieuw]);  melding(tekst);} void dakStart(unsigned long nu) {  if (digitalRead(EIND_DICHT) == HIGH) dakNaar(DICHT, nu);  else if (digitalRead(EIND_OPEN) == HIGH) { wensOpen = true; dakNaar(OPEN, nu); }  else dakNaar(SLUITEN, nu);                      // onbekende stand: veilig dichtdoen} void dakVraag(bool open) { wensOpen = open; } void dakKnop(unsigned long nu) {  if (geblokkeerd) return;  switch (dak) {    case DICHT:   wensOpen = true; break;    case OPEN:    wensOpen = false; break;    case OPENEN:    case SLUITEN: dakNaar(GESTOPT, nu); break;    // een druk tijdens de beweging = stoppen    case GESTOPT: wensOpen = false; dakNaar(SLUITEN, nu); break;    case DAK_FOUT: break;                         // alleen dakHerstel() haalt het dak hieruit  }} void dakHerstel(unsigned long nu) {  if (dak == DAK_FOUT) {    wensOpen = false;    dakNaar(SLUITEN, nu);  }} void dakBlokkeer(bool aan, unsigned long nu) {  geblokkeerd = aan;  if (aan && (dak == OPENEN || dak == SLUITEN)) dakNaar(GESTOPT, nu);} void taakDak(unsigned long nu) {  unsigned long t = nu - dakSinds;  switch (dak) {    case DICHT:      if (wensOpen && !geblokkeerd) dakNaar(OPENEN, nu);      break;    case OPEN:      if (!wensOpen && !geblokkeerd) dakNaar(SLUITEN, nu);      break;    case OPENEN:      if (digitalRead(EIND_OPEN) == HIGH) dakNaar(OPEN, nu);      else if (t > TIMEOUT_MS) dakNaar(DAK_FOUT, nu);      break;    case SLUITEN:      if (digitalRead(EIND_DICHT) == HIGH) dakNaar(DICHT, nu);      else if (t > TIMEOUT_MS) dakNaar(DAK_FOUT, nu);      break;    case GESTOPT:                                 // wacht op een knopdruk    case DAK_FOUT:                                // wacht op dakHerstel()      break;  }}

Let op dakStart(): na een stroomuitval weet het programma niet waar het dak staat. Het leest de eindeloopschakelaars; staat het dak ergens tussenin, dan sluit het. Hetzelfde principe had de meetkas (eis G5: na een stroomuitval herstarten met het raam dicht). In DAK_FOUT probeert het programma niet uit zichzelf opnieuw: een motor die tegen een blokkade blijft duwen, kan de riem of het paneel beschadigen. Een mens moet eerst kijken.

Verhaal · 32.10

Een lift die niet valt

10/17

Rond 1850 waren liften in fabrieken en pakhuizen gevaarlijk: platformen aan een touw of een ketting, en als die brak, stortte alles naar beneden. Niemand wilde er zelf in. In 1852 bouwde de Amerikaanse monteur Elisha Otis in Yonkers een lift met een veiligheidsrem. Aan het platform zat een veer die door de spanning in het touw platgedrukt bleef. Brak het touw, dan viel die spanning weg, en duwde de veer grijpers in tanden langs de geleiders: het platform bleef hangen.

In mei 1854 toonde Otis zijn lift op de wereldtentoonstelling in het Crystal Palace van New York. Hij liet zich met het platform hoog optrekken en liet het touw doorsnijden. Het platform zakte een klein stukje en bleef hangen. Het publiek was overtuigd, en de veilige lift maakte later wolkenkrabbers mogelijk.

Het geniale zit niet in de rem, maar in wanneer ze werkt: de rem heeft niets nodig om in te grijpen. Geen stroom, geen bediening, geen werkend onderdeel. Het defect zelf (een gebroken touw) brengt de lift in de veilige toestand. Dat heet fail-safe.

Fail-safe in de klimaatmodule

De eindeloopschakelaars van het dak zijn verbreekcontacten met een pull-up: een losse draad leest als 'eindpunt bereikt', en de motor stopt. De vlotter op D2 leest bij een losse draad 'droog', en de pomp stopt. De MOSFET van de verwarming heeft een pull-down van 10 kΩ: als de Mega uitvalt, gaat de gate laag en de verwarming uit. Bij elke keuze vraag je: wat gebeurt er als deze draad breekt?

Een reconstructie: de lift bleef hangen toen het touw doorgesneden werd.
Een reconstructie: de lift bleef hangen toen het touw doorgesneden werd.
Uitleg · 32.11

Foutsituaties en de veilige toestand

11/17

Wat kan er misgaan, en hoe ziet een programma dat?

  • Een sensor antwoordt niet: de SHT31 geeft nan. Na 15 mislukte metingen (30 s) is het geen toeval meer.
  • Een sensor geeft een onmogelijke waarde (−40 °C in een verwarmde module) of een onmogelijke combinatie (beide eindeloopschakelaars tegelijk ingedrukt).
  • Een actuator doet niet wat hij moet: het dak haalt zijn eindpunt niet binnen 12 s. Een time-out ziet het.
  • Het programma zelf hangt: de waakhond ziet het (volgend blad).
  • De stroom valt weg: bij het herstarten begint alles in OPSTART, met alle uitgangen uit.

Dan de vraag: wat is veilig? Niet altijd 'alles uit'. Een verwarming uitzetten is veilig in juli, maar laat de sla bevriezen in januari. Het dak dichtdoen is veilig bij storm, maar laat de module koken op een zonnige middag. De veilige reactie hangt af van de fout en van de omstandigheden, en ze doet alleen iets aan wat de fout raakt: een droge vlotter stopt de pomp, niet de verwarming.

fouthoe de Mega ze zietveilige reactieweg als
temperatuursensor weg15 metingen na elkaar nan (30 s)verwarming blind op het gemiddelde; dak blijft staaner weer metingen zijn
vlotter droogD2 hoog, 10 s lang (ook bij een losse draad)pomp uitde vlotter weer drijft
reservoir onder 10 cmMPX5010 op A5pomp uithet niveau boven 12 cm is
dak klemteindeloop niet gehaald binnen 12 smotor stil, geen nieuwe pogingiemand bevestigt: één nieuwe poging
Veilig betekent alles uitzetten

Een lift die bij een fout de rem loslaat om 'alles uit' te zetten, valt. Een schuifdeur op een vluchtweg moet bij een stroomuitval juist open. Kies per fout de toestand met de minste schade, en laat de rest van het systeem zo goed mogelijk verder werken.

verbreekcontact (zoals D23 en D24)5 Vpull-uppindraad losmaakcontact5 Vpull-uppindraad losNormaal: contact dicht, pin LOW.Eindpunt: contact open, pin HIGH.Draad los: ook HIGH, de motor stopt.Normaal: contact open, pin HIGH.Eindpunt: contact dicht, pin LOW.Draad los: nooit LOW, de motor loopt door.
Dezelfde losse draad, twee soorten contact: alleen het verbreekcontact is fail-safe.
Code · 32.12

De waakhond

12/17

Een programma kan blijven hangen: een while die wacht op een sensor die nooit antwoordt, een oneindige lus door een fout. Dan draait niets meer, ook de veiligheid niet. De ATmega heeft daarvoor een waakhond (watchdog): een aparte timer met een eigen oscillator. Het programma moet hem regelmatig geruststellen met wdt_reset(). Blijft dat uit, bijvoorbeeld 2 s lang, dan herstart de waakhond de microcontroller: setup() begint opnieuw, alle uitgangen gaan in hun veilige beginstand.

C++ (Arduino)waakhond.ino31 regelsDownload
// waakhond.ino - de waakhond (watchdog) herstart de Mega als loop() blijft hangen (hoofdstuk 32)#include <avr/wdt.h> const int KLAAR = 3;                  // 'meting klaar'-lijn van een sensor: HIGH = er is een metingunsigned long vorige = 0;int metingen = 0; void setup() {  wdt_disable();                      // eerst uit: misschien zijn we net door de waakhond herstart  Serial.begin(115200);  pinMode(KLAAR, INPUT);  vorige = 0;  metingen = 0;  Serial.println("opgestart");  wdt_enable(WDTO_2S);                // komt loop() 2 s niet meer langs wdt_reset(): herstart} void loop() {  wdt_reset();                        // 'ik leef nog', elke ronde  unsigned long nu = millis();  if (nu - vorige >= 1000) {    vorige += 1000;    while (digitalRead(KLAAR) == LOW) {      // wachten op de sensor, zonder uitweg: hangt de sensor, dan hangt alles    }    metingen++;    Serial.print(nu / 1000.0, 1);    Serial.print(" s  meting ");    Serial.println(metingen);  }}
sensor klaar (D3)loop() hangtreset0246810121416t (s)
Hangt loop() langer dan 2 s, dan volgt een herstart.

In de test hangt de sensor van 4,5 tot 11,5 s. De seriële monitor (de regels met --- komen van de simulatie):

opgestart
1.0 s  meting 1
2.0 s  meting 2
3.0 s  meting 3
4.0 s  meting 4
--- 7.0 s: de waakhond herstart de Mega ---
opgestart
--- 10.0 s: de waakhond herstart de Mega ---
opgestart
1.0 s  meting 1
2.0 s  meting 2
3.0 s  meting 3
4.0 s  meting 4
5.0 s  meting 5
6.0 s  meting 6

Na een herstart begint millis() opnieuw bij 0. De waakhond lost de oorzaak niet op: zolang de sensor hangt, herstart de Mega om de 2 à 3 s. Hij beperkt wel de schade: het programma komt telkens in een veilige begintoestand. De echte oplossing is een wachtlus met een uitweg (een time-out). Zet wdt_reset() nooit in zo'n wachtlus: dan kan de waakhond niets meer zien. Test de waakhond op je eigen bord voor je erop vertrouwt.

Ontleed · 32.13

De modi van de klimaatmodule

13/17

De klimaatmodule doet niet altijd hetzelfde. Overdag branden de lampen en volgt het dak de temperatuur, 's nachts niet. Bij een fout moet ze voorzichtig zijn, en als iemand binnen werkt, mag er niets onverwacht bewegen. Die modi vormen een toestandsmachine. DAG en NACHT reageren op de meeste gebeurtenissen hetzelfde: ze zitten samen in een samengestelde toestand NORMAAL. Een pijl vanaf de rand van NORMAAL geldt voor DAG en NACHT allebei.

NORMAALOPSTARTalles uit, 5 sDAG6 tot 22 uurNACHT22 tot 6 uurALARMled snelONDERHOUDled vast aan[5 s]kort / dak met de hand[22 uur][6 uur][alarm]kort[geen oorzaak]langlang of60 minlang
Vijf modi; DAG en NACHT vormen samen NORMAAL.
Wijs een nummer aan: de figuur zoomt in. Klik om vast te zetten.
  1. 1
    OPSTART

    Na het aanzetten, na een stroomuitval of na de waakhond: alle uitgangen uit, 5 s om de sensoren en de klok te lezen, de stand van het dak zoeken.

  2. 2
    Samengestelde toestand

    NORMAAL bevat DAG en NACHT. Na OPSTART kiest de klok welke van de twee.

  3. 3
    Een korte druk

    In DAG en NACHT opent of sluit een korte druk op S1 het dak met de hand, zoals in hoofdstuk 29.

  4. 4
    De klok

    Om 22 uur naar NACHT (lampen uit, dak dicht), om 6 uur naar DAG. De klok komt van de DS3231 (hoofdstuk 30).

  5. 5
    Naar ALARM

    Zodra er een oorzaak is: sensor weg, vlotter droog, reservoir te laag, dak klemt. Vanuit DAG en NACHT.

  6. 6
    Bevestigen

    Een korte druk, alleen als de oorzaak weg is. Een alarm gaat niet vanzelf uit: iemand moet het gezien hebben.

  7. 7
    Onderhoud

    Een lange druk (3 s) zet ONDERHOUD aan en weer uit. Vergeten? Na 60 minuten keert de module vanzelf terug.

  8. 8
    Van ALARM naar ONDERHOUD

    Wie een fout komt herstellen, zet onderhoud aan: dan beweegt het dak niet terwijl hij binnen werkt.

Uitleg · 32.14

Wat elke modus doet

14/17

Een toestandsmachine zegt wanneer de module van modus verandert; de actietabel zegt wat elke modus met de uitgangen doet. Die tabel is het echte ontwerp:

modusgroeilampenpompverwarmingventilatorenkasdakCO₂-klepstatusled
OPSTARTuituituitzachteindeloop lezendichtuit
DAGaanmin. 0-918/19 °Cvol boven 25 °Cvolgt 27/25 °C30 s per halfuur500 ms
NACHTuitmin. 0-916/17 °Czachtdichtdicht500 ms
ALARMzoals dag of nachtuit bij een wateralarmblind bij een sensorfoutzoals dag of nachtstil bij een dakfoutdicht100 ms
ONDERHOUDwerklichtuitzoals dag of nachtvolgeblokkeerddichtvast aan

Drie keuzes vallen op. In ONDERHOUD is het dak geblokkeerd, gaat de CO₂-klep dicht (wie binnen werkt, mag geen extra CO₂ inademen, hoofdstuk 14) en staat de pomp stil (handen in het reservoir). In ALARM werkt de module zo gewoon mogelijk verder: alleen wat de fout raakt, stopt. En de alarmen zijn bits: er kunnen er meerdere tegelijk zijn, en de module weet welke.

Eén variabele per onafhankelijk aspect

Dag of nacht hangt van de klok af; alarm of niet van de sensoren. Wie ook nog 'batterij bijna leeg' als modus wil toevoegen, krijgt DAG_ZUINIG, NACHT_ZUINIG, ... : het aantal modi verdubbelt. Dan is een tweede, kleine toestandsmachine voor het energiebeheer beter (opdracht 32.13).

Code · 32.15

klimaat.ino versie 4: de modi in C++

15/17

De modi komen bovenop de taken van hoofdstuk 29 tot 31. Om het overzichtelijk te houden, staan hier alleen de modi, de alarmen, de verwarming, de ventilatoren, de pomp, de CO₂-klep en het dak (uit dak.cpp); de logger van hoofdstuk 30 en het dimmen van de lampen van hoofdstuk 25 blijven zoals ze waren. De knop onderscheidt een korte en een lange druk.

C++ (Arduino)modi.ino212 regelsDownload
// modi.ino - de modi van de klimaatmodule als toestandsmachine (hoofdstuk 32; klimaat.ino versie 4)// OPSTART -> DAG of NACHT volgens de klok; ALARM bij een fout; ONDERHOUD met een lange druk op S1 (3 s).#include <Wire.h>#include <RTClib.h>#include <Adafruit_SHT31.h>#include "pinnen.h"#include "dak.h" enum Modus { OPSTART, DAG, NACHT, ALARM, ONDERHOUD };const char* NAAM[] = {"OPSTART", "DAG", "NACHT", "ALARM", "ONDERHOUD"};const byte A_SENSOR = 1, A_WATER = 2, A_NIVEAU = 4, A_DAK = 8;     // oorzaken van een alarm, als bits const unsigned long MEET_MS = 2000;const unsigned long LANG_MS = 3000;                  // zo lang indrukken = onderhoud aan of uitconst unsigned long ONDERHOUD_MAX_MS = 3600000UL;    // vergeten uit te zetten: na een uur terugconst int WERKLICHT = 60, LAMPEN_DAG = 200;          // PWM van de groeilampenconst int VENT_LAAG = 90, VENT_VOL = 255; Adafruit_SHT31 sht;RTC_DS3231 rtc;DateTime klok;Modus modus = OPSTART;unsigned long modusSinds = 0, vorigeMeting = 0, knopSinds = 0, droogSinds = 0, ledSinds = 0;byte alarmen = 0;                                    // welke alarmen er nu zijnfloat luchtT = NAN, gemAan = 0.5;int geenMeting = 0;bool verwarmen = false, koelen = false, warm = false, knopWas = false, langGemeld = false, niveauLaag = false;bool ledAan = false; // ---------------------------------------------------------------- meldingenvoid printTwee(int x) {  if (x < 10) Serial.print('0');  Serial.print(x);} void melding(const char* tekst) {  DateTime k = rtc.now();  Serial.print("# ");  printTwee(k.hour());  Serial.print(':');  printTwee(k.minute());  Serial.print(':');  printTwee(k.second());  Serial.print(' ');  Serial.println(tekst);} // ---------------------------------------------------------------- sensoren en alarmenfloat niveauCm() {                                   // MPX5010 onderaan het reservoir (hoofdstuk 11 en 29)  float u = analogRead(DRUK) * 5.0 / 1024.0;  float p = (u / 5.0 - 0.04) / 0.09 * 1000.0;  return p / (1000.0 * 9.81) * 100.0;} void meten(unsigned long nu) {  float t, rv;  if (sht.readBoth(&t, &rv)) { luchtT = t; geenMeting = 0; }  else { luchtT = NAN; geenMeting++; }  bool droog = digitalRead(VLOTTER) == HIGH;         // verbreekt ook als de draad los is  if (!droog) droogSinds = nu;  float h = niveauCm();  if (!niveauLaag && h < 10.0) niveauLaag = true;    // hysterese: 10 en 12 cm  else if (niveauLaag && h > 12.0) niveauLaag = false;  byte nieuw = 0;  if (geenMeting >= 15) nieuw |= A_SENSOR;           // 30 s geen temperatuur  if (nu - droogSinds >= 10000) nieuw |= A_WATER;    // 10 s droog: geen dender van een golfje  if (niveauLaag) nieuw |= A_NIVEAU;  if (dak == DAK_FOUT) nieuw |= A_DAK;  if (nieuw & ~alarmen & A_SENSOR) melding("oorzaak: geen temperatuur");  if (nieuw & ~alarmen & A_WATER) melding("oorzaak: vlotter droog");  if (nieuw & ~alarmen & A_NIVEAU) melding("oorzaak: reservoir onder 10 cm");  if (nieuw & ~alarmen & A_DAK) melding("oorzaak: dak haalt zijn eindpunt niet");  alarmen = nieuw;} // ---------------------------------------------------------------- de knop: kort of langvoid leesKnop(unsigned long nu, bool& kort, bool& lang) {  bool in = digitalRead(KNOP) == HIGH;               // na de 74HC14: hoog = ingedrukt (hoofdstuk 24)  kort = lang = false;  if (in && !knopWas) { knopSinds = nu; langGemeld = false; }  if (in && !langGemeld && nu - knopSinds >= LANG_MS) { lang = true; langGemeld = true; }  if (!in && knopWas && !langGemeld) kort = true;  knopWas = in;} // ---------------------------------------------------------------- de modibool isDag() { return klok.hour() >= 6 && klok.hour() < 22; }Modus gewoon() { return isDag() ? DAG : NACHT; } void naar(Modus nieuw, unsigned long nu) {  Modus oud = modus;  modus = nieuw;  modusSinds = nu;  char tekst[32] = "modus ";  strcat(tekst, NAAM[nieuw]);  melding(tekst);  // bij binnenkomen en bij verlaten  if (nieuw == ONDERHOUD) dakBlokkeer(true, nu);     // niemand onder een bewegend dak  if (oud == ONDERHOUD) dakBlokkeer(false, nu);  if (nieuw == NACHT) dakVraag(false);  if (nieuw == DAG) dakVraag(warm);                  // overdag volgt het dak de temperatuur} void taakModus(unsigned long nu, bool kort, bool lang) {  switch (modus) {    case OPSTART:                                    // 5 s: sensoren en klok lezen      if (nu - modusSinds >= 5000) naar(gewoon(), nu);   // een alarm volgt dan meteen vanuit DAG of NACHT      break;    case DAG:    case NACHT:      if (alarmen) naar(ALARM, nu);      else if (lang) naar(ONDERHOUD, nu);      else if (gewoon() != modus) naar(gewoon(), nu);      else if (kort) dakKnop(nu);                    // korte druk: het dak met de hand      break;    case ALARM:      if (kort) {                                    // bevestigen        if (alarmen & A_DAK) {                       // het dak nog eens laten sluiten          dakHerstel(nu);          alarmen &= ~A_DAK;                         // het dak is niet meer in DAK_FOUT        }        if (alarmen == 0) naar(gewoon(), nu);        else melding("alarm blijft: oorzaak nog aanwezig");      } else if (lang) naar(ONDERHOUD, nu);      break;    case ONDERHOUD:      if (lang || nu - modusSinds >= ONDERHOUD_MAX_MS) naar(gewoon(), nu);      break;  }} // ---------------------------------------------------------------- wat elke modus doetvoid acties(unsigned long nu) {  bool dag = isDag();  // verwarming en ventilatoren met hysterese (hoofdstuk 31); zonder meting blind  float laag = dag ? 18.0 : 16.0, hoog = laag + 1.0;  if (isnan(luchtT)) {    verwarmen = (nu / 1000UL) % 600UL < (unsigned long)(gemAan * 600);    koelen = false;  } else {    if (!verwarmen && luchtT < laag) verwarmen = true;    else if (verwarmen && luchtT > hoog) verwarmen = false;    if (!koelen && luchtT > 25.0) koelen = true;    else if (koelen && luchtT < 24.0) koelen = false;    if (verwarmen) koelen = false;    gemAan += ((verwarmen ? 1.0 : 0.0) - gemAan) * (MEET_MS / 3600000.0);    // het dak: hysterese 25/27 geeft gebeurtenissen; overdag in DAG en ALARM (niet bij een dakfout)    bool automatisch = dag && (modus == DAG || (modus == ALARM && !(alarmen & A_DAK)));    if (!warm && luchtT > 27.0) { warm = true; if (automatisch) dakVraag(true); }    else if (warm && luchtT < 25.0) { warm = false; if (automatisch) dakVraag(false); }  }  if (modus == ONDERHOUD) koelen = true;             // verse lucht voor wie binnen werkt  digitalWrite(VERWARMING, (verwarmen && modus != OPSTART) ? HIGH : LOW);  analogWrite(VENTILATOREN, koelen ? VENT_VOL : VENT_LAAG);  // groeilampen: overdag aan (het dimmen van hoofdstuk 25 laten we hier weg), werklicht bij onderhoud  int lamp = 0;  if (modus == ONDERHOUD) lamp = WERKLICHT;  else if (dag && modus != OPSTART) lamp = LAMPEN_DAG;  analogWrite(LAMPEN, lamp);  // pomp: minuut 0 tot 9 van elk uur, nooit zonder water, niet bij onderhoud  bool pomp = (modus == DAG || modus == NACHT || modus == ALARM) && klok.minute() < 10              && !(alarmen & (A_WATER | A_NIVEAU));  digitalWrite(POMP, pomp ? HIGH : LOW);  // CO2: alleen in DAG, met het dak dicht en zonder koelen: om het halfuur 30 s  bool co2 = modus == DAG && dak == DICHT && !koelen && klok.minute() % 30 == 0 && klok.second() < 30;  digitalWrite(CO2_KLEP, co2 ? HIGH : LOW);} void taakStatusled(unsigned long nu) {               // DAG en NACHT: rustig, ALARM: snel, ONDERHOUD: vast aan  if (modus == ONDERHOUD) { digitalWrite(STATUSLED, HIGH); return; }  unsigned long periode = (modus == ALARM) ? 100 : 500;  if (nu - ledSinds < periode) return;  ledSinds = nu;  ledAan = !ledAan;  digitalWrite(STATUSLED, (ledAan && modus != OPSTART) ? HIGH : LOW);} // ---------------------------------------------------------------- setup en loopvoid setup() {  const int UIT[] = {LAMPEN, PWM_DICHT, PWM_OPEN, DAK_ENABLE, POMP, CO2_KLEP, VENTILATOREN, VERWARMING, STATUSLED};  for (int p : UIT) { pinMode(p, OUTPUT); digitalWrite(p, LOW); }   // veilig starten: 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);  Wire.begin();  rtc.begin();  sht.begin(0x44);  klok = rtc.now();  unsigned long nu = millis();  droogSinds = nu;  melding("klimaatmodule v4 (hoofdstuk 32)");  naar(OPSTART, nu);  dakStart(nu);} void loop() {  unsigned long nu = millis();  bool kort, lang;  leesKnop(nu, kort, lang);  if (nu - vorigeMeting >= MEET_MS) {    vorigeMeting += MEET_MS;    klok = rtc.now();    meten(nu);    acties(nu);  }  taakModus(nu, kort, lang);  taakDak(nu);  taakStatusled(nu);}

Getest op een zonnige aprildag, van 5:50 tot middernacht, met het warmtemodel van hoofdstuk 31 en een gesimuleerd dak. Het scenario: onderhoud van 9:30 tot 9:52, het stekkertje van de SHT31 los van 10:40 tot 10:45, een dak dat klemt van 15:30 tot 16:40, en een droge vlotter van 23:10 tot 23:35. De seriële monitor:

# 05:50:00 klimaatmodule v4 (hoofdstuk 32)
# 05:50:00 modus OPSTART
# 05:50:00 dak: DICHT
# 05:50:05 modus NACHT
# 06:00:00 modus DAG
# 09:30:03 modus ONDERHOUD
# 09:52:03 modus DAG
# 10:40:28 oorzaak: geen temperatuur
# 10:40:28 modus ALARM
# 10:45:00 dak: OPENEN
# 10:45:07 dak: OPEN
# 10:50:00 modus DAG
# 10:51:38 dak: SLUITEN
# 10:51:45 dak: DICHT
# 11:06:10 dak: OPENEN
# 11:06:17 dak: OPEN
# 11:12:24 dak: SLUITEN
# 11:12:31 dak: DICHT
# 11:21:52 dak: OPENEN
# 11:21:59 dak: OPEN
# 11:30:14 dak: SLUITEN
# 11:30:21 dak: DICHT
# 11:37:30 dak: OPENEN
# 11:37:37 dak: OPEN
# 11:50:54 dak: SLUITEN
# 11:51:01 dak: DICHT
# 11:56:44 dak: OPENEN
# 11:56:51 dak: OPEN
# 15:39:34 dak: SLUITEN
# 15:39:46 dak: DAK_FOUT
# 15:39:48 oorzaak: dak haalt zijn eindpunt niet
# 15:39:48 modus ALARM
# 16:45:00 dak: SLUITEN
# 16:45:00 modus DAG
# 16:45:03 dak: DICHT
# 22:00:00 modus NACHT
# 23:10:08 oorzaak: vlotter droog
# 23:10:08 modus ALARM
# 23:40:00 modus NACHT

De test vond een fout die in een eerdere versie zat. Tijdens het sensoralarm steeg de lucht boven 27 °C. Die gebeurtenis ging verloren, en na het bevestigen bleef het dak dicht tot 35 °C. De oplossing staat in naar(): bij het binnenkomen van DAG vraagt het programma het dak opnieuw wat de temperatuur vraagt (dakVraag(warm)), en in ALARM volgt het dak de temperatuur gewoon verder. Een gebeurtenis die komt terwijl niemand luistert, is weg: zet wat telt ook in de actie bij binnenkomen.

681012141618202224141618202224262830tijdstip (h)luchttemperatuur (°C)lucht
De luchttemperatuur met de modi: rood ALARM, paars ONDERHOUD, blauwgroen NACHT.
ONDERHOUDALARMNACHTdak openverwarming681012141618202224tijdstip (h)
Rond 11 uur pendelt het dak: vier keer open in een uur. Opdracht 32.14 zoekt een oplossing.
Naslag · 32.16

Notatie, patroon en controlelijst

16/17
tijd in de huidige toestand (ms)
symboolbetekeniseenheid
millis() in deze rondems
millis() bij het binnenkomenms
tekenbetekenis
zwart bolletje met pijlbegin
afgeronde rechthoektoestand, eventueel met 'bij binnenkomen: actie'
pijl met gebeurtenis [voorwaarde] / actieovergang
pijl naar dezelfde toestandzelflus: actie zonder van toestand te veranderen
rechthoek rond toestandensamengestelde toestand: een pijl vanaf de rand geldt voor alle binnenste
controlelijst voor een toestandsmachine
Is er precies één begintoestand, en is ze veilig (alles uit, stand van actuatoren lezen)?
Is elk vakje van de toestandstabel bewust ingevuld, ook de lege?
Heeft elke toestand die op een sensor wacht, een time-out?
Zijn onmogelijke combinaties een fout in plaats van 'niets'?
Is voor elke fout de veilige toestand gekozen, per seizoen of omstandigheid?
Staat de toestand in één variabele, en gebeuren acties bij de overgang, niet elke ronde?
Wacht er nergens een delay() of while zonder uitweg?
Is elke overgang minstens één keer getest met een scenario?
Het geheel · 32.17

Waar toestandsmachines vandaan komen en waar ze heen gaan

17/17

Toestandsmachines zitten overal waar iets in stappen gebeurt of op gebeurtenissen reageert: verkeerslichten, liften, wasmachines, drankautomaten, protocollen tussen computers, spelfiguren in een game. In de informatica heten ze eindige automaten; in Expert bouw je ze ook met poorten en flipflops (hoofdstuk 23), en worden ze bewezen veilig ontworpen met een FMEA (hoofdstuk 46).

Gebeurtenissen en 'wanneer'-blokken: de eerste stap naar toestanden.

Het verkeerslicht met delay(): nu zonder wachten en met een knop.

De dakmotor, de H-brug, de eindeloopschakelaars en de time-out.

millis(), taken, enum en eigen bestanden (.h en .cpp).

Hysterese maakt van een temperatuur een gebeurtenis voor de toestandsmachine.

Het energiebeheer met drempels: een toestandsmachine voor de batterij.

Lijn volgen, meten, obstakels vermijden: de robot als toestandsmachine.

Toestandsmachines in hardware: flipflops en de noodstop-logica.

Interrupts, timers en real-time eisen.

Faalwijzen systematisch zoeken (FMEA) en fail-safe ontwerpen.

HabitatDe klimaatmodule heeft nu vijf modi (OPSTART, DAG, NACHT, ALARM, ONDERHOUD), een actietabel en een alarmtabel, en het kasdak is een toestandsmachine met time-out en veilige start. In hoofdstuk 33 wordt het inspectierobotje een toestandsmachine; in het integratieproject (hoofdstuk 40) test je alle modi met een scenario op de echte module.
Samenvatting

In het kort

Begrippen

Wat je nu kent

toestandsmachine
Een systeem dat op elk ogenblik in precies één van een beperkt aantal toestanden is, en bij gebeurtenissen van toestand verandert.
toestand
Een situatie waarin een systeem kan blijven, met een vast gedrag (DICHT, OPENEN, NACHT).
gebeurtenis
Iets dat gebeurt en een overgang kan veroorzaken: een knopdruk, een eindeloopschakelaar, een tijd die om is.
overgang
De stap van een toestand naar een andere (of dezelfde), bij een gebeurtenis, eventueel alleen als een voorwaarde klopt.
toestandstabel
Tabel met de toestanden als rijen en de gebeurtenissen als kolommen; elk vakje zegt wat er gebeurt.
time-out
Een grens op de tijd in een toestand; wordt ze overschreden, dan is er iets mis.
veilige toestand
De toestand waarin een systeem bij een fout de minste schade doet; ze hangt af van de fout en van de omstandigheden.
fail-safe
Zo ontworpen dat een defect (een losse draad, een stroomuitval) vanzelf tot de veilige toestand leidt.
waakhond (watchdog)
Een aparte timer in de microcontroller die hem herstart als het programma niet regelmatig laat weten dat het nog loopt.
Aan de slag in het werkboek16 opdrachten, van oefenen tot uitdagen