Modern Embedded Systems Programming with QP Real-Time Event Frameworks
Many embedded developers say they use event-driven programming and state machines, yet their implementations often stop at message queues, event loops, and switch statements. Building true event-driven, hard-real-time systems requires a more advanced architectural foundation.
This workshop introduces the QP/C and QP/C++ Real-Time Event Frameworks—mature, lightweight, open-source frameworks that implement the Active Object (Actor) model and modern Hierarchical State Machines (UML statecharts). Participants will learn how this reusable architecture avoids shared-state concurrency, enables deterministic real-time behavior, and provides a scalable infrastructure for extensible embedded applications.
Through hands-on exercises on both a host PC and an STM32 NUCLEO board, attendees will explore the whole workflow of designing and implementing event-driven systems with QP. Topics include:
- Building and running QP examples to get started with the framework
- Implementing Hierarchical State Machines manually in C or C++
- Modeling UML statecharts graphically and generating code automatically
- Using time events to manage timing without blocking
- Executing Active Objects for hard real-time performance:
- with QP’s built-in kernels (non-preemptive QV, preemptive non-blocking QK)
- with traditional RTOSes (FreeRTOS, Zephyr)
- with general-purpose OSes (embedded Linux, Windows)
- Event delivery mechanisms: direct posting and publish–subscribe
- Event memory management: immutable events and mutable events with user-defined parameters
- Software tracing for debugging, optimization, and monitoring
- Trace-based testing of event-driven QP applications
By the end of the workshop, participants will understand what it truly means to architect embedded software around events, state machines, and non-blocking concurrency—and how QP frameworks and the ecosystem of the surrounding tools make this approach practical, maintainable, and certifiable.
Hi Nathan!
Thank you for this thoughtful comment — it goes right to the core of the workshop, which was blocking and, more importantly, how to avoid it. There is a lot to unpack here, so my answer will have a few sections.
Blocking
I realized that I may not exactly know what you mean by "blocking"...
In the workshop, blocking has a very specific meaning: explicit, in‑line waiting for something to happen. This includes both:
-
synchronization blocking — when a thread blocks itself by calling something like xSemaphoreTake() or xQueueReceive(), and
-
mutual‑exclusion blocking — when a thread needs a resource currently held by another thread (e.g., waiting on a mutex protecting an I2C peripheral).
The key characteristic is that blocking is explicit and synchronous. You put it directly in the code (it is hard-coded): the thread stops right there and waits. In a traditional RTOS architecture, everything is handled through this kind of blocking — both synchronization and mutual exclusion. That’s exactly what I illustrated in slides 9 and 10.
This explicit blocking is very different from preemption, which is asynchronous and invisible in the code. Preemption can interrupt you at any point (when it is not explicitly disabled), but it’s not something you write or control explicitly — it’s something the preemptive scheduler does to you.
How Active Objects Eliminate Blocking
how do AOs fix that?
Active Objects allow the non-blocking architecture by enabling two strategies:
-
Active Objects strictly encapsulate resources (such as the I2C peripheral) so that only the owner AO can access the resource, which eliminates the direct sharing and any need for mutual exclusion (by blocking). This strict ownership dictates that the rest of the system can only post events to the owner AO, using it as the "server" (a.k.a., "manager" or "broker") for the resource. The "server" AO naturally serializes the access to the resource by the fact that those request events are queued and then processed to completion (RTC). I admit that I didn't explicate this in the workshop because I talked about it in every EOC before (see my previous talks "Modern Embedded Software Goes Beyond the RTOS (2020)" and "Modern Embedded Programming with Hierarchical State Machines and Active Objects", still available in the archive).
-
This leaves events as the only artifacts shared among AOs (under the deterministic "zero-copy" event exchange policy of QP). Any potential concurrency hazards around sharing events are handled (in QP) by explicit event ownership transfers, which I've spent a lot of time explaining in slides 28 and 29.
In the end, the most important idea here is ownership. Rust made ownership famous, but the concept predates Rust and can be implemented in other languages and frameworks. QP is one such example: it applies ownership both to resources (peripherals) and events (data).
Single-Event Tasks
This does seem useful, but only for tasks that need to respond to multiple events...
Tasks that wait for a "single event" are typically structured as a variant of the event loop I discussed in slide 8. Here, it's easy to conflate the blocking mechanism with the various event signals (slide 22) delivered by that mechanism.
An event loop is not limited to blocking on a message queue. Some event loops block on select(). Others block on a time delay. The defining characteristic of an event‑loop thread is that it blocks in exactly one place and never blocks while processing the event. That’s what makes it extensible.
Threads that block only on a periodic time delay are typically input‑driven state machines (slide 35). They don't need external events, but rather poll for the specific combinations of the "inputs" on each pass through the loop. This works — and is extensible — precisely because blocking is avoided inside the state machine logic. (Input‑driven state machines have their own limitations, which I briefly mentioned.)
A task that truly needs to respond to only one event (event signal) is rare. Even if it starts that way, it almost always evolves to require more events. That's the inevitable change a good architecture should anticipate and make easy.
Trace-Based Testing with QUTest
I'm similarly bummed that we didn't get to hear the end of your presentation! I was very excited to see the slides about QUTest;
I’m also sorry we didn’t get to QUTest and its trace‑based testing strategy. I was especially looking forward to showing the recent results with 100% MC/DC coverage measured using the new GCC 15.2 toolchain — some of the newer QP tests demonstrate this beautifully.
Fortunately, I covered QUTest in detail in my earlier EOC talk, “Testing of Event‑Driven Embedded Software with Python,” which is still available in the EOC archives. That session walks through the whole idea of driving automated tests from trace data, both on the host and on real hardware.
Unfortunately, I ran out of time during the workshop to cover how QP integrates with various real‑time kernels. If you’d like to explore this topic further, the following video lessons walk through each execution model in detail:
Lesson 54 — Non‑preemptive QV Kernel for Active Objects
https://youtu.be/lXkAQR7TNOg?si=JM--dpWSREM-_h1K
Lesson 55 — Preemptive QK Kernel for Active Objects
https://youtu.be/QPQ5OQtqaV8?si=xJV-imiMfxAPK3fo
Lesson 56 — Zephyr RTOS for Active Objects
https://youtu.be/NDcsIehwGIE?si=_UKNHaI6S8g2JC2a
These lessons provide the full background and practical examples that I couldn’t fit into the live session. They also provide deeper answers to the questions asked during the workshop.
Thank you for a great presentation Miro. If there is a runaway event generator due to an implementation problem, are there any recommended steps for recovery. Also wonder about preventing and detecting this scenario
Hi Jeff,
I’m assuming that by a “runaway event generator” you mean a fault such as an unstable interrupt line or misconfigured peripheral that repeatedly triggers an ISR, which in turn posts or publishes events into the framework. When you use the regular event posting API, this condition will quickly overflow an event queue, and QP will detect the violation and assert. That assertion is your built‑in detection mechanism for this class of hardware or integration faults.
If, however, your system architecture requires tolerance to such behavior, QP provides a controlled, non‑asserting “best‑effort” delivery mode. Using the extended posting API (QACTIVE_POST_X()), you can specify a safety margin of free queue slots that must remain available. When that margin cannot be maintained, the post simply fails and the function returns false instead of triggering an assertion . The matching non‑asserting allocator (Q_NEW_X()) applies the same principle to event allocation.
This gives you a deterministic way to prevent a runaway source from destabilizing the system: events are dropped in a controlled manner, and the application can detect it and react to the failed post if needed.
At a higher level, an event‑driven system behaves no differently from a traditional RTOS‑based design in this respect. The same architectural questions apply: how do you detect a misbehaving interrupt source, how do you bound its impact, and what recovery strategy makes sense for your safety goals.
For anyone planning to participate in the hands‑on exercises, I highly recommend downloading and installing the most recent QP‑bundle for Windows from:
https://www.state-machine.com/
I will also assume familiarity with fundamental embedded concepts such as interrupts, race conditions, and basic embedded architectures—including both bare‑metal systems and RTOS‑based designs (threads, context switching, blocking, mutual exclusion, RMS/RMA).
If you’d like to brush up on any of these topics, I suggest watching the relevant lessons from the free Modern Embedded Systems Programming video course on YouTube:
https://www.youtube.com/playlist?list=PLPW8O6W-1chwyTzI3BHwBLbGQoPFxPAPM








Great workshop, Miro! I very much enjoy your work, as always.
I realized that I may not exactly know what you mean by "blocking" and how Active Objects fix that. I usually think of a thread being "blocked" if it needs to access a resource being held by another thread, such as a hardware peripheral or a piece of shared memory. This can cause problems, of course, but how do AOs fix that? If I have an AO that needs to do a burst read from an I2C device then something is blocking the other tasks from using the I2C peripheral during that bursty read. It would be the same for an RTOS task that acquired an "I2C mutex".
The other definition of "blocking" could be referring to a task which has blocked itself on a specific OS call:
xSemaphoreTake,xQueueReceive, etc. In this case, the tasks won't do anything until the waited-on condition occurs. An AO, however, would be in a "waiting for event" state and could still respond to other events within that state. This does seem useful, but only for tasks that need to respond to multiple events. An AO that defers events in a given state while looking for a singular event is functionally the same as that RTOS task that was waiting on a single OS call. And not all tasks may need to respond to more than one event in each of their states. So the flexibility of an AO seems to be not necessarily needed in every case (and can also be misused to circumvent the benefits).Also, I'm similarly bummed that we didn't get to hear the end of your presentation! I was very excited to see the slides about QUTest; it's a really neat idea to me to run an automated test suite based on log data from a system. You could run tests on a simulated host and even on a real device as it was streaming log data as it was running!