DEV Community

Emily Johnson
Emily Johnson

Posted on

Unlock Efficient Coding: Master Embedded Systems with Finite State Machines

Embedded systems, which interact with their environment through sensors and respond to changes, are inherently responsive. They can display information, activate motors, or send notifications to other systems. A prime example of a responsive system is a finite state machine, which always exists in one of a finite and well-defined set of possible states.

However, manually programming finite-state machines can lead to convoluted and difficult-to-maintain code. Graphical design tools simplify the process by allowing you to track all possible states and actions within your system. This article introduces you to programming with state machines using graphical design tools and shows you how to integrate the generated platform-independent code with custom hardware-specific code to interact with the hardware, such as an Arduino board.

State machines are ideal for developing responsive systems, which interact with their environment using sensors and actuators. Examples of sensors include motion, brightness, or temperature sensors, while commonly used actuators include LEDs, displays, valves, and motors. A key characteristic of these systems is that they possess a finite set of possible states and exist in exactly one of those, making state machines a natural fit.

A simple yet practical example of a state machine is a light switch control, as shown in Figure 1. It has two states — On and Off — and only one of both states can be active at the same time. A change of one state to the next occurs when a so-called transition is taken, such as when the buttonpressed event is raised.

     1
Enter fullscreen mode Exit fullscreen mode

buttonpressed

Fig
ure 1. The two states and transitions of a light switch control. (Source: itemis AG)

Visualizing your system with all its states can aid in planning and provide a clear understanding of your system’s expected behavior in different situations. By using a diagram as a blueprint for source code and tests, you can ensure that your system works as intended. To learn more about simplifying complex code and mastering embedded systems through finite state machines, visit https://carsnewstoday.com/.

If an individual attempts to craft tests based on an antiquated diagram, they will inevitably face defeat. This can lead to substantial problems if a model is solely utilized for specification or documentation purposes. Therefore, the diagram should not merely serve as a blueprint for the code; it should essentially embody the code itself.

Why bother writing code when you’ve already created the diagram? The required logic is already inherent in the diagram. Converting the diagram into equivalent source code in languages like Java or C is a straightforward, mechanical task that could be easily performed by a machine. This approach, where the diagram serves as the single source of truth and code is generated from it automatically, is known as the model-driven approach. However, a simple drawing board is insufficient for this purpose.

Instead, you should utilize a proper modeling tool like YAKINDU Statechart Tools to create the state diagram (statechart). Diagrams created with such a tool are easy to comprehend and facilitate effective communication between software developers and domain experts. Moreover, unlike a diagram on paper or in a drawing application, modeling tools possess a formal understanding of what a state machine is.

This enables them (and you) to simulate and test their behavior — without even writing a single line of code. The models themselves are platform-independent, allowing you to generate source code in any language you prefer from them. Tools typically support C, C++, Java, and Python.

If you’re still unsure about how model-driven software development works, don’t worry — we’ll now explore it through an example. We’ll use statecharts and code generation to develop a simple automated light with a few inputs and outputs.

Our example: automated and motion-activated lights

The task of automatic illumination is relatively straightforward: There should be light only when it’s dark, but it shouldn’t waste energy when no one is around. To achieve this, most staircase lights are controlled by a timer. By pushing a button, the light is activated, and after a certain time, it’s automatically switched off again. However, that would be a rather dull statechart example, so for this article, we’ve added an extra layer of complexity by incorporating an additional mode driven by a motion sensor.

The light should have three possible operation modes:

  • Permanently inactive
  • Activated — with time-controlled switch-off
  • Automatic — with motion sensor

A single button allows users to seamlessly switch between various operational modes, while two LEDs provide a visual indication of the currently active mode.

Based on these specifications, it’s relatively easy to deduce the fundamental architecture of the state machine, as depicted in Figure 2:

automated and motion-activated lights

Figure 2. Automated and motion-activated lights. (Source: itemis AG)

Transitions between the Off, Timer, and Motion_Automatic states are triggered by specific events — either by pressing the button or when a timer expires. If the user presses the button once, the timer mode is activated, and the light turns on, automatically switching off after 30 seconds. If the user presses the button again before the 30 seconds elapse, the motion sensor mode is activated. 

Whenever the motion sensor detects movement, the light is switched on, if necessary, for an additional 30 seconds. The timer is reset each time motion is detected. The two LEDs indicating the current mode are activated and deactivated as needed when entering or exiting their respective states. This way, the entire controller logic is fully encapsulated within the state machine, also known as the automaton.

If the automaton is intended to run on an embedded system, we can now generate C or C++ code directly from the diagram. The generated code contains all the logic from the model. Only the code that interfaces with the actual hardware needs to be written manually. 

In this example, this involves raising the button event when the actual button is pressed, controlling the actual staircase light, and controlling the status LEDs. This manual programming is necessary because the generated code is independent of the target platform. The same holds for the timers — the time is handled very differently on different target platforms.

There are numerous ways to implement a state machine. The most commonly used methods include state tables, switch-case based constructs, or — often used in object-oriented programming languages — the state pattern. If you want to delve deeper into this topic, you can find an extensive comparison in this whitepaper. By default, YAKINDU Statechart Tools generates state machine code using switch-case statements, ensuring good performance while maintaining good readability of the source code.

Unraveling the Intricacies of the Automatically Produced Code

As mentioned earlier, the state machine code is realized through a switch-case paradigm. The main execution sequence will be coordinated by the runCycle function:

void Lightswitch::runCycle() {
  clearOutEvents();
  for (stateConfVectorPosition = 0; stateConfVectorPosition
       < maxOrthogonalStates; stateConfVectorPosition++) {
    switch (stateConfVector[stateConfVectorPosition]) {
    case lightswitch_Off : {
      lightswitch_Off_react(true); break;
    }
    case lightswitch_Timer : {
      lightswitch_Timer_react(true); break;
    }
    case lightswitch_Motion_Automatic_motion_Motion : {
      lightswitch_Motion_Automatic_motion_Motion_react(true);
      break;
    }
    case lightswitch_Motion_Automatic_motion_No_Motion : {
      lightswitch_Motion_Automatic_motion_No_Motion_react(true);
      break;
    }
    default: break;
    }
  }
  clearInEvents();
}

The runCycle function will be called whenever an event is raised. It iterates over all orthogonal states to do whatever is to be done there. A switch-case statement decides which function to call to execute the corresponding state reaction. For example, the Off state has one entry reaction, setting the light variable to false, which will only be executed when entering the state. It has one outgoing and one incoming transition. If the button event is raised, the state will be exited. This behavior is handled in the lightswitch_Off_react function:

sc_boolean Lightswitch::lightswitch_Off_react(const sc_boolean
                                              try_transition)
{
  sc_boolean did_transition = try_transition;
  if (try_transition) {
    if (iface.button_raised) {
      exseq_lightswitch_Off();
      enseq_lightswitch_Timer_default();
      react();
    } else {
      did_transition = false;
    }
  }
  if ((did_transition) == (false)) {
    did_transition = react();
  }
  return did_transition;
}

Let's assume we've already shifted into the Off state. With every call to the runCycle function, it's vital to determine whether the button event has been activated or not. This determination process is overseen by the lightswitch_Off_react function. If the button event has indeed been activated, two pivotal actions must be undertaken: executing the exit sequence of the current state and initiating the enter sequence of the target state:

if (iface.button_raised) {
  exseq_lightswitch_Off(); enseq_lightswitch_Timer_default();
  react();
}

Animating the Design on an Arduino Uno Board

1 1

arduino uno setup

Figure 3. Arduino Uno Schematic Diagram. (Source: itemis AG)
The Arduino Uno implementation schematic is depicted in Figure 3. To maintain a straightforward circuit, the onboard LED serves as the actual staircase light. The two mode-displaying LEDs are connected to pins 9 and 10, while the motion sensor is linked to pin 7. If necessary, these pin assignments can be modified. However, the button must be connected to either pin 2 or 3, as only these pins can trigger interrupts. The LEDs are connected in series with a 220 Ω resistor, and the button is linked to a 22 kΩ pulldown resistor.

The software architecture consists of two primary components: the C++ code generated from the statechart and the handwritten glue code, which bridges the platform-independent state machine logic with the hardware.

The code generator creates the interface of the state machine based on the events and variables defined in the model: void raise_button(); void raise_motion(); sc_boolean get_light() const; sc_boolean get_led_timer() const; sc_boolean get_led_motion() const; void init(); void enter();

To interface with the state machine, an object of the specific state machine type must be defined, in this case, Lightswitch. This object embodies the actual state machine and enables programmatic interaction with it. For instance:

Lightswitch lightswitch;
int main(){
  lightswitch.init();
  lightswitch.enter();
  lightswitch.raise_button();
}

By implementing this straightforward approach, the lightswitch state machine will be initialized, entered, and the button event triggered. However, this is not the ideal solution. Our objective is to integrate the hardware components (in this case, the Arduino with connected LEDs, sensor, and button) with the state machine. To achieve this, we will utilize the state machine in a simple input-process-output pattern, which can be described as a straightforward loop consisting of the following stages:

  • Monitor the hardware and sensors for any changes
  • Translate this information into the state machine’s inputs
  • Allow the state machine to process these inputs
  • Examine the state machine’s outputs and respond accordingly

Initially, the timer is refreshed with the current time. On an Arduino, we employ the millis function to obtain the number of milliseconds that have elapsed since the system was started. If necessary, the timer will trigger time events within the state machine.

long now = millis();
if(now - time_ms > 0) {
  timerInterface->proceed(now - time_ms);
  time_ms = millis();
}

Based on other inputs, such as button presses or motion detections, we can trigger the “in” events of the state machine. Here, we don’t need to concern ourselves with the state machine’s current mode – the generated state machine code encapsulates all that logic. We simply trigger the event and leave it to the state machine to decide whether to respond to it or not.

// handle button press from ISR
if(buttonPressed) {
  lightswitch.raise_button();
  buttonPressed = false;
}
// read out motion sensor
if(digitalRead(7)) {
  lightswitch.raise_motion();
}

Following the processing of all "in" events, the state machine configures the boolean variables accordingly. These variables can then be leveraged to regulate the "stair light" and the indicator LEDs.

// configure lighting
digitalWrite(13, lightswitch.get_light());
// configure mode indicators
digitalWrite(9, lightswitch.get_led_timer());
digitalWrite(10, lightswitch.get_led_motion());

Ultimately, the Arduino will be placed in sleep mode if it is in the Dormant state, thereby conserving energy. If the user presses the button, its interrupt service routine will be triggered, and the Arduino will resume operation. Note that the timer responsible for updating the value returned by millis remains inactive during sleep mode. Consequently, software timers reliant on millis will not be updated during this period. In this example, no timers are active while the Dormant state is engaged, allowing the Arduino to safely enter sleep mode.

// if in Dormant state, enter sleep mode (wake up via ISR) if(lightswitch.isStateActive(Lightswitch::lightswitch_Off)) {
enterSleep();
}

Configuring the Arduino board is facilitated through the utilization of the standard Arduino Integrated Development Environment (IDE). To accomplish this, we integrate the project containing the state machine as a library and manually develop only the Arduino-specific code, as exemplified above, within the Arduino IDE.

Key Takeaways

This example serves as a compelling demonstration of the advantages of leveraging models, such as statecharts, in software development. The primary benefits of this approach include:

  • State machines possess a sufficient level of formality to enable seamless execution and integration.
  • The visual nature of statecharts makes them readily accessible and easy to understand.
  • The execution logic of a device and the associated hardware-related code are effectively decoupled, allowing for enhanced flexibility and adaptability.
  • This decoupling of hardware and device logic promotes greater portability and reduces the effort required for modifications or subsequent iterations.
  • Moreover, they can be developed independently of each other, thereby streamlining the development process and improving overall efficiency.

This example provides a solid foundation for further exploration and experimentation with state machines. The project is available in the repository of YAKINDU Statechart Tools.

Top comments (0)