← Kursens material Tillstånd · område 3 Område 3

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

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.

Ett tillståndsdiagram med fyra rutor placerade i en fyrkant och pilar mellan dem i en rundgång medurs. Uppe till vänster rutan STÄNGD, uppe till höger ÖPPNAR, nere till höger ÖPPEN och nere till vänster STÄNGER. Pilen från STÄNGD till ÖPPNAR är märkt båda ned, pilen från ÖPPNAR till ÖPPEN är märkt i läge, pilen från ÖPPEN till STÄNGER är märkt tiden ute, och pilen från STÄNGER tillbaka till STÄNGD är märkt i läge. STÄNGD ÖPPNAR ÖPPEN STÄNGER båda ned i läge tiden ute i läge

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:

  1. 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.
  2. 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.
  3. Dra pilar för varje övergång du vill ha, och skriv villkoret intill varje pil.
  4. 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.
  5. 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

  1. 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.
  2. 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.
  3. 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.
  4. 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(...) mot digitalWrite(...) och låt lampan lysa i lägena ÖPPNAR och ÖPPEN. Kontrollera varje pil för sig.
  5. 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.
  6. 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

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.