Område 3 · lektion 14
Tillstånd
Två knappar och en regel räcker inte. Ett riktigt bygge behöver också veta var i förloppet det befinner sig: om luckan redan är öppen, om någon fortfarande håller in en knapp sedan förra gången, om väntetiden är igång. Det systemet minns kallas dess tillstånd, och att skriva ner det är den billigaste felsökning som finns.
Mål
- Du kan förklara skillnaden mellan en krets som bara reagerar på nuet och ett system som minns, och säga vilken av dem ditt bygge är.
- Du kan införa en tillståndsvariabel med namngivna lägen och skriva övergångarna mellan dem.
- Du kan rita ett tillståndsdiagram över ditt eget bygge och räkna ut hur många fall diagrammet måste svara på.
- Du kan kombinera tillstånd med
millis(), så att ett läge kan ta slut av sig självt.
Genomgång
Två sorters kretsar
Grindarna i den första guiden har inget minne. Skickar du in samma insignaler får du samma utsignal, varje gång, oavsett vad som hände en sekund tidigare. En sådan krets kallas kombinatorisk, och hela dess beteende ryms i sanningstabellen.
Så fort svaret beror på något mer än de nuvarande insignalerna har systemet ett tillstånd. Utsignalen beror då på insignalerna och på vad som hänt tidigare, och sanningstabellen räcker inte längre som beskrivning.
Du har redan byggt ett sådant system utan att kalla det så. Raden
tand = !tand; i
avstudsningsexemplet
från förra området är en
tillståndsvariabel med två lägen: lampan är tänd eller släckt, och ett
tryck betyder olika saker beroende på vilket. Samma knapptryck, två olika
utfall – det är definitionen på ett system med minne.
Tillståndet är det systemet minns, inte allt det vet
Ett tillstånd är inte varje variabel i programmet. Det är den knappa uppsättning lägen systemet kan befinna sig i, där varje läge betyder något annat för hur nästa insignal ska tolkas. Bra tillstånd går att säga högt i ett ord: stängd, öppnar, öppen, stänger.
Testet är enkelt: om du kan svara på frågan "vad händer om någon trycker på knappen nu?" utan att veta vilket läge systemet står i, har du förmodligen inget tillstånd. Måste du fråga tillbaka "var är det nu?" – då har du det.
Tillståndsdiagrammet
Diagrammet ritas som lägen med pilar emellan. Varje pil är en övergång, och intill pilen står vad som utlöser den.
- Rutorna är lägen. Systemet står alltid i exakt ett av dem, aldrig i två och aldrig i inget.
- Pilarna är övergångar, och texten intill är villkoret som utlöser dem. "Båda ned" betyder att båda knapparna är nedtryckta, "i läge" att servot nått sin vinkel, "tiden ute" att väntetiden i öppet läge löpt ut.
- Rundgången går bara åt ett håll. Det finns ingen pil från ÖPPEN direkt till STÄNGD, och det är ett medvetet val: luckan måste passera STÄNGER, där rörelsen sker.
- Saknad pil är också information. Från ÖPPNAR leder ingen pil tillbaka till STÄNGD. Släpper du knapparna medan luckan är på väg upp händer alltså ingenting – och det är precis den sortens fråga diagrammet tvingar dig att svara på innan du skriver koden.
Räkna fallen innan du ritar Med fyra lägen och två knappar finns 4 · 2² = 16 kombinationer av läge och insignal. Diagrammet ovan har fyra pilar, alltså fyra fall där något händer. I de återstående tolv står systemet kvar där det är – vilket också är ett svar, men bara om du tagit ställning till det. Ett bygge går sönder i det fall ingen tänkte på.
En tillståndsvariabel i praktiken
Ett tillstånd är i koden ett heltal med ett namn. Lägena får varsin
const int överst i filen, och allt som ska minnas mellan
varven – tillståndet självt och tidsstämplarna – står också utanför
funktionerna, annars glöms det varje varv.
// Lägena. Prefixet T gör det tydligt i koden att det är ett tillstånd
// och inte en vinkel eller ett pinnummer.
const int T_STANGD = 0;
const int T_OPPNAR = 1;
const int T_OPPEN = 2;
const int T_STANGER = 3;
// Tiderna. unsigned long på allt som rör millis(), utan undantag.
const unsigned long RORELSETID = 600; // ms servot behöver för att nå fram
const unsigned long OPPEN_TID = 3000; // ms luckan står öppen
// Det systemet minns. Utanför funktionerna, alltså kvar mellan varven.
int tillstand = T_STANGD;
unsigned long bytteVid = 0; // när tillståndet senast byttes
unsigned long oppnadesVid = 0; // när luckan nådde öppet läge
Resten av namnen kommer från förra guiden
KNAPP_A_PIN, KNAPP_B_PIN,
VINKEL_STANGD, VINKEL_OPPEN och
luckservo deklareras precis som i den fullständiga skissen i
Villkor i C++, tillsammans
med setup(). Blocket ovan läggs till, det ersätter
ingenting.
Själva logiken blir en kedja av if och
else if, en gren per läge. Det ser långt ut och det ska det
göra: varje gren är ett läge i diagrammet, och det är den likheten som gör
koden möjlig att granska mot ritningen.
void loop() {
unsigned long nu = millis();
bool a_nere = (digitalRead(KNAPP_A_PIN) == LOW); // LOW betyder nedtryckt
bool b_nere = (digitalRead(KNAPP_B_PIN) == LOW);
if (tillstand == T_STANGD) {
if (a_nere && b_nere) {
luckservo.write(VINKEL_OPPEN);
bytteVid = nu;
tillstand = T_OPPNAR;
}
} else if (tillstand == T_OPPNAR) {
if (nu - bytteVid >= RORELSETID) { // servot har hunnit fram
oppnadesVid = nu;
tillstand = T_OPPEN;
}
} else if (tillstand == T_OPPEN) {
if (nu - oppnadesVid >= OPPEN_TID) {
luckservo.write(VINKEL_STANGD);
bytteVid = nu;
tillstand = T_STANGER;
}
} else if (tillstand == T_STANGER) {
if (nu - bytteVid >= RORELSETID) {
tillstand = T_STANGD;
}
}
}
Lägg märke till formen. Varje gren gör två saker och inte fler: den
undersöker om något ska hända, och om det ska det utför den handlingen
och byter tillstånd. Ingen gren pillar på ett annat läges
variabler, och ingen kod utanför kedjan ändrar tillstand.
Håller du den disciplinen kan du läsa koden mot diagrammet med fingret.
Tillstånd och tid hör ihop
Exemplet ovan hade varit omöjligt före förra guiden. Två av de fyra
övergångarna utlöses inte av en knapp utan av att tid gått – att servot
hunnit fram, att luckan stått öppen tillräckligt länge. Utan
millis() hade de krävt delay(), och då hade
knapparna inte lästs under tiden.
Mönstret är alltid detsamma: stämpla tiden när du byter tillstånd, och jämför mot den i det läge du hamnade i. En egen tidsstämpel per sak som mäter tid, precis som förra guiden sa.
Hur många lägen, och hur många bitar
Antalet lägen ska vara litet. Fyra är lagom för bygge 2, och räkningen från den första guiden i området säger varför fyra är ett trevligt tal:
4 lägen ryms i 2 bitar, eftersom 2² = 4
5 lägen kräver 3 bitar, eftersom 2² = 4 är för litet och 2³ = 8 räcker
På ditt kort spelar det ingen roll – en int rymmer alla
lägen du kan tänka dig. Men i en PLC eller i ett datablad ser du samma
räkning på riktigt: tillståndet lagras i ett par bitar i ett register, och
antalet bitar avgör hur många lägen som får plats.
Den viktigare gränsen är den mänskliga. Varje nytt läge du lägger till ökar antalet fall att tänka igenom med antalet insignalkombinationer. Med två knappar kostar ett femte läge fyra nya fall. Med tre knappar kostar det åtta. Håll antalet lägen nere tills bygget fungerar.
Rita ditt eget bygges diagram
Gör det på papper, innan du skriver kod, och gör det i den här ordningen:
- Skriv ner alla lägen bygget kan vara i. Ett ord vardera. Är du osäker på om två är samma läge – kan du svara likadant på "vad händer om någon trycker nu?" i båda, är de samma.
- Rita en ruta per läge och sätt ut vilket som gäller när kortet just startat. Det läget måste vara ofarligt, eftersom kortet kan starta om mitt i vad som helst.
- Dra pilar för varje övergång du vill ha, och skriv villkoret intill varje pil.
- Gå igenom rutorna en i taget och fråga vad som händer vid varje insignalkombination. Saknas ett svar har du hittat ett fall att ta ställning till.
- Skriv koden med en gren per ruta. Diagrammet är då redan din dokumentation, och det ska med i inlämningen.
I industrin och i vardagen
I industrin: en motordriven rullport i ett lager har exakt de fyra lägen som diagrammet ovan visar, och de är valda av säkerhetsskäl snarare än av bekvämlighet. Porten har en fotocell tvärs över öppningen, och den signalen betyder olika saker i olika lägen. I läget ÖPPNAR bryr sig styrningen inte om den alls – porten går bort från allt som kan klämmas. I läget STÄNGER är den avgörande: bryts strålen ska porten omedelbart vända och gå tillbaka till ÖPPNAR. Samma insignal, motsatt betydelse, och det är tillståndet som avgör vilken. Det är inte möjligt att uttrycka i en sanningstabell.
Tiderna ger en känsla för storleksordningen. En port med tre meters öppning som stänger med 0,2 m/s tar
3 m / 0,2 m/s = 15 s
att stänga, och under hela de femton sekunderna är fotocellen den enda som håller koll. Därför är den kopplad direkt till styrningens ingång och inte via något som kan bli upptaget – och därför får porten aldrig sitta i ett program som kan blockera sig självt. Tillstånd och tid utan blockering är samma krav sett från två håll.
I vardagen: ett trafikljus i en korsning har i Sverige fyra lägen i en fast rundgång: röd, röd och gul tillsammans, grön, gul, och tillbaka till röd. Fyra lägen ryms i 2 bitar, och rundgången går bara åt ett håll – det finns ingen övergång från gul till grön, hur mycket trafik som än står och väntar. Den saknade pilen är inte en glömska utan hela säkerhetskonstruktionen.
Det intressanta för dig sitter i tryckknappen för gående. Den ändrar ingenting i samma sekund. Trycket lagras som ett minne – en enda bit – och nästa gång rundgången kommer till rätt ställe tas den biten med i beslutet och nollställs. Det är samma sak som din knapp gör i bygge 2: insignalen tolkas inte där den kommer in, utan där rundgången är beredd att hantera den. Skillnaden mot rullporten är att här är tillståndet till för att hålla ordning på en sekvens, medan portens tillstånd är till för att tolka en givare.
Prova själv
- Rita tillståndsdiagrammet för bygge 1: knappen och spaken, med två lägen. Det tar två minuter och blir din mall för det svårare.
- Bygg en lampa som växlar mellan tänd och släckt vid varje tryck, med
flankdetektering och avstudsning enligt förra guiden. Skriv om koden så
att den använder en namngiven tillståndsvariabel i stället för
bool tand. Beteendet ska vara identiskt – poängen är formen. - Lägg till ett tredje läge: efter tio sekunder tänd släcks lampan av sig själv. Nu behöver du en tidsstämpel som sätts när lampan tänds.
- Koppla in den andra knappen och gör om bygget till fyra lägen enligt
diagrammet ovan, med en lysdiod i stället för servot – byt alltså
luckservo.write(...)motdigitalWrite(...)och låt lampan lysa i lägena ÖPPNAR och ÖPPEN. Kontrollera varje pil för sig. - Prova det som inte står i diagrammet: släpp knapparna mitt i ÖPPNAR. Tryck igen medan systemet står i ÖPPEN. Håll båda nere hela tiden. Skriv ner vad som händer i varje fall och jämför med vad du trodde skulle hända.
- Rita om diagrammet så att det stämmer med det du observerade, inte med det du tänkte. Skillnaden mellan de två ritningarna är den mest användbara raden i din dokumentation.
Kontrollfrågor
Vad skiljer en kombinatorisk krets från ett system med tillstånd?
Den kombinatoriska kretsens utsignal beror bara på insignalerna just nu, och hela dess beteende ryms i en sanningstabell. Ett system med tillstånd svarar olika på samma insignal beroende på vad som hänt tidigare, och behöver därför ett diagram i stället för en tabell.
Hur många fall måste ett diagram med fyra lägen och två knappar ta ställning till?
4 · 2² = 16. Fyra lägen gånger fyra insignalkombinationer. De flesta av dem är "ingenting händer", men det ska vara ett beslut och inte en glömska – det är i det oövervägda fallet bygget går sönder.
Varför måste starttillståndet vara ofarligt?
Därför att kortet kan starta om när som helst: ett strömglapp, ett servo som drar för mycket, någon som råkar trycka på reset. Vid en omstart nollställs alla variabler, och systemet hamnar i det läge du skrivit som utgångsvärde. Är det läget "luckan öppen" öppnar sig luckan varje gång strömmen blinkar till.
Din tillståndsvariabel står deklarerad inuti loop().
Vad ser du?
Systemet står kvar i sitt starttillstånd för alltid. Variabeln skapas på nytt varje varv och sätts till utgångsvärdet, så tillståndsbytet du gjorde förra varvet är borta. Symtomet är att första övergången verkar ske om och om igen, eller att ingenting alls verkar hända.
Hur många bitar behövs för sex lägen?
Tre. 2² = 4 räcker inte för sex lägen, 2³ = 8 gör det, och två av de
åtta kombinationerna blir då oanvända. På ditt kort spelar det ingen
roll – tillståndet ligger i en int med gott om plats – men
räkningen är densamma som i den första guiden i området.
Fotocellen i rullporten ger samma signal i ÖPPNAR och i STÄNGER. Varför gör porten olika saker?
Därför att betydelsen av en insignal avgörs av tillståndet. I ÖPPNAR rör sig porten bort från det som kan klämmas, och signalen är ointressant. I STÄNGER rör den sig mot, och samma signal måste då vända porten. Ett system utan tillstånd kan inte skilja de två fallen åt, eftersom insignalen är identisk.
Vanliga fel
- Tillståndsvariabeln deklareras inuti
loop(). Systemet kommer aldrig ur sitt första läge. Den ska stå utanför funktionerna, som räknaren i språkreferensens exempel. - Två grenar kan byta till samma läge samtidigt. Koden blir oförutsägbar och beter sig olika varje gång. En gren per läge, och bara den grenen ändrar tillståndet.
- En handling ligger utanför sitt läge. Ett
write()som körs varje varv oavsett tillstånd överröstar det grenarna gör. Allt som ska ske vid ett byte ska ske i själva bytet. - Diagrammet ritas efteråt. Då ritar du det du kodade, inte det du menade, och det fall du missade saknas i båda. Rita först.
- Ett läge saknar väg ut. Systemet fastnar där, och det ser ut som att kortet hängt sig. Gå igenom rutorna och kontrollera att varje ruta har minst en pil ut.
- Tillståndet byts men tidsstämpeln sätts inte. Nästa läge jämför mot en gammal stämpel och löper ut omedelbart, eller aldrig. Stämpla i samma block som du byter.
- Starttillståndet är det öppna. Vid varje omstart öppnar sig bygget. Utgångsvärdet på tillståndsvariabeln är en säkerhetsfråga och inte en smaksak.
- Alltför många lägen. Nio lägen och två knappar ger 36 fall att tänka igenom. Slå ihop lägen som svarar likadant på alla insignaler – de är samma läge.
Vidare
Kapitlen Villkor och Tid och millis() i språkreferensen hör båda hit: tillståndsmaskinen är ingenting annat än villkor och tidsstämplar satta i en form som går att granska. Typer och variabler förklarar varför variabeln måste stå utanför funktionerna för att minnas något.
Nästa guide är I industrin och i vardagen. Där sätts allt du byggt hittills mot två verkliga spärrar – en industripress och en mikrovågsugn – och där kravställs bygge 2.