Embedded Software Architecture Design – State Machine

Click on the blue text above to learn more practical skills in embedded programming. If you find this article helpful, feel free to like + follow

Introduction

As the function business code increases, upon reviewing the code, it becomes evident that the use of if-else if-else statements is becoming more prevalent due to the need to distinguish between various scenarios where functionalities differ. Therefore, if statements are used for judgment, and as scenarios increase, the use of if-else if-else will also increase.

The if-else if-else statements here are typically used to distinguish the implementation of functionalities in different scenarios, which is somewhat different from the table-driven programming mentioned in the previous article.

To avoid this situation, this article introduces a development method – state machine programming.

Why is state machine programming needed? Many people may occasionally overlook or incorrectly write some trigger conditions when implementing if judgments, leading to functional anomalies, especially in complex logical conditions with a plethora of &&/||/() etc., making it challenging to ensure that all conditions trigger correctly. State machines are typically implemented using switch-case.

So what problems does the introduction of state machines solve?

  • When a program has multiple states, it standardizes the state transitions of the program, avoiding the introduction of complex judgment logic.

  • It standardizes the implementation and capabilities of the program in different states.

  • It allows for horizontal expansion in capabilities, providing new states to enhance existing logic.

  • With clearer logic, the implementation process considers more situations, facilitating problem identification.

What is a State Machine?

A state machine is an abbreviation for Finite State Machine (FSM), which is a mathematical model abstracted from the operating rules of real-world entities.

Simply put: Real-world entities have different states. For example, a light has two states: “on” and “off”. If we add a “broken” state, that would be an exceptional case.

Concepts

  • Current State: The state currently being occupied. A state machine must contain at least two states, and at any moment can only be in one state. For instance, the light has two states: “on” and “off”.

  • Condition: Also known as an event, it is the trigger condition or command for executing an operation. For example, controlling the light through a switch, operating the switch is an event.

  • Action: The action to be executed after the event occurs. For example, pressing the switch turns the light on, releasing it turns it off.

  • Next State: The new state to transition to when the condition is met. For example, when the switch is pressed, the light transitions from the current “off” state to the “on” state.

Embedded Software Architecture Design - State Machine

Those who have used “RTOS” should know about the various states of tasks. The four states of a task are: Ready State, Running State, Blocked State, Suspended State. Two or more states cannot exist simultaneously. The OS switches the task state based on the current state, task priority, tick clock, and other conditions.

Types of Actions in State Machines

  • Entry Action: Performed upon entering a state.

  • Exit Action: Performed upon exiting a state.

  • Input Action: Depends on the current state and input conditions.

  • Transition Action: Performed during specific transitions.

Implementation

First, let’s look at a simple example of controlling a motor under different scenarios: powering on the device starts the motor three times, pressing the switch starts once, and powering off starts the motor three times. The non-state machine implementation uses various flags to determine whether the device needs to control the motor and under what conditions to exit.

This is just a simple implementation, and there may be some state machine ideas involved (after all, the concept of state machine programming is already in mind, it’s unavoidable). However, I will try to restore the logical thinking I had during my initial programming experience, please don’t mind.

/* Control Motor Function */
void MotorCtrlTask(void)
{
    if (ctrlCnt)
    {
        MotorCtrl(ON);
        delay(1);
        MotorCtrl(OFF);
    }
    else
    {
        MotorCtrl(OFF);
    }
}

int isPowerOn = true;
int isPowerOff = false;
int ctrlCnt = 0;

void main(void)
{   
    while (1)
    {
        if (isPowerOn)
        {
            isPowerOn = false;
            ctrlCnt = 3;
        }

        if (keyPress)
        {
            keyPress = false;
            ctrlCnt = 1;
        }

        if (...)  // Power off condition
        {
            if (ctrlCnt == 0 && !isPowerOff && !isPowerOn)
            {
                isPowerOff = true;
                ctrlCnt = 3;
            }
        }

        MotorCtrlTask();

        if (ctrlCnt > 0)
            ctrlCnt--;
        else
        {
            if (ctrlCnt == 0 && isPowerOff && !isPowerOn)
            {
                return;
            }
        }
    }
}

By adopting state machine programming, the first consideration is that there are three states: power on, power off, and working state. It is essential to clarify the conditions for transitioning between these three states and the relevant functionalities that need to be executed in the current state. However, during the implementation process, it becomes evident that an additional transitional state is needed: preparing to power off (a series of operations that need to be executed during the power-off process).

Only when the logic is clear will one subconsciously notice that something is missing, especially regarding critical handling.

int sysState = POWER_OFF; // Default power off state
int ctrlCnt = 0;

/* Control Motor Function */
void MotorCtrlTask(void)
{
    if (ctrlCnt)
    {
        MotorCtrl(ON);
        delay(1);
        MotorCtrl(OFF);
    }
    else
    {
        MotorCtrl(OFF);
    }
}

void main(void)
{
    while (1)
    {
        switch (sysState)
        {
            case POWER_OFF: // Power off state
                sysState = POWER_ON; // Automatically switch to power on state
                ctrlCnt = 3;
                break;
            case POWER_ON:  // Power on process state
                ... // Other functionalities during the power on process

                if (ctrlCnt == 0) // Control ends, automatically switch to working state
                {
                    sysState = WORKING;
                    break;
                }
                break;
            case WORKING:  // Working state
                if (...) // Power off condition
                {
                    sysState = POWER_OFF_READY;
                    ctrlCnt = 3;
                    break;
                }

                if (keyPress)
                {
                    keyPress = false;
                    ctrlCnt = 1;
                }
                break;

            case POWER_OFF_READY:  // Preparing to power off
                ... // Other functionalities during the power off preparation

                if (ctrlCnt == 0) // Control ends, automatically exit
                {
                    sysState = POWER_OFF;
                    return; // Exit program
                }
                break;

            default:
                break;
        }

        MotorCtrlTask();

        if (ctrlCnt > 0)
            ctrlCnt--;
    }
}

Conclusion: From the above two pieces of code, which logic do you find clearer? The non-state machine implementation also lacks some exception handling. For example, what happens if the switch is pressed during the three motor adjustments in the power-on process? To avoid this situation, how many if conditions would need to be added?

Applicable Scenarios

State machines have a wide range of applications, not just in C language; they can be used in others as well. To be precise, this is a programming philosophy. Especially in business functionalities, state machines are commonly used.

For instance, commonly used modules also feature state machines: for example, the pressing and releasing of buttons, which includes pressing momentarily, pressing multiple times, pressing continuously, releasing momentarily, and continuously releasing.

Typically, state machines can be combined with table-driven methods, where the current state, conditions, actions, and next states serve as data, while the logic for executing these state transitions is implemented separately. They can be flexibly used according to actual situations.

Code References:

Menu: Menu control can be understood this way: the current menu interface is the current state, entering and exiting the menu are conditions, executing functions during menu transitions are actions, and understanding the upper and lower menu levels are next states, where the menu option table cleverly employs a combination of state machines and table-driven methods.

https://gitee.com/const-zpc/menu.git

ESP8266: AT command data table, including commands, expected responses, time, and function pointers [subsequent actions]. This can be understood as: the array index current value is the current state, receiving a response and timing out are conditions, executing functions upon receiving a response or timing out are actions, and the future value of the array index is the next state.

https://gitee.com/const-zpc/esp8266

Embedded Software Architecture Design - State Machine

Embedded Software Architecture Design - State Machine

Leave a Comment

Your email address will not be published. Required fields are marked *