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.

- Je kan in een systeem toestanden, gebeurtenissen, overgangen en acties herkennen en benoemen.
- Je kan een toestandsdiagram lezen en tekenen met de notatie gebeurtenis [voorwaarde] / actie.
- Je kan een toestandsdiagram omzetten in een toestandstabel en ermee controleren of elke combinatie bedacht is.
- Je kan een toestandsmachine programmeren met enum en switch-case, zonder delay(), met de tijd in een toestand.
- Je kan fouten opsporen met time-outs en onmogelijke combinaties, en per fout een veilige toestand kiezen.
- Je kan uitleggen wat fail-safe betekent en wat een waakhond doet.
- Je kan de modi van de klimaatmodule ontwerpen, programmeren en met een testscenario controleren.
Een programma dat onthoudt waar het is
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.
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.
Het toestandsdiagram
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:
| toestand | knop | wens open | wens dicht | eindeloop open | eindeloop dicht | t > 12 s |
|---|---|---|---|---|---|---|
| DICHT | – (wens = open) | OPENEN | – | – | – | – |
| OPENEN | GESTOPT | – | – | OPEN | – | DAK_FOUT |
| OPEN | – (wens = dicht) | – | SLUITEN | – | – | – |
| SLUITEN | GESTOPT | – | – | – | DICHT | DAK_FOUT |
| GESTOPT | SLUITEN (wens = dicht) | – | – | – | – | – |
| DAK_FOUT | – | – | – | – | – | – |
DAK_FOUT verlaat je alleen met dakHerstel(), na een bevestigd alarm.
Een voetgangersoversteek ontwerpen
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.
- 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'
- toestanden, overgangen en acties
- een toestandsdiagram
- 1Toestanden 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.
- 2Gebeurtenissen: de knop, en de tijd in een toestand. De tijd schrijf je als voorwaarde: [t ≥ 3 s].
- 3Een 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.
- 4De overgang uit AUTO_GROEN heeft een samengestelde voorwaarde: [aanvraag en t ≥ 20 s]. Alle andere overgangen hangen alleen van de tijd af.
- 5Acties bij binnenkomen: de lampen van elke toestand. In VOETGANGER_GROEN ook: aanvraag = nee, lampje 'wacht' uit.
Het voetgangerslicht in C++
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).
// 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; }}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.
De automatische deur: van diagram naar tabel
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.
- gebeurtenissen: beweging, eindeloop open, eindeloop dicht, 5 s niemand, fotocel onderbroken
- toestanden
- toestandstabel
- veiligheid
- 1Toestanden waarin de deur anders doet: DICHT, OPENEN, OPEN (timer loopt), SLUITEN.
- 2Vul voor elke toestand elke kolom in. In DICHT doet alleen beweging iets; in OPENEN alleen 'eindeloop open'.
- 3In OPEN start 'beweging' de timer opnieuw: een zelflus. Ook de fotocel: wie stilstaat in de opening, beweegt misschien niet voor de radar.
- 4Veiligheid: in SLUITEN zijn 'beweging' en 'fotocel' de belangrijkste vakjes. De deur keert om naar OPENEN: niemand raakt gekneld.
- 5Lege 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.
| toestand | beweging | eindeloop open | eindeloop dicht | 5 s niemand | fotocel onderbroken |
|---|---|---|---|---|---|
| DICHT | OPENEN / motor open | – | – | – | – |
| OPENEN | – | OPEN / motor stil, timer | – | – | – |
| OPEN | OPEN / timer opnieuw | – | – | SLUITEN / motor dicht | OPEN / timer opnieuw |
| SLUITEN | OPENEN / motor open | – | DICHT / motor stil | – | OPENEN / motor open |
De wasmachine: een programma in stappen
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.
Een toestandsmachine als tabel
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.
# 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.
Van diagram naar code: het patroon
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 diagram | in de code |
|---|---|
| toestand | een waarde van de enum |
| beginpunt | de beginwaarde van toestand, of naar() in setup() |
| overgang | naar(...) in de case van de vertrektoestand |
| gebeurtenis | een test: een pin, een vlag, t > ... |
| [voorwaarde] | een extra test met && |
| actie bij binnenkomen | in naar() |
| zelflus met een actie | de 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
caseschrijft 'motor aan' in plaats van dat bij de overgang te doen, voert de actie duizenden keren per seconde uit, ensindsschuift mee: een time-out loopt dan nooit af. - Wachten in een toestand. Een
delay()of eenwhiledie 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.
dakendakOpensamen geven combinaties die niet kunnen bestaan. Eén variabele, één waarheid.
Het kasdak als toestandsmachine
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.
// 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.
Een lift die niet valt
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.
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?

Foutsituaties en de veilige toestand
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.
| fout | hoe de Mega ze ziet | veilige reactie | weg als |
|---|---|---|---|
| temperatuursensor weg | 15 metingen na elkaar nan (30 s) | verwarming blind op het gemiddelde; dak blijft staan | er weer metingen zijn |
| vlotter droog | D2 hoog, 10 s lang (ook bij een losse draad) | pomp uit | de vlotter weer drijft |
| reservoir onder 10 cm | MPX5010 op A5 | pomp uit | het niveau boven 12 cm is |
| dak klemt | eindeloop niet gehaald binnen 12 s | motor stil, geen nieuwe poging | iemand bevestigt: één nieuwe poging |
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.
De waakhond
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.
// 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); }}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.
De modi van de klimaatmodule
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.
- 1OPSTART
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.
- 2Samengestelde toestand
NORMAAL bevat DAG en NACHT. Na OPSTART kiest de klok welke van de twee.
- 3Een korte druk
In DAG en NACHT opent of sluit een korte druk op S1 het dak met de hand, zoals in hoofdstuk 29.
- 4De klok
Om 22 uur naar NACHT (lampen uit, dak dicht), om 6 uur naar DAG. De klok komt van de DS3231 (hoofdstuk 30).
- 5Naar ALARM
Zodra er een oorzaak is: sensor weg, vlotter droog, reservoir te laag, dak klemt. Vanuit DAG en NACHT.
- 6Bevestigen
Een korte druk, alleen als de oorzaak weg is. Een alarm gaat niet vanzelf uit: iemand moet het gezien hebben.
- 7Onderhoud
Een lange druk (3 s) zet ONDERHOUD aan en weer uit. Vergeten? Na 60 minuten keert de module vanzelf terug.
- 8Van ALARM naar ONDERHOUD
Wie een fout komt herstellen, zet onderhoud aan: dan beweegt het dak niet terwijl hij binnen werkt.
Wat elke modus doet
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:
| modus | groeilampen | pomp | verwarming | ventilatoren | kasdak | CO₂-klep | statusled |
|---|---|---|---|---|---|---|---|
| OPSTART | uit | uit | uit | zacht | eindeloop lezen | dicht | uit |
| DAG | aan | min. 0-9 | 18/19 °C | vol boven 25 °C | volgt 27/25 °C | 30 s per halfuur | 500 ms |
| NACHT | uit | min. 0-9 | 16/17 °C | zacht | dicht | dicht | 500 ms |
| ALARM | zoals dag of nacht | uit bij een wateralarm | blind bij een sensorfout | zoals dag of nacht | stil bij een dakfout | dicht | 100 ms |
| ONDERHOUD | werklicht | uit | zoals dag of nacht | vol | geblokkeerd | dicht | vast 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.
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).
klimaat.ino versie 4: de modi in C++
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.
// 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.
Notatie, patroon en controlelijst
| symbool | betekenis | eenheid |
|---|---|---|
| millis() in deze ronde | ms | |
| millis() bij het binnenkomen | ms |
| teken | betekenis |
|---|---|
| zwart bolletje met pijl | begin |
| afgeronde rechthoek | toestand, eventueel met 'bij binnenkomen: actie' |
| pijl met gebeurtenis [voorwaarde] / actie | overgang |
| pijl naar dezelfde toestand | zelflus: actie zonder van toestand te veranderen |
| rechthoek rond toestanden | samengestelde 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? |
Waar toestandsmachines vandaan komen en waar ze heen gaan
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.
In het kort
- Een toestandsmachine is altijd in precies één toestand; gebeurtenissen veroorzaken overgangen, met acties.
- In een diagram staat op elke pijl gebeurtenis [voorwaarde] / actie; acties bij binnenkomen staan in de toestand.
- Een toestandstabel (toestanden × gebeurtenissen) dwingt je om elke combinatie te bedenken, ook de lege.
- In C++: een enum, één variabele, naar() voor overgangen en acties, een switch met break, de tijd als nu − sinds.
- Time-outs en onmogelijke combinaties verraden fouten; de veilige toestand hangt af van de fout en de omstandigheden.
- Fail-safe: een defect brengt het systeem vanzelf in de veilige toestand (verbreekcontact, pull-up, rem van Otis).
- De waakhond herstart een programma dat blijft hangen; hij vervangt geen goede time-outs.
- De klimaatmodule heeft vijf modi; DAG en NACHT vormen samen NORMAAL, ALARM laat zoveel mogelijk verder werken.
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.