Integrating State Machines with RTOS: A Practical Approach with itemis CREATE and Zephyr

Integrating State Machines with RTOS: A Practical Approach with itemis CREATE and Zephyr

Integrating itemis CREATE with real-time operating systems like Zephyr, FreeRTOS, and ThreadX on embedded systems enables developers to manage state transitions seamlessly based on real-time events and timer-driven tasks. By leveraging RTOS tasks or threads, itemis CREATE’s graphical statecharts can be translated into efficient C or C++ code for embedded devices, providing a powerful tool for developing state-driven applications.

In this blog, we’ll explore how itemis CREATE supports you in implementing embedded applications with state machines in Zephyr. We’ll also walk through how the generated state machine code integrates into your Zephyr-based application, and show how similar concepts can be applied to other RTOS platforms like FreeRTOS or ThreadX. This makes itemis CREATE a versatile choice for embedded development across different RTOS environments, simplifying designing, generating, and deploying embedded statecharts effectively.

The Light Switch Example

There is already a comprehensive example and documentation, which shows how to generate C code and how it can be used in a desktop environment:

In the example, the three most common key components of itemis CREATE are used, which are relevant to communicate with a state machine:

  • Input events
  • Output events
  • Timed events


Light Switch State Machine

This simple statechart illustrates a toggle system where the light can be turned on, adjusted in brightness, and turned off manually or automatically after a delay. The On button gradually increases brightness each time it's pressed, up to a maximum of 10. The Off button or a 30-second timeout switches the light off:

  1. Initial Transition: The system starts with an initial transition that leads to the Off state.
  2. Off State: Entry Action: When entering the Off state, the light’s brightness is set to 0, and an event light.off is raised to indicate the light is off. Transition to On: When the user.on_button event is received, the system transitions from Off to On, setting light.brightness to 1 in the process.
  3. On State: Entry Action: When entering the On state, an event light.on is raised to indicate the light is on. Increasing Brightness: If the user.on_button event is received again while in the On state and the brightness is less than 10, the light's brightness increases by 1 each time the button is pressed. Transition to Off: The system transitions back to the Off state when either the user.off_button event is received or after 30 seconds have passed.

Setup the State Machine

Several steps are needed to set up the state machine for our application. This setup includes initializing a timer service to handle timed events and configuring the state machine itself, so it’s ready to process inputs and produce outputs. Those steps are independent of the RTOS integration and are commonly done using the state machine’s code.

Step 1: Set Up the Timer

The first part of our setup is to create a timer service. This service will manage multiple timers, each capable of triggering specific events in the state machine. Here’s how we configure it:

#define MAX_TIMERS 4
static sc_timer_t timers[MAX_TIMERS];
static sc_timer_service_t timer_service;
sc_timer_service_init(&timer_service, timers, MAX_TIMERS, (sc_raise_time_event_fp)&lightSwitch_raise_time_event);        

Defining MAX_TIMERS:

  • MAX_TIMERS sets the maximum number of timers the service can manage once. Here, we define it as 4, meaning the state machine can handle up to 4 independent timers running concurrently.

Creating the timers Array:

  • timers is an array of timer structures (sc_timer_t) that will store the individual timers. Each timer can be controlled independently, allowing for complex time-based behavior within the state machine.

Initializing timer_service:

  • timer_service is the main manager for all timers. By initializing it with sc_timer_service_init, we connect it to our timers array, set its capacity to MAX_TIMERS, and define lightSwitch_raise_time_event as the callback function that will be triggered when any timer expires.
  • The callback function, lightSwitch_raise_time_event, is crucial - it notifies the state machine that a specific timer has reached its end, allowing it to respond to time-based events.

With this timer setup, the state machine has a time management system that can handle delayed actions, periodic triggers, and other time-sensitive tasks.

Step 2: Set Up the State Machine

With the timer service ready, we can initialize the state machine itself. This setup ensures the state machine is ready to process events and communicate with other parts of the application:

LightSwitch lightSwitch;
lightSwitch_init(&lightSwitch);
subscribe_observers(&lightSwitch, &lightOnObserver, &lightOffObserver);
lightSwitch_enter(&lightSwitch);        

Create the State Machine Instance:

  • lightSwitch is the main instance of the LightSwitch state machine. This structure will handle the logic for our application’s states, transitions, and event responses.

Initialize the State Machine (lightSwitch_init):

  • lightSwitch_init sets up the state machine’s internal structures and configures it for operation. This step is essential for preparing the state machine to receive and process events.

Subscribe to Output Events:

  • subscribe_observers connects output events to observer functions that will handle the events generated by the state machine. Here, lightOnObserver and lightOffObserver are observers that react to the lightSwitch events, such as turning a light on or off.
  • This subscription process allows external components to “listen” for events generated by the state machine and respond accordingly.

Enter the State Machine (lightSwitch_enter):

  • lightSwitch_enter starts the state machine and moves it into its initial state. This step is where the state machine begins its operation, ready to receive inputs and produce outputs based on its configured logic.

Key Components of the Zephyr RTOS Integration

RTOS message queues serve as the bridge between RTOS tasks and the state machine, ensuring that events and timer updates are processed efficiently without blocking other system operations. Every participant uses the event queue to store upcoming events that will be raised in the state machine. Two main tasks handle this process: one reads input events (like user.on_button and user.off_button), and the other updates the timer service. Both tasks push events into the event queue whenever an event needs to be raised. A separate task or thread then processes the event queue, forwarding each event to the state machine for handling. Events are identified by simple integer IDs, making the system efficient and easy to manage. A generic way of doing this is shown in the following image:


Threads/Tasks with Queues

  • Timer (blue circle): A thread that periodically generates a time event and sends them to the input queue.
  • Input (another blue circle): Represents a thread that handles external inputs and also sent the input events to the input queue. An input could be an asynchronous event like a button click.
  • Input Queue: This queue collects events from both the timer and input thread.
  • State Machine (green circle): The core processing thread, which dispatches the events from the input queue and raises events in the generated state machine code based on the input events.
  • Output Queue: Collects output events or actions from the state machine and forwards them to the output thread.
  • Output (yellow circle): Represents the thread of processed outputs from the output queue. It handles actions that interact with other systems or provide feedback.

Raise time events

In the main function, we initialize and start a timer to ensure consistent updates to the timer service. This setup is crucial for handling timed events efficiently within? Zephyr RTOS.

The initialisation and start of the timer are achieved with the following calls:

k_timer_init(&timer_service_update_timer, timer_service_callback, NULL);
k_timer_start(&timer_service_update_timer, K_MSEC(100), K_MSEC(100));        

  • k_timer_init sets up the timer (timer_service_update_timer) with a callback function, timer_service_callback, which will be invoked whenever the timer expires.
  • k_timer_start then activates this timer with an initial delay and an interval of 100 milliseconds (both specified with K_MSEC(100)). This means that every 100 ms, the timer_service_callback function will be triggered.

Within the callback function timer_service_callback, we handle the timer expiration event:

void timer_service_callback(struct k_timer *unused1) {
    uint32_t event_id = updateTimerServiceID;
    k_msgq_put(&event_queue, &event_id, K_NO_WAIT);
}        

In timer_service_callback:

  1. A unique integer ID (updateTimerServiceID) is assigned to represent the timer update event.
  2. The event is then pushed to the message queue event_queue using k_msgq_put. The K_NO_WAIT argument ensures that if the queue is full, the function returns immediately without blocking.

This mechanism ensures that the timer service sends a regular event into the event_queue, where it will eventually be processed by a separate task that raises the event within the state machine. By identifying events with integer IDs, this design remains lightweight and responsive, ideal for embedded systems with limited resources.

Raise button events

This task is designed for a Linux environment, where keyboard input is used to trigger "on" and "off" events for the state machine. By creating a dedicated thread to handle keyboard polling, the system remains responsive to user input while operating within the RTOS.

Defining and Starting the Keyboard Polling Thread

To handle keyboard input, a thread is defined and started in the main function:

K_THREAD_STACK_DEFINE(keyboard_poll_stack_area, STACK_SIZE); struct k_thread keyboard_poll_thread_data;
k_thread_create(&keyboard_poll_thread_data, keyboard_poll_stack_area, K_THREAD_STACK_SIZEOF(keyboard_poll_stack_area), keyboard_poll_thread, NULL, NULL, NULL, PRIORITY + 1, 0, K_NO_WAIT);        

  • K_THREAD_STACK_DEFINE allocates memory for the thread’s stack, keyboard_poll_stack_area.
  • k_thread_create starts the thread, executing the keyboard_poll_thread function, which monitors keyboard input.

In k_thread_create:

  • K_THREAD_STACK_SIZEOF specifies the size of the stack.
  • PRIORITY + 1 sets the thread priority.
  • K_NO_WAIT ensures the thread starts immediately.

Non-Blocking Keyboard Polling Callback

The keyboard_poll_thread function reads keyboard input non-blocking, checking if a specific key is pressed and raising events accordingly. Each time the user presses a key, the corresponding event is pushed into event_queue, where a separate task processes it and raises it within the state machine. This design allows user input to be handled independently of other RTOS tasks, maintaining responsiveness and modularity. The details can be looked up in the example.

Handling Events with RTOS Message Queues

The event_delegater_thread acts as the bridge between queued events and the state machine, processing each event and triggering the corresponding state transition or action. This thread ensures that all events are properly delegated to the state machine.

Defining and Starting the Event Delegater Thread

In the main function, the event_delegater_thread is created and configured as follows:

K_THREAD_STACK_DEFINE(event_delegater_stack_area, STACK_SIZE); struct k_thread event_delegater_thread_data; 
k_thread_create(&event_delegater_thread_data, event_delegater_stack_area, K_THREAD_STACK_SIZEOF(event_delegater_stack_area), event_delegater_thread, &lightSwitch, NULL, NULL, PRIORITY, 0, K_NO_WAIT);        

  • K_THREAD_STACK_DEFINE allocates memory for the stack area, event_delegater_stack_area.
  • k_thread_create starts event_delegater_thread, passing a pointer to lightSwitch (the state machine object) as an argument, so it can be accessed and modified by the thread.
  • The PRIORITY value assigns this thread’s priority level, while K_NO_WAIT ensures immediate execution.

The Event Delegater Thread Callback

The event_delegater_thread function is designed to retrieve events from the event_queue and raise them within the state machine:

void event_delegater_thread(void lightSwitch, void unused1, void unused2) { 
    LightSwitch ls = (LightSwitch *)lightSwitch; 

    uint32_t receivedTaskID; while (1) {
    ? ? k_msgq_get(&event_queue, &receivedTaskID, K_FOREVER);

    ? ? if (receivedTaskID == updateTimerServiceID) {
            sc_timer_service_proceed(&timer_service, 100);
        } else if (receivedTaskID == raiseOnButtonID) {
            lightSwitch_user_raise_on_button(ls);
        } else if (receivedTaskID == raiseOffButtonID) {
            lightSwitch_user_raise_off_button(ls);
        } else if (receivedTaskID == displayBrightnessID) {
            printk("Brightness: %d.\n", lightSwitch_light_get_brightness(ls)); 
        }

        k_yield();
    }
}        

In event_delegater_thread:

Retrieving Events: k_msgq_get waits indefinitely (K_FOREVER) until an event is available in event_queue. This ensures the thread is only active when needed.

Handling Events: Based on the receivedTaskID (an integer ID identifying the event type):

  • Timer Service Update: When updateTimerServiceID is received, sc_timer_service_proceed is called with a 100 ms update to keep the timer service in sync.
  • Raise On and Off Events: When the IDs for the "on" and "off" buttons are received, lightSwitch_user_raise_on_button and lightSwitch_user_raise_off_button are called to raise these events in the state machine, triggering corresponding transitions.
  • Display Brightness: If displayBrightnessID is received, the current brightness level is printed, allowing real-time feedback from the state machine.

Yielding Control: After processing each event, k_yield allows other threads to execute, improving system responsiveness.

Summary

This example demonstrates integrating state machines with Zephyr RTOS in a modular and efficient manner. The system relies on three key tasks:

  1. Timer Update Task: Uses an RTOS timer to send periodic timer update events to the message queue. This ensures the state machine’s timing is consistently updated.
  2. Keyboard Polling Task: Reads keyboard input non-blocking on Linux, raising "on" and "off" events when specific keys are pressed. Events are pushed into the queue for processing.
  3. Event Delegater Task: Acts as the core event processor, continuously pulling events from the queue and raising them in the state machine. It handles timing updates, input events, and triggers actions like displaying brightness levels.

While this example shows the integration of a single state machine, integrating multiple state machines follows the same approach.?

Each event is identified by a unique integer ID, making the setup lightweight and easy to extend. This structure enables a responsive, state-driven application that efficiently uses RTOS tasks and queues to manage complex interactions. The full code and setup are available in the itemis CREATE example repository and a general approach to integrate any RTOS can be found in the Embedded Systems Integration Guide.

Henning B?ger

Head of Embedded Systems & Robotics @ MaibornWolff | Speaker

1 个月

Thanks for the down-to-earth article - especially the code snippets provide a good insight how the integration is done

回复
Peter Westergerling

Senior Account Manager bei der codecentric AG | #gerneperdu

3 个月

Sebastian Baier evtl. interessant für dich?

Great article, thanks a lot for sharing this detailed insight!

Axel Terfloth

Head R&D Embedded Systems at itemis

4 个月

The article and example serves as a perfect blueprint for all those who are using Zephyr RTOS and want to specify and implement their application logic using state machines. Do you also know that everyone can contribute to itemis CREATE examples. So if you have a nice project which makes use of itemis CREATE statecharts which you want to share than simply open a PR in our examples fit repo: https://github.com/itemisCREATE/examples/tree/release5

要查看或添加评论,请登录

社区洞察

其他会员也浏览了