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:
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:
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:
Creating the timers Array:
Initializing timer_service:
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:
Initialize the State Machine (lightSwitch_init):
Subscribe to Output Events:
Enter the State Machine (lightSwitch_enter):
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:
领英推荐
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));
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:
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);
In k_thread_create:
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);
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):
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:
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.
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
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!
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