Author Topic: How to properly structure code with multiple states and inputs  (Read 2390 times)

0 Members and 1 Guest are viewing this topic.

Offline craigc952Topic starter

  • Newbie
  • Posts: 3
  • Country: gb
How to properly structure code with multiple states and inputs
« on: January 14, 2024, 12:47:42 pm »
I'm making a personal project of an alarm clock using a Raspberry Pi Pico and the C/C++ SDK with 4 buttons connected to the Pico to use for setting the time and alarm etc. The software I have now works, but it seems very messy, for example my main loop is a few hundred lines long as I need to check each input and respond to it differently depending on the current state (ie, whether I'm updating the time or alarm). Additionally I just have a global class instance for each button to store whether the button has been pressed and how long for.

Is there a better way to structure the code? The code I have seems to go against every "rule" I read about avoiding global variables and long functions, but I'm not sure how I would go about improving that.

This is just for a personal project, so ultimately it doesn't really matter, but I want to learn the proper way to write embedded code as I'm an electronics student and I'm interested in working with embedded hard/software so want to get a good understanding. Thanks
 

Offline tggzzz

  • Super Contributor
  • ***
  • Posts: 19511
  • Country: gb
  • Numbers, not adjectives
    • Having fun doing more, with less
Re: How to properly structure code with multiple states and inputs
« Reply #1 on: January 14, 2024, 01:13:56 pm »
I'm making a personal project of an alarm clock using a Raspberry Pi Pico and the C/C++ SDK with 4 buttons connected to the Pico to use for setting the time and alarm etc. The software I have now works, but it seems very messy, for example my main loop is a few hundred lines long as I need to check each input and respond to it differently depending on the current state (ie, whether I'm updating the time or alarm). Additionally I just have a global class instance for each button to store whether the button has been pressed and how long for.

Is there a better way to structure the code? The code I have seems to go against every "rule" I read about avoiding global variables and long functions, but I'm not sure how I would go about improving that.

This is just for a personal project, so ultimately it doesn't really matter, but I want to learn the proper way to write embedded code as I'm an electronics student and I'm interested in working with embedded hard/software so want to get a good understanding. Thanks

Welcome to the forum. it is good to see someone observing and thinking about improvements.

I note in passing that beginners think about "how thing will work", whereas experienced professionals think about "how things can fail and what to do about failures". Good FSM design and implementation is very valuable for the latter :)

There are several "design patterns" for this, each with their own advantages and disadvantages.

One I've used for simple FSMs is to have a 2-dimensional table where one dimension is the current state and the other is the input event. Each table entry is a pointer to a function that is executed when an new event occurs in the current state. The function is responsible for executing the actions and specifying the next state. Both "current state" and "events" are implemented as integers used as indexes into the table.

That has the advantages that each event+state action has to be explicitly specified, so it is easy to see that all combintions have been covered. The disadvantage is that it cannot directly express a hierarchy of states, thus complicating some designs.

Another design pattern is more suitable for complex and evolving FSMs, and is described at https://www.gofpattern.com/behavioral/patterns/state-pattern.php and related design patterns. Each state is a class, each event is a method, the body of the method is the actions required. All classes implement the methods (i.e. events) that must be dealt with, and other superclasses implement  methods for events that should not occur in normal operation or which are applicable to many states (e.g. a "PSU fault" event in any state should cause the "clean shutdown" actions and a transition to a "fault state" where all events are ignored). "Current state" is implemented as a singleton instance of the relevant class.

That technique has the advantage of making it trivial to encode a terse record of each event that occurred (and when), plus the state transition trajectory. Those can be very valuable during commissioning and operation, since they aid understanding where your code needs improving and/or demonstrating to other companies that their code needs improving. The latter is a good defence against lawyers :)
« Last Edit: January 14, 2024, 01:20:14 pm by tggzzz »
There are lies, damned lies, statistics - and ADC/DAC specs.
Glider pilot's aphorism: "there is no substitute for span". Retort: "There is a substitute: skill+imagination. But you can buy span".
Having fun doing more, with less
 
The following users thanked this post: mikerj, tooki, IOsetting, craigc952

Offline craigc952Topic starter

  • Newbie
  • Posts: 3
  • Country: gb
Re: How to properly structure code with multiple states and inputs
« Reply #2 on: January 14, 2024, 03:14:14 pm »
Welcome to the forum. it is good to see someone observing and thinking about improvements.

I note in passing that beginners think about "how thing will work", whereas experienced professionals think about "how things can fail and what to do about failures". Good FSM design and implementation is very valuable for the latter :)

There are several "design patterns" for this, each with their own advantages and disadvantages.

One I've used for simple FSMs is to have a 2-dimensional table where one dimension is the current state and the other is the input event. Each table entry is a pointer to a function that is executed when an new event occurs in the current state. The function is responsible for executing the actions and specifying the next state. Both "current state" and "events" are implemented as integers used as indexes into the table.

That has the advantages that each event+state action has to be explicitly specified, so it is easy to see that all combintions have been covered. The disadvantage is that it cannot directly express a hierarchy of states, thus complicating some designs.

Another design pattern is more suitable for complex and evolving FSMs, and is described at https://www.gofpattern.com/behavioral/patterns/state-pattern.php and related design patterns. Each state is a class, each event is a method, the body of the method is the actions required. All classes implement the methods (i.e. events) that must be dealt with, and other superclasses implement  methods for events that should not occur in normal operation or which are applicable to many states (e.g. a "PSU fault" event in any state should cause the "clean shutdown" actions and a transition to a "fault state" where all events are ignored). "Current state" is implemented as a singleton instance of the relevant class.

That technique has the advantage of making it trivial to encode a terse record of each event that occurred (and when), plus the state transition trajectory. Those can be very valuable during commissioning and operation, since they aid understanding where your code needs improving and/or demonstrating to other companies that their code needs improving. The latter is a good defence against lawyers :)

Thank you very much for the detailed reply! This seems like exactly what I need and I'll have a good look into it  :)
 

Online nctnico

  • Super Contributor
  • ***
  • Posts: 26907
  • Country: nl
    • NCT Developments
Re: How to properly structure code with multiple states and inputs
« Reply #3 on: January 14, 2024, 04:39:05 pm »
When dealing with state machines also keep in mind that it can be helpfull to create sub-state machines that performs a certain task. This de-clutters the primary state machine.
There are small lies, big lies and then there is what is on the screen of your oscilloscope.
 
The following users thanked this post: Psi, craigc952

Offline tggzzz

  • Super Contributor
  • ***
  • Posts: 19511
  • Country: gb
  • Numbers, not adjectives
    • Having fun doing more, with less
Re: How to properly structure code with multiple states and inputs
« Reply #4 on: January 14, 2024, 05:06:41 pm »
When dealing with state machines also keep in mind that it can be helpfull to create sub-state machines that performs a certain task. This de-clutters the primary state machine.

There can be two variants of that.

One is to have a single FSM, e.g. an FSM for a door might be where the "in operation" state has substates "open" and "closed", and the "faulty" state has substates "technician called" and "dead".

The other is to split a single FSM into multiple independent FSMs that send messages/events to each other. An example of that might be a telephone billing system where one FSM "understands" the network events to decide what the phone is doing (e.g. on hook, dialling, connected, ended) and another "understands" the pot of gold (in credit, credit low, out of credit).

Sooner or later the OP will come across UML StateCharts. I like many aspects of them, but the semantics of pre and post event actions can be a pain in the backside to implement. Choose whatever is useful.
There are lies, damned lies, statistics - and ADC/DAC specs.
Glider pilot's aphorism: "there is no substitute for span". Retort: "There is a substitute: skill+imagination. But you can buy span".
Having fun doing more, with less
 
The following users thanked this post: craigc952

Offline Nominal Animal

  • Super Contributor
  • ***
  • Posts: 6264
  • Country: fi
    • My home page and email address
Re: How to properly structure code with multiple states and inputs
« Reply #5 on: January 15, 2024, 06:48:48 am »
I like to use a state machine based on the user interface.

For example, consider a minimal 24-hour alarm clock with HH:MM display, and perhaps an extra LED to indicate alarm is enabled, some kind of a noisemaker, and three buttons.

We have four possible events – alarm triggered, and three button press or autorepeat events.  We have six different user interface states:
  • Normal – display current time.
    Alarm event triggers a switch to Alarm state, and starts the alarm blaring.
    Button 1 event toggles alarm_enabled.
    Button 2 event switches to Time/Hour state.
    Button 3 event switches to Alarm/Hour state.
     
  • Alarm – display current time, with the alarm blaring.
    Alarm event does not trigger anything. (It is already blaring.)
    Button 1 event stops the alarm blaring, clears alarm_enabled, and switches to Normal state.
    Button 2 event stops the alarm blaring, and switches to Normal state.
    Button 3 event stops the alarm blaring, and switches to Normal state.
     
  • Time/Hour – modify current time (hour)
    Alarm event does not trigger anything. (Alarm is disabled while modifying time.)
    Button 1 event switches to Time/Minute state.
    Button 2 event increments current time by one hour.
    Button 3 event decrements current time by one hour.
     
  • Time/Minute – modify current time (minute)
    Alarm event does not trigger anything. (Alarm is disabled while modifying time.)
    Button 1 event switches to Normal state.
    Button 2 event increments current time by one minute.
    Button 3 event decrements current time by one minute.
     
  • Alarm/Hour – modify alarm time (hour)
    Alarm event does not trigger anything.  (Alarm is disabled while modifying alarm time.)
    Button 1 event switches to Alarm/Minute state.
    Button 2 event increments alarm time by one hour.
    Button 3 event decrements alarm time by one hour.
     
  • Alarm/Minute – modify alarm time (minute)
    Alarm event does not trigger anything. (Alarm is disabled while modifying alarm time.)
    Button 1 event switches to Normal state.
    Button 2 event increments alarm time by one minute.
    Button 3 event decrements alarm time by one minute.
I find it very useful to visualize each such user interface state separately; perhaps sketch it out on paper, or draw in Inkscape or Gimp (layers are useful here) or whatever program I find most suitable.  If the user experience is important, one can create a mock-up or simulation in a browser using static HTML + CSS + SVG + JavaScript: those open in any browser, does not require an internet connection.  Very good for creating expensive custom gadgets for a client, and pretty quick to cobble together in JS when you get used to it.

State machines can be documented in various forms, from spreadsheets, to tables, and to graphs.  In particular, if you have K event triggers that can cause a transition to another state, and N states, you can describe the target states in a N-row, K-column table, with additional columns describing other details for the user interface state on that row.  For example, you could use another set of K columns to describe side effects for each event, for example if an event changes a some internal value.

The code implementation details vary a lot – immutable structures or arrays in ROM/Flash, or a switch () { } statement using a numeric state identifier are common, sometimes the source code even generated from simpler text descriptions using scripts! – but organizing the main loop along the user interface states is the key here I'm pushing.

Chosen input button handling approach affects the design a lot, though; especially if you support modifier buttons (like Shift key on your keyboard).  One option is to have an event for each useful button combination.  If you have N buttons, there are N×(N-1)/2 two-button combinations; or N×(N+1)/2 one- or two-button combinations.  Another option is to have the modifier button press event switch to a modified state, release event switch back to the corresponding un-modified state, so that the other keys can correspond to different events in the two states.  (It tends to be a question of what kind and how many button event types you need/want to implement.)
 
The following users thanked this post: craigc952

Offline Kalvin

  • Super Contributor
  • ***
  • Posts: 2145
  • Country: fi
  • Embedded SW/HW.
Re: How to properly structure code with multiple states and inputs
« Reply #6 on: January 15, 2024, 07:58:46 am »
In addition to the valuable insights already provided in previous replies, I highly recommend exploring Hierarchical State Machines (HSMs), especially when dealing with complex projects. HSMs offer a robust framework for managing intricate state machines, significantly simplifying your design process.

Key advantages of HSMs include:

1. Nested States: This feature allows for better organization within the state machine. By nesting states, you can effectively manage complexity, reducing both state explosion and code duplication.

2. Optional Entry/Exit Actions: HSMs provide the flexibility to define actions when entering or exiting a state, which can be crucial for certain applications.

3. Streamlined State Transitions: The model ensures proper handling of entry and exit actions during state transitions, enhancing reliability and predictability.

For a deeper understanding of HSMs, I strongly suggest looking into the work of Miro Samek. His website https://www.state-machine.com/ is a treasure trove of information on this topic.

Additionally, Miro Samek's book, "Practical UML Statecharts in C/C++, 2nd Ed: Event-Driven Programming for Embedded Systems," is an invaluable resource. You can download it here: https://www.state-machine.com/psicc2

While initially, the implementation of HSMs in C/C++ might seem complex or even daunting, the effort pays off significantly in the long run.

Even if you're not planning to implement HSMs in your current project, understanding them is beneficial. They offer a compelling alternative to traditional flat FSMs (Finite State Machines) and can broaden your perspective on managing state in embedded systems.
 
The following users thanked this post: craigc952

Offline craigc952Topic starter

  • Newbie
  • Posts: 3
  • Country: gb
Re: How to properly structure code with multiple states and inputs
« Reply #7 on: January 15, 2024, 09:22:25 am »
Thanks everyone for taking the time to reply. Lots of great information here for me to dig into and try out. Cheers! :)
 

Offline 5U4GB

  • Frequent Contributor
  • **
  • Posts: 391
  • Country: au
Re: How to properly structure code with multiple states and inputs
« Reply #8 on: January 15, 2024, 11:07:31 am »
I note in passing that beginners think about "how thing will work", whereas experienced professionals think about "how things can fail and what to do about failures". Good FSM design and implementation is very valuable for the latter :)

There are several "design patterns" for this, each with their own advantages and disadvantages.

+1 for everything in the original message, but adding a pointer to additional documentation: For the first 15-20 odd years of Windows programming the main part of every program was a giant event loop (the message loop in Windows terminology), so an awful lot has been written by some very sharp people about ways of dealing with that sort of thing.  It's been too long for me to remember any references on event loop design offhand and a lot of it will predate the web, but if you have access to a source of old books/magazines like MSDN Journal...
 

Offline Nominal Animal

  • Super Contributor
  • ***
  • Posts: 6264
  • Country: fi
    • My home page and email address
Re: How to properly structure code with multiple states and inputs
« Reply #9 on: January 15, 2024, 07:29:07 pm »
Event-driven programming (link to Wikipedia) is a good place to start looking.
The same event-based paradigm is still used everywhere for user interfaces, including web pages (JavaScript DOM Events).

I often mix state machines and event-driven programming in my microcontroller projects.  I like to use an interrupt to scan the button states, and 'post' the events thus generated to a queue of some sort.  The main state machine loop then does not need to run at regular intervals, as it can handle multiple events in a batch, then go to sleep waiting for a new event.

One very common issue with event-driven programming and user interfaces is that programmers often try to do too much in the event handler itself.  It leads to that user interface becoming unresponsive until that event handler completes.  The proper approach is to do as little as is necessary in the event handler itself, and then 'post' or 'queue' the work to be done when there is idle time.  In web pages, this is easiest to do by using window.setTimeout() JavaScript method (on the browser window object).

On a microcontroller, this means that it is better to have a lightweight interrupt (10 Hz to 1000 Hz) scanning the buttons, and based on the states and changes in states, provide a 'queue' of button events the main state machine look can examine.
 
The following users thanked this post: 5U4GB, craigc952

Offline tggzzz

  • Super Contributor
  • ***
  • Posts: 19511
  • Country: gb
  • Numbers, not adjectives
    • Having fun doing more, with less
Re: How to properly structure code with multiple states and inputs
« Reply #10 on: January 15, 2024, 09:06:05 pm »
Event-driven programming (link to Wikipedia) is a good place to start looking.
The same event-based paradigm is still used everywhere for user interfaces, including web pages (JavaScript DOM Events).

I often mix state machines and event-driven programming in my microcontroller projects.  I like to use an interrupt to scan the button states, and 'post' the events thus generated to a queue of some sort.  The main state machine loop then does not need to run at regular intervals, as it can handle multiple events in a batch, then go to sleep waiting for a new event.

One very common issue with event-driven programming and user interfaces is that programmers often try to do too much in the event handler itself.  It leads to that user interface becoming unresponsive until that event handler completes.  The proper approach is to do as little as is necessary in the event handler itself, and then 'post' or 'queue' the work to be done when there is idle time.  In web pages, this is easiest to do by using window.setTimeout() JavaScript method (on the browser window object).

On a microcontroller, this means that it is better to have a lightweight interrupt (10 Hz to 1000 Hz) scanning the buttons, and based on the states and changes in states, provide a 'queue' of button events the main state machine look can examine.

That is good advice. It leads to scalable systems that can be understood, maintained and extended.

I'll add only two levels of priority: normal operation and emergency shutdown.

Of course any FSM implementation will fail if deadlock or livestock occurs. Ensuring that can't happen is non-trivial.
There are lies, damned lies, statistics - and ADC/DAC specs.
Glider pilot's aphorism: "there is no substitute for span". Retort: "There is a substitute: skill+imagination. But you can buy span".
Having fun doing more, with less
 
The following users thanked this post: craigc952

Online nctnico

  • Super Contributor
  • ***
  • Posts: 26907
  • Country: nl
    • NCT Developments
Re: How to properly structure code with multiple states and inputs
« Reply #11 on: January 15, 2024, 09:26:44 pm »
On a microcontroller, this means that it is better to have a lightweight interrupt (10 Hz to 1000 Hz) scanning the buttons, and based on the states and changes in states, provide a 'queue' of button events the main state machine look can examine.
Not really. A queue means overhead and using an interrupt for something non-critical as scanning buttons is adding more complexity then necessary. Avoid needing to carry information between process boundaries with all the necessary process synchronisation issues and code overhead. A better solution is to scan the buttons from a main loop based on a tick counter.
There are small lies, big lies and then there is what is on the screen of your oscilloscope.
 

Online SiliconWizard

  • Super Contributor
  • ***
  • Posts: 14481
  • Country: fr
Re: How to properly structure code with multiple states and inputs
« Reply #12 on: January 15, 2024, 11:01:28 pm »
No, I have a better solution! All of the above is bad. ;D
 

Offline Nominal Animal

  • Super Contributor
  • ***
  • Posts: 6264
  • Country: fi
    • My home page and email address
Re: How to properly structure code with multiple states and inputs
« Reply #13 on: January 15, 2024, 11:03:37 pm »
As the RP2040 is a dual-core 32-bit ARM Cortex M0+ with ample memory for small event queues, we can use 32-bit unsigned integers to hold the event descriptors in a reasonably sized queue.  Obviously not just keypresses, but all sorts of events; even X-Y coordinate pairs for touch events on touchscreens.

To protect against concurrent access, the queues need a spin lock (that work across cores and interrupts).  Since no queue function will access another queue, a single spin lock across all queues will suffice.
    spin_lock_t  *queue_lock;
It must be initialized at runtime by allocating one of the very few available spinlocks via
    queue_lock = spin_lock_init(spin_lock_claim_unused(true));
The Pico SDK implementation not only blocks the other core, but also temporarily disables interrupts, so the push and pop functions can be used on the same queue on either core and from interrupt handlers.

To get some polymorphicity from plain C99 or later code (no templates), we can declare one or more queues as just uint32_t arrays with an extra element.  The length must be between 4 and 2048, inclusive, and preferably even (or you'll waste four bytes). For example,
    volatile uint32_t  example_queue[2048];
Each such queue is initialized at run time via
    queue_init(example_queue, sizeof example_queue);
which sets the first word to reflect an empty queue of that particular size.
Bits 31..22 of the first word is m; the size of the queue is 2m+1.
Bits 21..11 contain the index of the oldest element in the queue.
Bits 10..0 contain the number of elements in the queue.
Code: [Select]
static inline void  queue_init(volatile uint32_t *const q, uint32_t bytes) {
    // TODO: assert that bytes is between 12 and 8192, inclusive.  If not a multiple of eight, some bytes are wasted.
    q[0] = ((bytes - 8) & 0x1ff8) << 19;
}

/* Push a (nonzero) event to a queue.
 * Returns event if successful, zero to indicate queue is full.
*/
static inline uint32_t  queue_push(volatile uint32_t *const q, uint32_t ev) {
    uint32_t  saved = spin_lock_blocking(queue_lock);

    const uint32_t  key  =  q[0];
    const uint32_t  have =  key        & 0x7FF;
    const uint32_t  base = (key >> 11) & 0x7FF;
    const uint32_t  size = (key >> 21) | 0x001; // Always odd

    if (have >= size) {
        // Queue is full
        ev = 0;

    } else {
        uint32_t  i = base + have;
        if (i >= size)
            i -= size;

        q[i + 1] = ev;
        q[0] = key + 1; // In q[0]: have++;
    }

    spin_unlock(queue_lock, saved);
    return ev;
}

/* Pop a (nonzero) event from the queue.
 * Returns event if successful, zero to indicate the queue was empty.
*/
static inline uint32_t  queue_pop(volatile uint32_t *const q) {
    uint32_t  saved = spin_lock_blocking(queue_lock);

    const uint32_t  key  =  q[0];
    const uint32_t  have =  key        & 0x7FF;
    uint32_t        base = (key >> 11) & 0x7FF;
    const uint32_t  size = (key >> 21) | 0x001; // Always odd

    uint32_t  ev;

    if (have <= 0) {
        // Queue is empty
        ev = 0;

    } else {
        ev = q[++base];
        if (base >= size) {
            // In q[0]: have--, base = base - size, with base incremented by one
            q[0] = key + 0x7FF - (size << 11);
        } else {
            // In q[0]: have--, with base incremented by one
            q[0] = key + 0x7FF;
        }
    }

    spin_unlock(queue_lock, saved);
    return ev;
}
Above, the buffer is circular.  The math applied to the first word is "tricky" (in the bad, not easily maintained sense), because instead of repacking it, we can apply the operations directly to it using a single addition or an addition and a subtraction.  The simple idea is that if
    val = ((size & 0x7FE) << 21) | ((base & 0x7FF) << 11) | (have & 0x7FF);
incrementing base and then repacking val is the same as adding (1<<11)=0x800 to val.  Similarly, incrementing have is the same as incrementing valsize never changes.

Note that size always being odd means we don't need to store its least significant bit, which allows us to store three 11-bit values in a single 32-bit value.  This corresponds to a queue structure whose size is a multiple of eight bytes.  The maximum queue length is then 2047 events.

On raw metal on single-core ARM Cortex-M, queue_lock is not needed, and uint32_t saved = spin_lock_blocking(queue_lock); is replaced by something like uint32_t saved = __get_PRIMASK(); if (saved) __disable_irq();, and spin_unlock(queue_lock, saved); is replaced with something like __set_PRIMASK(saved); or if (saved) __enable_irq();.  The exact details vary a bit between M0, M4, M7, etc.

On AVRs, queue_lock is not needed, and uint32_t saved = spin_lock_blocking(queue_lock); is replaced with unsigned char saved = SREG; cli(); and spin_unlock(queue_lock, saved); is replaced with SREG = saved;.

In general, on single-core microcontrollers saved=spin_lock_blocking() needs to disable interrupts that may use queue functions, returning the previous interrupt enable/disable state, which spin_unlock(saved) then needs to restore.



Not really. A queue means overhead and using an interrupt for something non-critical as scanning buttons is adding more complexity then necessary.
I like my buttons reliable and precise, with software debouncing and accelerating autorepeat.  I hate devices that occasionally miss keypresses, because you are "too fast".  Varying autorepeat rate also feels very clunky and amateurish to me.

If the buttons are directly connected (not a matrix), the periodic scanning interrupt can be normally disabled, and instead only a state change interrupt active that re-enables the periodic scanning interrupt on the first detected state transition.  (On many Cortex-M implementations, there is only one hardware input pin interrupt per GPIO bank, so this is quick and easy to do.)  The periodic scanning interrupt also counts the number of iterations with no events and no keys being pressed, and when that exceeds some configured limit, disables itself and enables the state change interrupt.  This saves resources.

When the input events include combined button presses and/or doublepresses, it greatly simplifies the input handling, because the state machine only notices verified events, and can run at varying intervals, without having to consider elapsed time between iterations.  Instead of counting milliseconds, I count scanning loop iterations, and can tune the scan rate to something suitable.

As a particular example, consider a capacitive touchscreen on a small 240×320 display (2.8", 3.2", or 3.5" size).  You can use an event queue with 32-bit entries to record the touch tracks (id:pressure:X:Y), and even support multitouch (one id per concurrent touch).  The rate at which you poll the touchscreen determines how many waypoints you get for gestures and such.  If you poll at specific regular intervals, the waypoints are regularly spaced in time, and delta between consecutive points is directly proportional to velocity.  Otherwise, your waypoints are basically randomly polled along the track the user dragged.

The code is not hard to craft nor use; it is really only the RAM used by such queues that can be a limiting factor.

Consider my example code earlier in this message: the state machine simply calls uint32_t ev = queue_pop(input_events); whenever it wants to see if there are pending input events that would affect the state machine.  The corresponding queue_push(input_events, new_ev) calls can occur in interrupt service routines, or even on another core (or in an interrupt service routine on another core) on Raspberry Pi Pico (aka RP2040).
If your gadget provides a serial port or USB serial for commands (so it can be remotely operated instead of using the physical buttons), your serial command parser can generate the corresponding input events too, without added complexity.

My point here is to show that such event queues can be used on microcontrollers, even AVRs; and that they can be used in various ways with finite state machines.  Whether they fit to any specific use case, depends obviously on the use case, with the memory they need being their most limiting factor.
I don't always use them in my UI projects, either: after all, they're just another tool one can use when appropriate.

No, I have a better solution! All of the above is bad. ;D
:-[ :'(
 
The following users thanked this post: craigc952

Offline tggzzz

  • Super Contributor
  • ***
  • Posts: 19511
  • Country: gb
  • Numbers, not adjectives
    • Having fun doing more, with less
Re: How to properly structure code with multiple states and inputs
« Reply #14 on: January 15, 2024, 11:09:25 pm »
On a microcontroller, this means that it is better to have a lightweight interrupt (10 Hz to 1000 Hz) scanning the buttons, and based on the states and changes in states, provide a 'queue' of button events the main state machine look can examine.
Not really. A queue means overhead and using an interrupt for something non-critical as scanning buttons is adding more complexity then necessary. Avoid needing to carry information between process boundaries with all the necessary process synchronisation issues and code overhead. A better solution is to scan the buttons from a main loop based on a tick counter.

"Overhead" (whatever that means) is normally unimportant nowadays.

Arguably more complex when considering a very simple FSM. But that has to be weighed against the simplicity of there being only a single flow-of-control design pattern and implementation.

A half-way house would be to have several FSMs, one for the buttons plus a separate FSM for the main execution processing.

Or you could just do it properly, and dedicate one processor to the buttons, and one or more processors to the main execution processing. Go xC+xCORE; you know it makes sense :)
There are lies, damned lies, statistics - and ADC/DAC specs.
Glider pilot's aphorism: "there is no substitute for span". Retort: "There is a substitute: skill+imagination. But you can buy span".
Having fun doing more, with less
 

Online nctnico

  • Super Contributor
  • ***
  • Posts: 26907
  • Country: nl
    • NCT Developments
Re: How to properly structure code with multiple states and inputs
« Reply #15 on: January 15, 2024, 11:24:30 pm »
On a microcontroller, this means that it is better to have a lightweight interrupt (10 Hz to 1000 Hz) scanning the buttons, and based on the states and changes in states, provide a 'queue' of button events the main state machine look can examine.
Not really. A queue means overhead and using an interrupt for something non-critical as scanning buttons is adding more complexity then necessary. Avoid needing to carry information between process boundaries with all the necessary process synchronisation issues and code overhead. A better solution is to scan the buttons from a main loop based on a tick counter.

"Overhead" (whatever that means) is normally unimportant nowadays.

Arguably more complex when considering a very simple FSM. But that has to be weighed against the simplicity of there being only a single flow-of-control design pattern and implementation.
What you see with a lot of programmers is that during their career they go through roughly 3 stages:
1) Do everything in a single loop
2) Discover the existence of processes so start to put everything into seperate processes only to find out this makes software A) unreliable, B) difficult to debug and C) difficult to maintain
3) Go back to implementing related tasks into single 'super loop' processes and only use seperate processes after very careful deliberation

There are small lies, big lies and then there is what is on the screen of your oscilloscope.
 

Offline tggzzz

  • Super Contributor
  • ***
  • Posts: 19511
  • Country: gb
  • Numbers, not adjectives
    • Having fun doing more, with less
Re: How to properly structure code with multiple states and inputs
« Reply #16 on: January 15, 2024, 11:28:46 pm »
On a microcontroller, this means that it is better to have a lightweight interrupt (10 Hz to 1000 Hz) scanning the buttons, and based on the states and changes in states, provide a 'queue' of button events the main state machine look can examine.
Not really. A queue means overhead and using an interrupt for something non-critical as scanning buttons is adding more complexity then necessary. Avoid needing to carry information between process boundaries with all the necessary process synchronisation issues and code overhead. A better solution is to scan the buttons from a main loop based on a tick counter.

"Overhead" (whatever that means) is normally unimportant nowadays.

Arguably more complex when considering a very simple FSM. But that has to be weighed against the simplicity of there being only a single flow-of-control design pattern and implementation.
What you see with a lot of programmers is that during their career they go through roughly 3 stages:
1) Do everything in a single loop
2) Discover the existence of processes so start to put everything into seperate processes only to find out this makes software A) unreliable, B) difficult to debug and C) difficult to maintain
3) Go back to implementing related tasks into single 'super loop' processes and only use seperate processes after very careful deliberation

Yup, but hopefully the code in (3) is better architected and implemented than in (1) :)

A good start are Hoare's CSP concepts, as implemented in xC and partially implemented in Rust, Go, etc.
There are lies, damned lies, statistics - and ADC/DAC specs.
Glider pilot's aphorism: "there is no substitute for span". Retort: "There is a substitute: skill+imagination. But you can buy span".
Having fun doing more, with less
 

Offline 5U4GB

  • Frequent Contributor
  • **
  • Posts: 391
  • Country: au
Re: How to properly structure code with multiple states and inputs
« Reply #17 on: January 16, 2024, 05:59:08 am »
As the RP2040 is a dual-core 32-bit ARM Cortex M0+ with ample memory for small event queues, we can use 32-bit unsigned integers to hold the event descriptors in a reasonably sized queue.

I think for Windows memorial purposes you need to sprinkle at least a few  HANDLE_MSG()'s throughout the code though :P.

More seriously though, you could at least look at Windows message maps because they do a lot of the messy stuff for you, the handling is done via macros so you could just retarget them for your own purposes.  Not saying they're the perfect solution, but they are a ready-made one that's evolved over time to handle this sort of thing.
 

Online Psi

  • Super Contributor
  • ***
  • Posts: 9951
  • Country: nz
Re: How to properly structure code with multiple states and inputs
« Reply #18 on: January 16, 2024, 06:09:53 am »
While a FSM with function points is super useful and is sometimes the best option, it also can make things harder to read.

A big switch statement but where each case is just a single function call to somewhere else can sometimes be better.

It really depends on the situation
Greek letter 'Psi' (not Pounds per Square Inch)
 

Offline tggzzz

  • Super Contributor
  • ***
  • Posts: 19511
  • Country: gb
  • Numbers, not adjectives
    • Having fun doing more, with less
Re: How to properly structure code with multiple states and inputs
« Reply #19 on: January 16, 2024, 10:01:46 am »
While a FSM with function points is super useful and is sometimes the best option, it also can make things harder to read.

A big switch statement but where each case is just a single function call to somewhere else can sometimes be better.

It really depends on the situation

For FSMs that are guaranteed to remain tiny, I agree a switch statement is sufficient. But when they evolve to be less than tiny, the readability becomes questionable - and modifiability becomes problematic.

I've seen an FSM that was extended over the years and it had mutated to the extent that if-then-else statements were nested 10 deep. Each individual mutation was sensible and justified, based on evolving customer requirements. Yes, that was heroic and incompetent in retrospect, but it was a classic case of boiling a frog by raising the temperature one degree at time.

All techniques have their brick walls, but IMHO the switch statement technique hits the brick wall earlier than most.

Example os a "tiny" FSM: debouncing a switch before then sending a up/down event to a "real" FSM.
« Last Edit: January 16, 2024, 10:04:40 am by tggzzz »
There are lies, damned lies, statistics - and ADC/DAC specs.
Glider pilot's aphorism: "there is no substitute for span". Retort: "There is a substitute: skill+imagination. But you can buy span".
Having fun doing more, with less
 

Online nctnico

  • Super Contributor
  • ***
  • Posts: 26907
  • Country: nl
    • NCT Developments
Re: How to properly structure code with multiple states and inputs
« Reply #20 on: January 16, 2024, 02:45:16 pm »
While a FSM with function points is super useful and is sometimes the best option, it also can make things harder to read.

A big switch statement but where each case is just a single function call to somewhere else can sometimes be better.

It really depends on the situation

For FSMs that are guaranteed to remain tiny, I agree a switch statement is sufficient. But when they evolve to be less than tiny, the readability becomes questionable - and modifiability becomes problematic.

I've seen an FSM that was extended over the years and it had mutated to the extent that if-then-else statements were nested 10 deep. Each individual mutation was sensible and justified, based on evolving customer requirements. Yes, that was heroic and incompetent in retrospect, but it was a classic case of boiling a frog by raising the temperature one degree at time.

All techniques have their brick walls, but IMHO the switch statement technique hits the brick wall earlier than most.
It also depends on the tooling you are using to write code. Typically an IDE won't be able to show call hiearchies through lookup tables. So an FSM implemented using a lookup table is harder to deal with compared to using switch / case statements. Even with thourough documentation, many programmers are not keen to work on a project which uses an FSM implemented using lookup tables. Been there, done that.

So a lot is to say for using switch / case statements (or equivalent as not all language support the switch / case construct). For example: have one function with a switch / case per state could be an implementation which is more friendly to maintain.
« Last Edit: January 16, 2024, 04:11:41 pm by nctnico »
There are small lies, big lies and then there is what is on the screen of your oscilloscope.
 

Offline tggzzz

  • Super Contributor
  • ***
  • Posts: 19511
  • Country: gb
  • Numbers, not adjectives
    • Having fun doing more, with less
Re: How to properly structure code with multiple states and inputs
« Reply #21 on: January 16, 2024, 04:13:08 pm »
While a FSM with function points is super useful and is sometimes the best option, it also can make things harder to read.

A big switch statement but where each case is just a single function call to somewhere else can sometimes be better.

It really depends on the situation

For FSMs that are guaranteed to remain tiny, I agree a switch statement is sufficient. But when they evolve to be less than tiny, the readability becomes questionable - and modifiability becomes problematic.

I've seen an FSM that was extended over the years and it had mutated to the extent that if-then-else statements were nested 10 deep. Each individual mutation was sensible and justified, based on evolving customer requirements. Yes, that was heroic and incompetent in retrospect, but it was a classic case of boiling a frog by raising the temperature one degree at time.

All techniques have their brick walls, but IMHO the switch statement technique hits the brick wall earlier than most.
It also depends on the tooling you are using to write code. Typically an IDE won't be able to show call hiearchies through lookup tables. So an FSM implemented using a lookup table is harder to deal with compared to using switch / case statements.

"Naked" function pointers will "disrupt" most analysis tools; FSMs aren't unique in that respect.

Quote
Even with thourough documentation, many programmers are not keen to work on a project which uses an FSM implemented using lookup tables. Been there, done that.

I refused to even look at that 10-deep if-then-else mess, except to make sure the implementation was a grotesque as people said. It was. Quite astounding.

IDEs are a crap way to document and/or understand FSMs, full stop. The best way is a combination of diagram showing the states and interconnecting events, plus text for the details of any actions. While it is possible to auto-generate code from such diagrams, it isn't easy and requires discipline.

The real issue is that before long someone will patch the binaries auto-generated code. Either it won't be back-annotated or the modifications won't be easily expressible in the pretty pictures.

Quote
So a lot is to say to use switch / case statements (or equivalent as not all language support the switch / case construct). For example: have one function with a switch / case per state could be an implementation which is more friendly to maintain.

While I agree the table technique is limited to small/medium FSMs, in my experience switch/if statements are worse in the medium and long terms.

The table technique has the virtue of being simple, explicit, and has an obvious direct correspondance with the FSM design spec. That satisfies my meta-requirement that it should be easy to determine exactly which line of code needs to be changed, and that changing it won't accidentally change other parts of the FSM.

But overall it is a case that an engineer should be aware that there are multiple techniques, and to be able to choose the appropriate one for the current job. "Horses for courses", "many ways to skin a cat", etc.

Oh, it is also necessary that an engineer recognises that they are in the process of building an FSM. Too many softies think "FSM==parsing in a compiler", and just throw a lot of spaghetti if-then-else statements in the implementation.
There are lies, damned lies, statistics - and ADC/DAC specs.
Glider pilot's aphorism: "there is no substitute for span". Retort: "There is a substitute: skill+imagination. But you can buy span".
Having fun doing more, with less
 

Offline Nominal Animal

  • Super Contributor
  • ***
  • Posts: 6264
  • Country: fi
    • My home page and email address
Re: How to properly structure code with multiple states and inputs
« Reply #22 on: January 16, 2024, 05:16:18 pm »
What you see with a lot of programmers is that during their career they go through roughly 3 stages:
1) Do everything in a single loop
2) Discover the existence of processes so start to put everything into seperate processes only to find out this makes software A) unreliable, B) difficult to debug and C) difficult to maintain
3) Go back to implementing related tasks into single 'super loop' processes and only use seperate processes after very careful deliberation
I've spent quite a lot of time in systems programming (libraries, services, backends), and in high-performance computing, specializing in parallel and distributed computing.  Instead of step 2 and step 3, step 2 was an explosion of new opportunities, techniques, and approaches for me; and going back to step 1 or step 3 feels extremely constricting.  Even on single-core 8-bit microcontrollers, I find the way interrupts behaves analogous to pre-emptive threads on a single-core, and I enjoy applying the associated techniques.

So, all I can say is that I do not see that.  Sure, I do know and meet a lot of programmers who are not able or willing to spend the time and effort to really understand parallel and distributed computing, message-passing techniques, and all the related problems and the various ways they can be approached, but when I help others learn, I do steer them resolutely towards the path I took and found useful, powerful, and rewarding.

While it is possible to auto-generate code from such diagrams, it isn't easy and requires discipline.

The real issue is that before long someone will patch the binaries auto-generated code. Either it won't be back-annotated or the modifications won't be easily expressible in the pretty pictures.
Yep.  This is why I prefer the inverse method, easily done when the object file format is ELF: use a macro for declaring the state structure, and move all state structures to their own section.  This means they will be consecutive in memory, like an array; and easy to pluck out from the final linked firmware image.  Parse using few lines of Python (struct.unpack() as its format makes it easy), describing each entry in Graphviz DOT language for visualization.

With a bit more work, you can use another section for structures that describe each state in human terms, and contain a pointer to the referred to state structure; maybe another that describes each handler (with a pointer to the function).  These sections are dropped from the firmware image, but you have a second link target that does contain these sections and the state structure section (and code and data sections, so you get the function pointers).  You can then use a simple Python script to extract the state structures and their descriptions, and generate the documentation and state graphs from the actual data structures used on the target.

If you use a gdb-derived debugger, pretty-printing structures and graphs and nested structures only requires you to write a Python helper gdb can use.  This means that although many intensely dislike Python, it is a good tool for build helpers and generators for those using gcc-based toolchains.
 

Offline tggzzz

  • Super Contributor
  • ***
  • Posts: 19511
  • Country: gb
  • Numbers, not adjectives
    • Having fun doing more, with less
Re: How to properly structure code with multiple states and inputs
« Reply #23 on: January 16, 2024, 05:44:33 pm »
What you see with a lot of programmers is that during their career they go through roughly 3 stages:
1) Do everything in a single loop
2) Discover the existence of processes so start to put everything into seperate processes only to find out this makes software A) unreliable, B) difficult to debug and C) difficult to maintain
3) Go back to implementing related tasks into single 'super loop' processes and only use seperate processes after very careful deliberation
I've spent quite a lot of time in systems programming (libraries, services, backends), and in high-performance computing, specializing in parallel and distributed computing.  Instead of step 2 and step 3, step 2 was an explosion of new opportunities, techniques, and approaches for me; and going back to step 1 or step 3 feels extremely constricting.  Even on single-core 8-bit microcontrollers, I find the way interrupts behaves analogous to pre-emptive threads on a single-core, and I enjoy applying the associated techniques.

So, all I can say is that I do not see that.  Sure, I do know and meet a lot of programmers who are not able or willing to spend the time and effort to really understand parallel and distributed computing, message-passing techniques, and all the related problems and the various ways they can be approached, but when I help others learn, I do steer them resolutely towards the path I took and found useful, powerful, and rewarding.

I've done that successfully, on a hard realtime Z80 system many decades ago. Two of the reasons it worked was because I was the only designer and implementer, and I could structure it into a cooperating RTOS based around a yield().

IMHO it doesn't scale well, either with number of developers, or with long-duration interrupt processing, or with modern multicore MCUs. Yes, it can be made to work - but I don't feel I am in control, nor that I can prove to others that it will work, nor that I can see how well it is/isn't working.

FSMs, messages and event queues are a superb mechanism for decoupling relatively independent processing at the design stage. During implementation and commissioning they provide a neat conceptual point for logging/measuring what the system is doing and how efficiently it is working.

They are a great way to be able to say "this log shows exactly what my system did and what your system didn't do; your problem, you fix it" :)


Quote
While it is possible to auto-generate code from such diagrams, it isn't easy and requires discipline.

The real issue is that before long someone will patch the binaries auto-generated code. Either it won't be back-annotated or the modifications won't be easily expressible in the pretty pictures.
Yep.  This is why I prefer the inverse method, easily done when the object file format is ELF: use a macro for declaring the state structure, and move all state structures to their own section.  This means they will be consecutive in memory, like an array; and easy to pluck out from the final linked firmware image.  Parse using few lines of Python (struct.unpack() as its format makes it easy), describing each entry in Graphviz DOT language for visualization.

With a bit more work, you can use another section for structures that describe each state in human terms, and contain a pointer to the referred to state structure; maybe another that describes each handler (with a pointer to the function).  These sections are dropped from the firmware image, but you have a second link target that does contain these sections and the state structure section (and code and data sections, so you get the function pointers).  You can then use a simple Python script to extract the state structures and their descriptions, and generate the documentation and state graphs from the actual data structures used on the target.

If you use a gdb-derived debugger, pretty-printing structures and graphs and nested structures only requires you to write a Python helper gdb can use.  This means that although many intensely dislike Python, it is a good tool for build helpers and generators for those using gcc-based toolchains.

I presume such home-grown tool rely on specific implementation artefacts being visible in the binaries. That's regrettably fragile, being reliant on informal design patterns in the code and specific compiler optimisations.

As for Python, semantically significant whitespace is too close to brainfuck for my taste. But it gets quick and dirty hacks done, so "fake it till you make it"/"move fast and break things pick up the pieces" youngsters like it.
« Last Edit: January 16, 2024, 06:02:42 pm by tggzzz »
There are lies, damned lies, statistics - and ADC/DAC specs.
Glider pilot's aphorism: "there is no substitute for span". Retort: "There is a substitute: skill+imagination. But you can buy span".
Having fun doing more, with less
 

Online nctnico

  • Super Contributor
  • ***
  • Posts: 26907
  • Country: nl
    • NCT Developments
Re: How to properly structure code with multiple states and inputs
« Reply #24 on: January 16, 2024, 07:26:06 pm »
IDEs are a crap way to document and/or understand FSMs, full stop. The best way is a combination of diagram showing the states and interconnecting events, plus text for the details of any actions.
The problem is that a lot of people don't care about documentation (or it is just too much work to look things up). So the better the code is suited for analysis through the tools a typical IDE provides, the easier / quicker a piece of software is to maintain.
There are small lies, big lies and then there is what is on the screen of your oscilloscope.
 


Share me

Digg  Facebook  SlashDot  Delicious  Technorati  Twitter  Google  Yahoo
Smf