Statemachine in C

@Henk Hoeveel geheugen de compiler nodig heeft is (zeker voor embedded!) eigenlijk nooit meer relevant.

En die teipvauten ook niet, iedere fatsoenlijke IDE (ook vi) kan dat dynamisch voor je oplossen.

Wat ervoor zorgt dat jij je aandacht kunt besteden aan wat echt belangrijk is: de juiste code.

@hardbass: Ik blijf een "oplossing zoekt probleem" gevoel houden bij die state-machine met functies. Het is qua concept veel lastiger dan een switch en je kunt lastig een overzicht krijgen welke states en state-overgangen er zijn.

Een netjes geprogrammeerde switch is ook verdeelt in kleine stukken code die allemaal hun eigen specifieke doel hebben.
Ze staan alleen allemaal in dezelfde scope, maar dat is geen wezenlijk probleem als je anders de hele state in een (pointer naar)struct meegeeft.

Hoe ingewikkeld moet je state-machine worden zodat een switch niet meer behapbaar worden? En is je state-machine dan uberhaupt nog wel de geschikte oplossing?

Bedenk dat je prima state-specifieke code in een functie kunt zetten die je vanaf de case voor die state aanroept.

Ik vind het een heel mooi initiatief.
Het zou nog mooier worden als de code stap voor stap uitgelegd wordt.
Mijn mening is dat veel mensen het noorden kwijt raken bij uitdrukkingen als
stateMachine->activeState = initialState;
met name het kortere pijltje notatie bij gebruik van pointers in dit geval.
Ik weet het, dit wordt dan een cursus C ; C++.
Maar eigenlijk vindt je bitter weinig verstaanbare uitleg daar omtrent.

Arco

Special Member

Ik gebruik ook gewoon een switch, heel overzichtelijk als states geen al te complexe handelingen benodigen.
(bij heel erg lange switch states maak ik er wel een aparte functie van anders wordt het een onoverzichtelijke boel)

Arco - "Simplicity is a prerequisite for reliability" - hard-, firm-, en software ontwikkeling: www.arcovox.com

Luister maar niet naar het gezijk van Henk en Blurp.

Ik gebruik zelf ook een funktie pointer om de huidige state te onthouden. Door van elke state een aparte funktie te maken breng je meer struktuur aan. Via een pointer naar een funktie springen is ook sneller dan door een "switch()" heen lopen, ook al is dat verschil marignaal.

Met "arduino" (Geen hoofdletter van mij) ben je al (ongeveer) C++ aan het schijven. In mijn eigen programma heb ik een class gemaakt van de hele statemachine. Daarmee houd je dingen nog netter bij elkaar. Zowel de states, als de context worden dan een onderdeel van de class.

Die "delay( 10)" waar blurp over zeurt is natuurlijk ook flauwekul. In je statemachine zelf gebruik je al een betere timer om een wacht tijd te genereren. Die "delay( 10) is ook gemakkelijk te vervangen door een sleep(). Mits eerst goed geinitialiseerd natuurlijk, slaapt je AVR dan tot de volgende timer tick. Maar verder is het maar een voorbeeldje. Je kunt deze delay() ook weglaten.

Een stuktuur zoals dit vind ik een goed compromis tussen flexibiliteit, overzicht en leesbaarheid. Met een switch, moet je dan 20 blz lager gaan kijken of een haakje nog op de goede plaats staat. Yuch!

Als een state machine complexer wordt (Meer dan 10 of 20 states ofzo), dan worden randcondities en bugs bij overgangen naar een andere state steeds moeilijker om met de hand bij te houden. Tegen die tijd is het vermoedelijk beter om gespecialiseerde software te gebruiken die de code van de state machine genereert.

Ik ben het niet eens met het

if (ctx->isFirstRun)

stuk. In jouw struktuur gaat dit bijna zeker in elke state terug komen. Denk eens aan de volgende stuktuur:

1). Als een state wordt uitgevoert, dan doet hij gelijk zijn ding (Led Aan/ uit)
2). Je zet een variable voor een delay in de state context, en zet daar een waarde in.
3). Je verandert de actieve state meteen naar een "delay" state. Je verantert hier de funktie pointer zelf, dus niet de ctx->nextState.
4). Netjes dat je voor je delay een tijdverschil uitrekent :) Deze code kun je (1 keer) in je "delay" state zetten. Als de delay is afgelopen wordt dan ctx->nextState in de funktie pointer gezet.

Op deze manier kun je de "delay state" gebruiken om alle state overgangen te timen. Als het voor bepaalde overgangen niet goed werkt, dan kun je voor die gevallen wat anders maken.

Op maandag 11 november 2024 16:34:56 schreef High met Henk:
en @dino: je moet ooke erst uitschrijven hoe je states samehnagen, anders wordt het een zooitje.

Voor mij ziet hij er compleet uit.
Ik begin altijd met een stuk 'Nederlandse' tekst. Dat is de specificatie.
Aan de hand van de specificatie maak je het diagram volgens het voorbeeld van @dino7. Dat is het bolletjesdiagram of oneerbiedig genoemd 'ballendiagram'.
Aan de hand van het ballendiagram schrijf je de code en die laat je lopen.
Als het niet goed is loopt op een bepaald moment je machine vast. Dan kijk je in welke toestand de 'ballen' (rondjes) en de 'vlaggen' (de lijnen tussen de rondjes) verkeren. Dan ga je heel goed kijken waarom hij niet deze toestand kan verlaten. Je herstelt dan de fout en indien nodig pas je je specificatie aan.
Het bolletjesdiagram is het skelet van je programma en is geen afbeelding van je programma, dit in tegenstelling tot een flowdiagram.
En uiteraard is het bolletjesdiagram taalonafhankelijk.

Op maandag 11 november 2024 17:29:40 schreef Kortsluiting_Online:
Luister maar niet naar het gezijk van Henk en Blurp.

Inderdaad.
Een switch / case constructie wordt in vrijwel alle gevallen als die wat groter wordt een grote bende.

@hardbass: Dit is een redelijk klassieke manier om netjes een statemachine te maken. Dus ben ik het er zeker mee eens.

Waar ik wel een bloedhekel aan heb is alle sub-functies boven aan zetten. Dus "bottom up" programmeren. (Ik noem het lamliederig programmeren)
Ik begin altijd met de main code boven aan dan naar onder werken met je functies, de belangrijkste eerst tot de onderste laag onder in. Gelukkig heb je wel de prototypes / aka forward declaraties er toch in staan maar de functies staan nog steeds boven de main().

Verder zou ik de statehandler zelf in een aparte file zetten. Dan is het ook duidelijk en gescheiden wat tot je "business" logic hoort en wat de onafhankelijke state handler is. Maar ja in de arduino commiunity rommelt men maar wat aan, dat is ook de reden dat ik zoiets vrijwel nooit gebruik.

Een nog mooiere method is ook echt C++ gebruiken. Een class maken per state afgeleid van een base-class. Met een entry/exit/run/test-condition state etc. as je toch een arduino hebt, want dat is hoofdzakelijk C++ wat onder water gebruikt wordt.

@dino7 : Ja dat is het eerste wat je moet doen, een zgn state-diagram maken, anders kun je niks fatsoenlijks maken.

1-st law of Henri: De wet van behoud van ellende. 2-nd law of Henri: Ellende komt nooit alleen.
picsels

Golden Member

Op maandag 11 november 2024 19:35:42 schreef henri62:
[...] Inderdaad.
Een switch / case constructie wordt in vrijwel alle gevallen als die wat groter wordt een grote bende.

@hardbass: Dit is een redelijk klassieke manier om netjes een statemachine te maken. Dus ben ik het er zeker mee eens.

Leuke invalshoek voor een state machine!

Wat ik zelf meestal doe met wachttijden is de nieuwe tijd één keer berekenen en daarmee vergelijken.
Dan verlies je niet steeds tijd, fwiw, met die berekening.
Bijv:


...
exitTime = millis() + 5000;
...
if(millis() > exitTime)...

Alleen jammer van die openingsaccolade op dezelfde regel <kuch>flamewar<kuch> ;)

Volgend project: funcgen met ad9833 afmaken, dan...

Op maandag 11 november 2024 16:15:57 schreef blurp:
Een kale integer variabele als state. Over Switch vs. state functies kun je discussieren (ik vind een goed geschreven switch van 1000 regels uitstekend leesbaar), maar die state moet echt een enum zijn.

Dat hangt er van af. Als je een algemene "state machine interpreter" maakt, met een tabel van condities, nextstate en "action-to-perform" dan moet het een integer zijn.

Maar als je de states opsomt en uitwerkt in je code zonder een statemachine-tabel-interpreter te schrijven, dan heb je gelijk.

@picsels:
ALs je dan ook nog iets doet als


nextcheck = nextcheck + CHECKTIME_MS;

dan krijg je niet dat de check steeds iedere zeg 5.1 sec gebeurt terwijl je 5sec had gewild. (hetgeen je krijgt als de

nextcheck=millis () + CHECKTIME_MS; 

achter de "doe-de-check" staat en de check dus 0.1 sec in beslagneemt.

[Bericht gewijzigd door rew op maandag 11 november 2024 21:42:56 (22%)

four NANDS do make a NOR . Kijk ook eens in onze shop: http://www.bitwizard.nl/shop/

...
exitTime = millis() + 5000;
...
if(millis() > exitTime)...

Ook jammer dat dit niet werkt igv overflow. 8)7

Op maandag 11 november 2024 22:05:18 schreef deKees:
Ook jammer dat dit niet werkt igv overflow. 8)7

Een kniesoor die daar op let. Meestal gaat het goed. :+

Paulinha_B

Honourable Member

Meestal gaat het goed

Wie dat aanvaardt kan beter knollen gaan planten. Hardware kan al eens misgaan, want tegen de fysica kan niemand op; maar software moet 100% waterdicht zijn.

(maar het emoticon ging niet ongemerkt voorbij ;) )

[Bericht gewijzigd door Paulinha_B op dinsdag 12 november 2024 00:34:02 (13%)

Ik loste dit soort problemen op door, indien nodig, een offset toe te voegen.
Het is niks anders dat klokrekenen.
Vb: Wat is het kleinste tijdsverschil tussen 2 minuten voor het uur en 2 minuten na het uur?
Oplossing: Hier heb je te maken met een minutenoverflow.
Bij alle twee de tijden tel je 30 seconde erbij op en als er een nieuwe tijd is die groter is dan 60 seconde, dan trek je er 60 seconde vanaf. Met een beetje mazzel gaat dat aftrekken 'vanzelf' ivm de overflow.
t1 = 58s -> t1 = 58s + 30s = 88s -> t1 = 88s - 60s = 28s.
t2 = 2s -> t2 = 2s + 30s = 32s.
t2 - t1 = 32s - 28s = 4s
Soortgelijke berekening kan je ook met millis() doen.

Uit arduino documentatie:

millis() will wrap around to 0 after about 49 days

Als je binnen de 49 dagen je arduino uit- en aanzet, dan heb je geen last van overflow.

Dat kan, maar waarom zou je.

Als je eerst het verschil uitrekent dan gaat het altijd goed, ook igv overflow. Mits alle variabelen hetzelfde type hebben, in dit geval 'unsigned long'


  if ((millis() - StartTime) > Delay)
  {  StartTime += Delay;
     ... 

Ongeveer 49 dagen, inderdaad. Om precies te zijn 4294967296 milliseconden.

[Bericht gewijzigd door deKees op dinsdag 12 november 2024 01:34:20 (12%)

@deKees,
Ik snap je betoog en je korte programma niet, maar gevoelsmatig zal je toch hinder van je overflow hebben.

Gaat StartTime ongeveer naar 49 dagen toe en blijft het daar uiteindelijk staan?

[Bericht gewijzigd door ohm pi op dinsdag 12 november 2024 01:51:40 (27%)

Op maandag 11 november 2024 21:39:22 schreef rew:
[...]Dat hangt er van af. Als je een algemene "state machine interpreter" maakt, met een tabel van condities, nextstate en "action-to-perform" dan moet het een integer zijn.

Alleen als je state een rechtstreekse index in de tabel moet zijn, EN het niet mogelijk is om dat mogelijk te maken door de enum (onder water gewoon een int) zelf te assignen.

Natuurlijk kun je een situatie hebben waarin je state machine letterlijk bestaat uit enkel die tabel en de algemene opzoek/uitvoer functie. Dan refereer je elders in de code ook nooit aan de state, en maakt de leesbaarheid niet veel meer uit.

Op maandag 11 november 2024 17:29:40 schreef Kortsluiting_Online:
Via een pointer naar een funktie springen is ook sneller dan door een "switch()" heen lopen, ook al is dat verschil marignaal.

Premature optimization is the root of all evil.

Veel van de statemachines met functiepointer die ik gezien heb zijn een ongelofelijke ratjetoe van verwijzingen, gerelateerde code in verschillende source-files, en met een beetje pech ook nog een gruwelijke lasagne aan class-inheritance.

Dan is een switch van 10 paginas al snel overzichterlijker.

Dat betekend niet dat een switch altijd beter is, maar zoals altijd: kies de juiste methode voor je probleem, en laat je niet gek maken door mensen die zeggen hoe het heurt.

Met een switch, moet je dan 20 blz lager gaan kijken of een haakje nog op de goede plaats staat. Yuch!

Daar heb je een IDE voor.

Als een state machine complexer wordt (Meer dan 10 of 20 states ofzo), dan worden randcondities en bugs bij overgangen naar een andere state steeds moeilijker om met de hand bij te houden. Tegen die tijd is het vermoedelijk beter om gespecialiseerde software te gebruiken die de code van de state machine genereert.

Absoluut. Of je ontwerp te heroverwegen, of je wel zoveel states nodig hebt en of je het niet op (kunt) moet delen.

Stijnos

Golden Member

ik vind de benadering van je statemachine framework niet verkeerd.
Ik geef eerlijk toe dat ik voor het gemak ook vaak een switch case gebruik, maar dan zijn het vaak pure sequentiele statemachientjes van hooguit een state of 8.
Je ziet toch vaak dat je in elke state wat zelfde checks wil hebben, zoals timeout een first time etc.
Dat kun je dus prima generiek afhandelen. Met losse functies voor state focus je je inderdaad wel meer per state.
Dit soort oplossingen leent zich erg goed als je heel vaak statemachines implementeerd die allemaal in dit framework passen. je hoeft je dan na eenmalig goed testen niet meer te focussen op de functionaliteit of het statemachine framework werkt. Maar bekijk je het per case.
Ik heb ook soort gelijke implementaties gezien, maar dan nog een stap verder, en rew haalde die ook al aan.
Dan heb je echt een tabel, met state functie pointers, alle events voor die state als functie pointers en relaties met andere states.
Op deze manier kun je wel een stuk eenvoudiger een wat generiekere unittest maken om je statemachine door te testen, waarbij het ook belangrijk is niet de echte flow van je statemachine te doorlopen om een bepaalde state te testen.
Ik heb niet inhoudelijk gekeken naar je implementatie, maar vind het altijd wel mooi dat mensen proberen iets wat generieker te maken. Dat is inderdaad in eerste instantie niet altijd beter leesbaar of overzichtelijker, maar het kan wel rbooster, minder fout gevoelig en beter onderhoudbaar zijn

Haha, er barst een leuke discussie los, volledig volgens verwachting en precies de bedoeling. :) Interessant om te zien hoe anderen denken en werken. Dat helpt bij het vinden van oplossingen voor de nadelen van deze code.

Misschien goed om te vermelden, dit is natuurlijk voor een knipperledje totaal overbodig. Voor kleine statemachines is een switch case prima.

Complexiteit:
Ja, deze implementatie gebruikt pointers, waaronder functiepointers. Persoonlijk vind ik dat dit bij de basiskennis van een C-programmeur hoort. De pijlnotatie en het werken met functiepointers zijn nu eenmaal onderdeel van de taal. Zoals ik in mijn startpost al aangaf, kun je met helperfuncties de complexiteit verder abstraheren. Denk bijvoorbeeld aan een helperfunctie `SetNextState(context, MyState);`, die het instellen van de volgende state makkelijker maakt zonder direct met functiepointers te werken.

isFirstRun:
@Kortsluiting_Online: Ik begrijp je voorstel als alternatief voor `isFirstRun` niet helemaal. Zou je een voorbeeld kunnen geven om dit verder toe te lichten?

Structuur en Volgorde van Code:
Ik ben het eens met de opmerkingen over het opsplitsen van de code. Normaal gesproken houd ik de `main` klein. In dit specifieke voorbeeld zou ik de code opdelen in drie delen:
State Machine Code: Een bestand met de logica van de state machine zelf.
Main Code: Een bestand voor de `main` functie.
State Logic: Een bestand voor de verschillende states en hun specifieke logica.

TimeInState:
De manier waarop `TimeInState` is opgezet voorkomt overflow problemen, zoals deKees ook al aangeeft. Omdat de waarden unsigned zijn, wordt de berekening bij overflow automatisch gecorrigeerd. Dit is robuuster dan werken met absolute tijdsverschillen. Bovendien geeft het je flexibiliteit om bijvoorbeeld met echte timestamps te werken (zoals `time()`), wat handig kan zijn bij logging of bij andere applicaties. Als je enkel bijhoudt hoelang je al in een state zit, verlies je de precieze starttijd en daarmee een stuk detail. Met de starttijd kan je de rest berekenen.

C++:
Op mijn werk programmeren we in C++, en dat biedt extra mogelijkheden om dit concept mooier te maken. Ik zou bijvoorbeeld kiezen voor een `StateMachine` class, waarin de states en context beter gestructureerd zijn via templates. De states zelf zouden ook classes kunnen zijn, al brengt dat extra overhead met zich mee. Voor complexe state machines kan dit nuttig zijn. In dit voorbeeld houd ik het echter bewust simpel om het concept goed over te brengen.

Een extra voordeel in C++: je kunt variabelen in de context private maken en enkel toegankelijk via functies zoals `SetNextState(MyState);`. Dat houdt de context en de statemachine interface schoon.

Uitleg:
Ik zal inderdaad nog een korte uitleg toevoegen aan de startpost om de structuur en werkwijze van de code toe te lichten.

PE2BAS

Op dinsdag 12 november 2024 01:44:26 schreef ohm pi:
@deKees,
Ik snap je betoog en je korte programma niet, maar gevoelsmatig zal je toch hinder van je overflow hebben.

Dat klopt... Maar je gevoel heeft het verkeerd. Om de getallen behapbaar te houden, doen we even alsof het in 8 bits is. Het verschil MOET je in een signed variabele zetten.

Stel het is nu tijdstip 5, we willen een timeout over 20 tijdstapjes. We rekenen uit eindtijd is 25. Doe je dan op t=10 je huidigetijd-einddtijd dan is dat 10-25 = -15... Nog geen tijd.
Wordt het t=26 dan krijg je 26-1 = 1: Het is tijd om iets te doen.

Dit werkt prima tot we op t=245 de timeout willen zetten... eindtijd wordt 245+20 = 265, maar dat past niet in 8 bits je krijgt een wraparound en eindtijd is 9.

Nu check je op t=250 en rekent uit: 250 - 9 = 241 = -15 in signed-8 bit. Nog geen tijd: niets doen.

Op t= 260 = 4 na de wraparound... krijg je 4-9 = -5 nog steeds VOOR de timeout -> Niets doen.

Op t=266=10 krijg je 10-9 = 1: positief: wel wat doen!

Ik raad aan om zowel je get_current_time() functie als de timeouts in signed te doen. Dan is het verschil van nature direct ook signed.

Je hebt er niets aan als je

if ((millis () - endtime) > 0) 

gaat doen. Omdat je nu nul NIET meeneemt krijg je geen waarschuwing van de compiler. Als je >= had geschreven had de compiler gezien: Dat is altijd waar als het om een unsigned gaat.

Als je dan vindt dat de belangrijke wraparounds bij 127 -> -128 gebeuren, mij best. Ook daar blijft alles werken, maar ik heb geen zin om dat nu te gaan uitschrijven. Oefening voor de lezer. :-)

four NANDS do make a NOR . Kijk ook eens in onze shop: http://www.bitwizard.nl/shop/

@rew: Bijna goed.

Jouw voorbeeld gaat uit van een StartTijd in de toekomst, dus wanneer je de aktie wilt uitvoeren, en dat levert inderdaad een negatief getal op als het nog geen tijd is. Maar dat is lastig testen want millis() is unsigned en is dus altijd positief.

Mijn voorbeeld gaat uit van een StartTime in het verleden, dus wanneer je begint met de delay.


   if ((millis() - StartTime) > Delay)
   {  StartTime += Delay;

StartTime is een moment in het verleden. En millis() loopt steeds door. Dus (millis() - StartTime) is altijd positief. En je start de aktie (en zet een nieuwe StartTijd) als het verschil groot genoeg is. De getallen hoeven niet signed te zijn. De enige eis is dat alle getallen even groot zijn, want als millis() bijv een 16-bit getal geeft en StartTime is 32-bit dan gaat het mis

Veel mensen zetten StartTime dmv millis()


  if ((millis() - StartTime) > Delay)
  {  StartTime - millis();

Dat werkt ook, maar geeft extra vertraging als je de test te laat uitvoert. Bijv als je systeem zo traag is dat je de controle maar 1 keer per meerdere milliseconden kunt uitvoeren. Dat is soms een probleem, niet altijd.

Rekenvoorbeeld:

Stel StartTime is unsigned long (32 bit). En de waarde is 0xFFFFFFFE (dus tegen overflow aan.
En je wilt een actie starten telkens na 5 milliseconden (Delay = 5).


Millis()      StartTime    Millis() - StartTime   
0xFFFFFFFE    0xFFFFFFFE   0x00000000             Nog niet
0xFFFFFFFF    0xFFFFFFFE   0x00000001             Nog niet
0x00000000    0xFFFFFFFE   0x00000002             Nog niet
0x00000001    0xFFFFFFFE   0x00000003             Nog niet
0x00000002    0xFFFFFFFE   0x00000004             Nog niet
0x00000003    0xFFFFFFFE   0x00000005             Nu wel
Zet nieuwe StartTime (StartTijd + Delay)
0x00000003    0x00000003   0x00000000             Nog niet
0x00000004    0x00000003   0x00000001             Nog niet
enz ...

PS Dat het goed blijft gaan ook igv overflow is 'by design'. Dat is inherent aan het gebruik van "2's complement" binaire notatie.

Mijn ervaring is dat wanneer je over zoiets nadenkt en dat uitwerkt met de diverse functies dat het resultaat wat complex wordt en dat anderen juist weer afschrikt. Vaak geeft dit een hoop gezeik of gezijk ;)

Ik wil hem nog wel een stukje complexer maken.

Ik werk veel met een statemachine in een SQL database. Dat is voor embedded misschien wat overdreven, maar ipv in "usercode" zoals hierboven zou je dus states in een array kunnen definieren en zo kunnen aanpassen zonder opnieuw programmeren. Bijvoorbeeld over-the-air of met een SD kaartje.

Ik doe een poging, in onderstaande array kan dus vrij makkelijk 'in memory' wijzingen aanbrengen zoals het overslaan van de betaling of bepaalde producten alleen op bepaalde tijden toestaan:


{
  "states": {
    "state": [
      {
        "id": "WaitPayment",
        "OnSuccessNextState": "ProductSelection",
        "OnFailNextState": "WaitPayment",
        "prereq": "time>8:00 and time < 18:00"
        "action": "read_payment_terminal"
      },
      {
        "id": "ProductSelection",
        "OnSuccessNextState": "MakeProduct",
        "OnFailNextState": "ProductSelection",
        "action": "$product=read_selection_terminal"
      },
      {
        "id": "MakeProduct",
        "OnSuccessNextState": "WaitForDrops",
        "OnFailNextState": "WaitPayment",
        "action": "start_product($product)"
      },
      {
        "id": "WaitForDrops",
        "OnSuccessNextState": "DoneOnDisplay",
        "OnFailNextState": "WaitForDrops",
        "action": "checkTimer($timer1,3s)"
      },   .....................
    ]
  }
}

edit: ik heb die prereq met tijden van een ander systeem, maar ik bedenk me dat je net zo goed een state "checkTime" zou kunnen maken.

Op die manier kan je de software zeer flexibel maken via configuratie. Ik heb ook zoon tool gemaakt voor het werk die op zoon soort manier te configureren is. Wel een nadeel, je kan het ook 'kapot' configureren. Daar moet je dan in de software weer rekening mee houden. Het configureren is ook niet voor iedereen weggelegd. Andere optie is iets als lua ondersteunen, maar dat is allemaal weer afhankelijk van de toepassing.

Voor zij die het leuk vinden, dit idee is hier ontstaan. Op deze site staan veel meer leuke design patterns.
https://refactoring.guru/design-patterns

PE2BAS

Op dinsdag 12 november 2024 11:43:49 schreef deKees:

Rekenvoorbeeld:
....
PS Dat het goed blijft gaan ook igv overflow is 'by design'. Dat is inherent aan het gebruik van "2's complement" binaire notatie.

Nu snap ik het!
In principe komt dit ook overeen met @rew's eerdere uitleg.
@rew en @deKees, dank voor de uitleg! _/-\o_

Over die timeout. De timer value moet wel unsigned zijn waarmee je begint. Het beste is een macro gebruiken hiervoor zoals in de linux kernel waar de wrapping weggewerkt wordt waardoor dit altijd goed gaat mits de timeout < 1/2 * resolutie van de timer variable.
Zie deze discussie: https://stackoverflow.com/questions/8206762/how-does-linux-handle-over…

1-st law of Henri: De wet van behoud van ellende. 2-nd law of Henri: Ellende komt nooit alleen.

In mijn beleving komt het weinig voor dat verschillende state machines gemeenschappelijke behoeften hebben. Meestal draaien de state machines volledig onafhankelijk van elkaar.

Bijv:


void  loop()
{  Knipperlicht.Run();
   ADC.Run();
   TempControl.Run();
   SerialPort.Run();
}

Dus voor iedere state machine maak ik dan een object zodat je de bijbehorende data mee kunt nemen. En het enige dat de machines gemeenschappelijk hebben is dat ze allemaal een Run() functie hebben. De implementatie is dan voor elke functie verschillend. Het Knipperlicht is bijv timer gestuurd, de ADC reageert op het ADC status register, de SerialPort reageert op inkomende data.

Op dinsdag 12 november 2024 19:55:57 schreef henri62:
Over die timeout. De timer value moet wel unsigned zijn waarmee je begint. Het beste is een macro gebruiken hiervoor zoals in de linux kernel waar de wrapping weggewerkt wordt waardoor dit altijd goed gaat mits de timeout < 1/2 * resolutie van de timer variable.
Zie deze discussie: https://stackoverflow.com/questions/8206762/how-does-linux-handle-over…

Strict genomen klopt dit niet. De timer value hoeft niet signed of unsigned te zijn. Feitelijk maakt dat geen verschil. Waar het om gaat is dat alle getallen bij dezelfde waarde over de kop gaan.

Je berekent eerst het verschil tussen 'nu' en de 'StartTime' en dat levert altijd een klein positief getal op. En de af te tellen delay is ook een klein positief getal. Of het type dan al dan niet signed is maakt geen verschil. Behalve als je een timeout value gaat zetten van meer dan 25 dagen. Het verschil wordt dan ooit groter dan 0x8000000 en dan maakt signed/unsigned weer wel verschil.

Die Macro van linux forceert een timeout berekening door eerst de verschiltijd te berekenen. Zonder die macro loop je het risico om het fout te doen.