“Input-Driven” vs. Event-Driven State Machines

share on: 
Originally published on:  
2021-02-16
Last updated on: 
input-driven FSM
Table of Contents
Tags:   

On the free support forum for the QP state machine frameworks, an engineer has recently asked a question “superloop vs event dispatching“, which I quote below. I think that this question is so common that my answer could be interesting to the readers of this “state-space” blog.

Question

In the classical way of programming, I write state-machines with switch statements, where each distinct case represents a separate state. The super loop ( while (1) ) executes continuously and looks if a different state is reached based on the past occurrences until that line is executed.

I am practicing with reactive class way of state-charts for a while and I get confused a little bit. First there is no explicit superloop, but an event dispatcher task instead, feeding events into the state-machine of the target reactive object, where the event dispatcher task serves possibly more than one object.

Please explain me the difference between this new approach and the traditional switch-case approach.

Your state-machine code requires dynamic instantiation of events, a place to store those events and deletion of events at the end. In the classical way, there is no need for such dynamic management of events, which makes me think that this new approach brings a more heavyweight solution.

I want to hear something more than “you can visually design states”.

What are things that can NOT be done in the classical way of state-machine coding with only switch-case statements but can be done in the real-time framework supported state-machine handling with event management.

Different Type of State Machines

What you code with the switch/case statements inside your while(1) “superloops” are state machines of a different type. They are so called “input-driven state machines”, sometimes also called “polled state machines”.

Specifically, the “input-driven state machines” are obviously not driven by events, because as you observe, there are no events in your system. (Events are understood here as objects specifically designed for communication, carrying a signal and optional parameters.)

Instead, your “input-driven” state machines constantly check various “inputs’ in every state. In the diagrams, these checks are shown as boolean expressions on transitions, for example a transition might be labeled with: (gear_lever == UP) && (!on_ground)... In the code the same conditions become simply if-statements (sometimes chains of if/else statements). The point is that you have a lot of those ifs, essentially in every state.

input-driven FSM
Example of a simple "input-driven" state machine.
Source: Martin Gomez "Finite State Machine Implementation", ESP Dec-2000

Here is the example code for an “input-driven” state machine corresponding to the “GearDown” state. Please note the if-statement in the state function. (Source: Martin Gomez “Finite State Machine Implementation”, ESP Dec-2000):

void GearDown() {
    /* Raise the gear upon command, but not if the airplane is on the ground.*/
    if ((gear_lever == UP) && (prev_gear_lever == DOWN)
        && (squat_switch == UP))
    {
        timer = 2.0;
        curr_state = WTG_FOR_TKOFF;
    };
    prev_gear_lever = gear_lever; /* Store for edge detection.*/
}

Problems with the "Input-Driven" Approach

The main problems with such “input-driven” approach are race conditions, data inconsistencies, and sometimes undersampling problems (Nyquist theorem). Let me explain these issues in the following sub-sections.

Race Conditions

The “inputs” are typically variables, or sometimes raw peripheral registers, that are changing asynchronously to the code execution in your while(1) “superloop”

By “asynchronous”, I mean that the “input” variables more often than not are changed from interrupts, which can preempt your “superloop”. The raw peripheral registers (e.g., GPIO registers, ADC samples, Timers, etc.) are even more independent from (not synchronized with) the code execution.

All such asynchronous variables/registers are subject to race conditions, and every access to them from your code should be very carefully protected with some sort of mutual exclusion mechanism (e.g., disabling interrupts, which is not helping with peripherals).

But in practice, the numerous if/else are not protected, or inadequately protected from race conditions. (Are they in your code?)

Data Inconsistencies