Hello! Welcome to your next lesson in the "System Design for Embedded Applications" module.
In our last lesson, we focused on the design of a single, crucial software component: a layered device driver. We established that a robust driver architecture separates the device-specific protocol logic from the MCU-specific physical bus access. Such components are the source and sink of asynchronous events in our system.
Today, we'll zoom out to the system level. Once you have multiple drivers, user inputs, network stacks, and timers all generating events concurrently, how do you architect the main application to handle this complexity in a responsive, scalable, and maintainable way? Simply stringing together blocking function calls in a main loop quickly leads to an unresponsive and unmanageable system.
This lesson directly addresses that challenge.
Your learning outcome for this lesson is to: Design an event-driven system using a reactor pattern or state machines for managing complex, asynchronous interactions.
We'll explore two powerful, complementary patterns that are cornerstones of modern embedded system design and frequently discussed in senior-level interviews.
1. The Core Problem: The Pitfalls of Blocking
Before we dive into design patterns, it's critical to understand the problem they solve. Traditional sequential programming, where the flow of control is linear and functions block until they complete, is fundamentally at odds with the nature of embedded systems, which must react to unpredictable external stimuli.
To understand why, let's look at the origins of event-driven programming and see a powerful demonstration of why blocking is so problematic.
#33 Event-Driven Programming Part-1: GUI example, events, event-loop, run-to-completion, no-blocking
The video 'Event-Driven Programming Part-1' from the Quantum Leaps channel provides an excellent explanation of the shift from sequential to event-driven thinking. While the example uses a PC-based GUI, the core principles are identical for any embedded system that needs to remain responsive to multiple inputs.
Please watch the following two segments: The Event Loop and its Properties (09:01 - 12:38): Focus on the three key properties the video identifies: asynchronous event delivery via a queue, run-to-completion (RTC) semantics, and inversion of control. This 'inversion of control' is the defining characteristic of an event-driven framework. The Dangers of Blocking (20:51 - 26:52): This is the most crucial part. The author deliberately introduces a Sleep() call—a blocking delay—into an event handler. Pay close attention to the consequences: the application becomes unresponsive ('freezes'), and events get queued up, leading to strange behavior. This perfectly illustrates why blocking calls are forbidden in event-driven code and how they violate the RTC principle.
The key takeaway is clear: in an event-driven system, application code must never block. Any long-running operation must be broken down into non-blocking, run-to-completion steps that are managed by events. Our two design patterns provide the structure to do exactly this.
2. Pattern 1: Hierarchical State Machines (HSMs) for Modeling Behavior
State machines are a natural way to describe the behavior of a system that has distinct modes or states. You have likely implemented simple state machines using switch statements. However, as complexity grows, these "flat" state machines become a tangled mess of transitions, often called "spaghetti code."
Hierarchical State Machines (HSMs) solve this problem by introducing nesting and inheritance. A substate automatically inherits the behavior (event handling) of its superstate. You only need to code the differences in behavior.

The Active Object (Actor) Model
In a concurrent system (like one using an RTOS), HSMs are often encapsulated within Active Objects (also called Actors). An Active Object is an object that owns a thread of control and an event queue. It communicates with other active objects only by asynchronously posting events to their queues. This "share nothing, communicate by events" philosophy eliminates the need for most locks and mutexes, preventing a whole class of concurrency bugs by design. The HSM then becomes the "engine" that processes events for its Active Object.
Let's watch a short video that introduces this powerful design philosophy.
Active Objects, Hierarchical State Machines and Modeling in Practice
This video, 'Active Objects, Hierarchical State Machines and Modeling in Practice', sets the stage for our design. It explains the 'why' behind the Active Object pattern and outlines the design process.
Watch the segment from (03:48) to (14:58). Focus on: Active Objects vs. RTOS Threads (03:48 - 06:02): Understand the 'no-sharing' and 'no-blocking' principles that make Active Objects a safer alternative to traditional shared-memory multithreading. Partitioning the Problem (06:02 - 08:58): The design process starts by identifying components (nouns in the requirements) and assigning resources to enforce the 'share nothing' rule. This is a critical first step in any system design. Identifying and Exchanging Events (08:58 - 14:58): See how sequence diagrams are used to map out the flow of events between different active objects. This helps define the system's dynamic behavior before writing any code.
Implementing an HSM in C
Now that we understand the philosophy, let's look at a concrete, high-performance C implementation. This is the kind of deep, practical knowledge that interviewers at top semiconductor companies look for.
The paper 'State-Oriented Programming' by Miro Samek is a foundational text describing a lightweight yet powerful technique for implementing HSMs directly in C/C++. It uses function pointers to create a flexible and efficient state machine engine.
This is a dense but rewarding paper. Please read the following parts, focusing on the C implementation: Implementation Structure (Figure 1 and surrounding text): Understand the core structures: Msg, State (with its function pointer hndlr and super pointer), and Hsm. C Header File (Listing 1a): Review the C header file to see how these structures are defined. Note the STATE_TRAN macro, which is the mechanism for triggering transitions. The HSM Engine (Listing 2a): This is the heart of the pattern. Trace the logic in HsmOnEvent(). See how it starts at the current state (me->curr) and walks up the hierarchy (s = s->super) until an event is handled (returns NULL). Example (Figure 3 & Listing 3a): Connect the state diagram of the digital watch to its C implementation. See how each state has a corresponding handler function (WatchTimekeepingHndlr) and how the constructor (WatchCtor) wires up the state hierarchy by setting the super pointers. Your goal is not to memorize the code, but to understand the pattern: states are objects with pointers to their handler function and their superstate. An event dispatch engine walks this hierarchy.
HSMs provide the behavioral logic, but they don't solve the problem of efficiently waiting for events from many different sources (e.g., a UART, an Ethernet socket, a GPIO interrupt). For that, we turn to our second pattern.
3. Pattern 2: The Reactor for Event Demultiplexing
The Reactor pattern provides a solution to this. It allows a single thread to efficiently manage I/O events from multiple sources without blocking. It inverts the flow of control: instead of your code calling read() and blocking, you register an event handler with the Reactor and tell it you're interested in "read" events. The Reactor uses a synchronous event demultiplexer (like select() or poll() on POSIX systems) to block until any of the registered sources has an event. It then "reacts" by dispatching the event to the appropriate handler.

Let's examine a C implementation of this pattern. You'll notice it shares design elements (function pointers, void* context) with the HAL we built in the last lesson.
Patterns in C - Part 5: REACTOR
The paper 'Patterns in C - Part 5: REACTOR' by Adam Tornhill details a clean implementation of the Reactor pattern in C.
Please read through the article, focusing on these sections: The REACTOR Pattern (Figure 2 and text): Understand the roles of the four participants: EventHandler (the interface), Handle (the resource, e.g., a socket), concrete handlers (like DiagnosticsServer), and the Reactor itself. Implementation Mechanism (Listing 1, 2, 3): See how the EventHandler interface is defined in C using a struct of function pointers (getHandleFunc, handleEventFunc). This should be very familiar. Implementing the Event Loop (Listing 7): This is the core of the pattern. The HandleEvents function calls poll() to wait for events. When poll() returns, the dispatchSignalledHandles function iterates through the results and calls the handleEvent function pointer for any source that has data. Consequences: Read the summary of benefits. Note especially points 1 (Single-Responsibility), 2 (Open-Closed), and 4 (Provides an alternative to multithreading).
4. Tying It All Together: Reactor + Active Objects
The Reactor pattern and the Active Object/HSM pattern are not mutually exclusive; they are a perfect match. In a sophisticated embedded system, you would use them together:
- The Reactor serves as the main event loop for the entire system or a major subsystem. It waits for low-level events from hardware (e.g., "data available on UART2," "timer 3 expired," "Ethernet packet received").
- The concrete event handlers registered with the Reactor are very lightweight. Their only job is to translate the low-level hardware event into a logical system event and post it to the queue of the appropriate Active Object.
- The Active Object, running in its own thread, wakes up when an event arrives in its queue. It dispatches this event to its internal HSM, which contains all the complex behavioral logic for that component.
This architecture gives you the best of both worlds:
- Efficiency: The Reactor waits for multiple event sources in a single blocked thread.
- Responsiveness: The system immediately reacts to any event.
- Concurrency Safety: Active Objects don't share data, so complex locking is avoided.
- Maintainability: Behavior is encapsulated cleanly within HSMs, and I/O handling is encapsulated within the Reactor.
This combined pattern is a powerful answer to a system design question like, "Design the software architecture for a gateway device that bridges a CAN bus and an Ethernet network." You would have a CAN Active Object and a TCP/IP Active Object, both driven by a Reactor handling the low-level I/O.
Conclusion
In this lesson, we explored two advanced and essential design patterns for managing the complexity of event-driven embedded systems.
Key Takeaways:
- Avoid Blocking: Event-driven code must be non-blocking and execute in short, run-to-completion (RTC) steps to maintain system responsiveness.
- Model Behavior with HSMs: Hierarchical State Machines provide a robust, scalable, and maintainable way to implement complex state-dependent behavior, especially when used within the Active Object pattern.
- Demultiplex Events with a Reactor: The Reactor pattern provides an efficient mechanism to wait for and dispatch events from multiple I/O sources to their corresponding handlers without resorting to a complex multi-threaded design.
- Combine for Power: The most powerful event-driven architectures often use a Reactor to handle low-level I/O dispatch, which then feeds events into a system of Active Objects whose internal logic is managed by HSMs.
Preview of the Next Lesson:
We have now concluded the "System Design for Embedded Applications" module. You've learned to architect systems from a high level and design critical components with an eye towards portability and maintainability. The next logical step is to bring these designs to life on actual hardware and verify their correctness.
Our next module, "Advanced Debugging and System Profiling," will give you the practical skills to do just that. We will begin with the most fundamental hardware debugging tool: Use a JTAG/SWD debugger to set hardware/software breakpoints, inspect memory and CPU registers, and step through code on-target.