Electronics Guide

Embedded Software Architecture

Embedded software architecture defines the fundamental organization of software within embedded systems, establishing the structures, components, and relationships that shape how code is designed, implemented, and maintained. Unlike general-purpose computing where standardized operating systems and frameworks provide architectural foundations, embedded systems require careful architectural decisions tailored to specific constraints including real-time requirements, limited resources, safety criticality, and long operational lifetimes.

A well-designed embedded software architecture enables teams to manage complexity, achieve reliability goals, accommodate change, and maintain systems over decades of deployment. This article explores the architectural patterns, design approaches, and structural principles that guide the development of robust embedded software systems.

Architectural Fundamentals

Embedded software architecture encompasses decisions that have system-wide impact and are difficult to change once implemented. Understanding fundamental architectural concepts provides the foundation for making informed design decisions.

What is Software Architecture

Software architecture refers to the high-level structure of a software system, the discipline of creating such structures, and the documentation of these structures. In embedded systems, architecture addresses how software components are organized, how they interact with hardware and each other, and how the system meets its quality requirements.

Architectural decisions differ from detailed design in their scope and impact. While detailed design addresses how individual modules or functions work internally, architecture addresses how major system components fit together, how data flows through the system, and how the system responds to its operational environment. Architectural decisions are typically expensive to change once implemented, making early architectural analysis critical.

Good embedded software architecture balances multiple concerns including functionality, performance, reliability, maintainability, testability, and resource utilization. These concerns often conflict, requiring architects to make informed trade-offs based on system priorities and constraints.

Architectural Drivers

Architectural decisions in embedded systems are driven by requirements that go beyond functional specifications:

Real-time constraints: Many embedded systems must respond to events within strict time bounds. Architecture must ensure that time-critical operations can execute with guaranteed latency and that the system can meet deadlines under worst-case conditions.

Resource constraints: Limited memory, processing power, and energy budgets shape architectural choices. Architecture must ensure efficient resource utilization while maintaining system functionality and quality attributes.

Safety requirements: Safety-critical systems require architectures that support fault detection, containment, and recovery. Architectural patterns for safety-critical systems must enable certification and provide evidence of reliability.

Longevity: Embedded systems often operate for decades, requiring architectures that accommodate component obsolescence, evolving requirements, and long-term maintenance by personnel who did not develop the original system.

Hardware dependencies: Unlike application software that runs on standardized platforms, embedded software interacts directly with specific hardware. Architecture must manage these dependencies while enabling portability where needed.

Architectural Views

Complex systems require multiple views to fully describe their architecture, as no single view captures all relevant aspects:

Structural view: This view describes the system's static organization into modules, components, and their relationships. It shows how code is organized into units, how these units depend on each other, and how they are grouped into subsystems.

Behavioral view: The behavioral view describes how the system operates dynamically, including control flow, data flow, state transitions, and timing relationships. This view is particularly important for real-time systems where timing behavior must be analyzed.

Deployment view: This view maps software elements to hardware elements, showing how software components are allocated to processors, memory regions, and communication channels. In distributed embedded systems, deployment views are essential for understanding system topology.

Development view: The development view describes how the codebase is organized for development, including directory structures, build configurations, and team responsibilities. This view supports development processes and configuration management.

Architectural Quality Attributes

Quality attributes describe system properties that architecture must support:

Reliability: The system performs its intended functions correctly under stated conditions. Architecture supports reliability through error handling strategies, redundancy, and failure isolation.

Maintainability: The system can be modified to correct defects, improve performance, or adapt to changed requirements. Modular architecture with clear interfaces supports maintainability.

Testability: The system can be tested effectively at various levels. Architecture that separates concerns and provides clear interfaces enables comprehensive testing.

Portability: The system can be adapted to different hardware platforms. Layered architectures with hardware abstraction support portability goals.

Performance: The system meets timing, throughput, and resource utilization requirements. Performance-sensitive architectures minimize overhead and enable optimization.

Layered Architecture

Layered architecture is the most common pattern in embedded software, organizing code into horizontal layers where each layer provides services to the layer above while using services from the layer below. This pattern manages complexity by separating concerns and controlling dependencies.

Layer Concepts

In a layered architecture, each layer has a specific responsibility and a defined relationship with adjacent layers. Lower layers are closer to hardware and provide more fundamental services; higher layers are closer to application functionality and deal with more abstract concepts.

The key principle of layered architecture is that dependencies flow downward only. A layer may depend on layers below it but never on layers above it. This constraint prevents circular dependencies and ensures that lower layers can be developed and tested independently of higher layers.

Layers communicate through defined interfaces. The interface defines what services a layer provides without exposing how those services are implemented. This separation of interface from implementation enables layers to evolve independently as long as interfaces remain stable.

The number and definition of layers varies between systems, but typical embedded systems include hardware abstraction, device drivers, operating system or runtime services, middleware, and application layers. The appropriate layering depends on system complexity, reuse requirements, and team organization.

Hardware Abstraction Layer

The Hardware Abstraction Layer (HAL) sits at the bottom of the software stack, providing a consistent interface to hardware resources regardless of specific hardware implementations. The HAL hides hardware details from higher layers, enabling portability and simplifying hardware changes.

HAL functions typically provide access to processor features such as interrupt control and memory protection, peripheral registers through read and write operations, timing services including delays and timestamps, and basic input/output operations for GPIO and simple peripherals.

Effective HAL design requires balancing abstraction benefits against performance overhead. The HAL should be thin enough to avoid unnecessary overhead while providing sufficient abstraction for portability. Inline functions and macros can provide abstraction without runtime cost for simple operations.

The HAL typically includes board-specific configuration that maps abstract resources to physical hardware. This configuration captures pin assignments, clock settings, memory maps, and peripheral configurations, enabling the same higher-level code to work across different hardware boards.

Device Driver Layer

The device driver layer builds on the HAL to provide higher-level interfaces to hardware devices. While the HAL deals with register access and basic hardware operations, device drivers implement complete device functionality including initialization, configuration, and operational modes.

Device drivers encapsulate the complexity of device operation, handling state management, timing requirements, protocol implementation, and error recovery. Applications interact with drivers through clean interfaces that hide device-specific details.

Drivers typically provide initialization functions that configure hardware and prepare for operation, control functions that set operating modes and parameters, data transfer functions that move data between the system and device, and status functions that report device state and error conditions.

Driver interfaces should be consistent across similar device types, enabling applications to work with different devices through common patterns. A serial communication interface, for example, should work similarly whether implemented for UART, SPI, or USB virtual COM port.

Operating System Layer

In systems using an operating system or RTOS, the operating system layer provides services for task management, synchronization, timing, and resource allocation. Even bare-metal systems often include runtime services that provide similar functionality without a full operating system.

Operating system services typically include task or thread management with scheduling and context switching, synchronization primitives such as semaphores, mutexes, and events, inter-task communication through message queues and mailboxes, memory management for dynamic allocation and memory pools, and timing services for delays, timeouts, and periodic execution.

The operating system layer presents uniform interfaces to higher layers regardless of the specific RTOS or runtime implementation. This abstraction enables application code to be portable across different RTOS choices and supports migration between operating systems.

In resource-constrained systems, the operating system layer may be minimal or absent, with applications directly managing system resources. The architectural principles of separation and abstraction remain valuable even without a full operating system.

Middleware Layer

The middleware layer provides domain-specific services that sit between operating system services and application logic. Middleware implements functionality needed by applications but not provided by the operating system.

Common middleware components include communication protocol stacks implementing TCP/IP, Bluetooth, CAN, or other protocols, file systems providing data storage and retrieval services, graphics libraries supporting display and user interface development, and security services providing encryption, authentication, and secure communication.

Middleware components are often obtained from third parties or developed for reuse across multiple products. Clear interfaces between middleware and adjacent layers enable middleware components to be replaced or upgraded without affecting other system parts.

The boundary between operating system and middleware is sometimes blurred, with some RTOS implementations including middleware functionality. The key architectural principle is that these services are separated from application logic and accessed through defined interfaces.

Application Layer

The application layer implements the system's primary functionality, the features and behaviors that users or external systems interact with. This layer uses services from lower layers but focuses on application-specific logic rather than system infrastructure.

Application architecture within the application layer may follow various patterns depending on system requirements. Simple systems may have monolithic application code, while complex systems may use component-based designs, service-oriented architectures, or other patterns within the application layer.

The application layer should be largely independent of specific hardware and operating system choices. When architectural goals are met, application code should be testable on development hosts, portable across hardware platforms, and maintainable by engineers focused on application domain rather than system infrastructure.

Strict Versus Relaxed Layering

Layered architectures can follow strict or relaxed layering rules:

Strict layering: Each layer can only access the layer immediately below it. This approach provides maximum isolation and simplest dependency management but may require pass-through functions and can reduce performance.

Relaxed layering: Layers can access any layer below them, not just the adjacent layer. This approach enables more efficient access to lower-level services but creates more complex dependencies and tighter coupling.

Most embedded systems use relaxed layering with discipline. The application layer may access operating system services directly rather than through middleware wrappers, but it does not typically access the HAL directly. The appropriate degree of layering strictness depends on portability requirements, performance constraints, and system complexity.

Component-Based Design

Component-based design organizes software into discrete, replaceable components with well-defined interfaces. This approach complements layered architecture by providing vertical organization within or across layers, enabling reuse and facilitating maintenance.

Component Concepts

A software component is a modular unit of functionality that encapsulates implementation behind a defined interface. Components have clear boundaries, explicit dependencies, and can be developed, tested, and maintained independently of other components.

Components differ from simple modules in their emphasis on replaceability and reuse. A component's interface is its contract with the rest of the system; as long as the interface is maintained, the component's implementation can change without affecting other system parts.

Component interfaces define both provided interfaces (services the component offers to other components) and required interfaces (services the component needs from other components). This explicit dependency specification enables dependency analysis and supports component integration.

Components may be deployed as static libraries linked at build time, dynamic libraries loaded at runtime, or source code integrated through build systems. The choice depends on system requirements for code size, flexibility, and build processes.

Interface Design

Component interfaces require careful design to enable loose coupling while providing needed functionality:

Minimal interfaces: Interfaces should expose only what clients need, hiding internal details. Minimal interfaces reduce coupling and provide implementation flexibility.

Stable interfaces: Interfaces should change infrequently, as interface changes affect all clients. Interface stability is particularly important for components shared across products or teams.

Consistent conventions: Interfaces across a system should follow consistent naming conventions, error handling patterns, and usage idioms. Consistency reduces learning curve and error opportunities.

Clear documentation: Interfaces must be documented completely, including preconditions, postconditions, error conditions, and thread-safety guarantees. Documentation enables correct component usage without examining implementation.

In C-based embedded systems, component interfaces are typically defined through header files that declare public functions, data types, and constants. Implementation details remain in source files, hidden from component clients.

Dependency Management

Managing dependencies between components is central to component-based design:

Dependency direction: Dependencies should flow toward stable, abstract components. Higher-level components depend on lower-level components, and specific components depend on generic components.

Acyclic dependencies: Circular dependencies between components create coupling that prevents independent development and testing. Dependency graphs should be acyclic, forming a directed graph from higher-level to lower-level components.

Dependency injection: Rather than components creating or finding their dependencies, dependencies can be provided to components during initialization. This pattern enables testing with mock dependencies and supports configuration flexibility.

Dependency inversion: Instead of high-level components depending on low-level components directly, both can depend on abstractions. This pattern enables replacing low-level components without affecting high-level components.

Component Cohesion

Cohesion measures how strongly related the elements within a component are. High cohesion improves understandability and maintainability:

Functional cohesion: All elements contribute to a single, well-defined function. A UART driver component contains only UART-related functionality.

Sequential cohesion: Elements are related because output from one is input to another. A data processing pipeline where stages are related by data flow.

Logical cohesion: Elements are grouped because they do similar things, though on different data. A component containing all configuration parsers for different data formats.

Components should exhibit high cohesion, with all elements working toward the component's primary purpose. Low cohesion components (containing unrelated functionality) should be split into multiple focused components.

Component Coupling

Coupling measures the degree of interdependence between components. Lower coupling improves flexibility and maintainability:

Content coupling: One component directly accesses another's internal data or code. This highest coupling level should be avoided.

Common coupling: Components share global data. This creates hidden dependencies and complicates testing.

Control coupling: One component passes control flags that affect another's behavior. This creates implicit dependencies on control flow.

Data coupling: Components communicate by passing data through parameters. This is the preferred coupling type, enabling clear interface definition.

Message coupling: Components communicate through messages or events. This loose coupling supports flexible system configuration.

Good component design minimizes coupling, preferring data and message coupling over tighter forms. Interface design should enable components to work together with minimal knowledge of each other's internals.

Component Testing

Component-based design supports effective testing strategies:

Unit testing: Individual components can be tested in isolation by providing mock implementations of required interfaces. This enables thorough testing without requiring complete system integration.

Integration testing: Components are progressively integrated, testing interactions between components. Well-defined interfaces make integration predictable and problems easier to isolate.

Component substitution: Test versions of components can be substituted for production versions, enabling testing of specific scenarios or fault conditions.

Regression testing: When components are modified, testing can focus on the changed component and its interfaces, reducing test scope while maintaining confidence.

State Machine Architectures

State machines provide powerful architectural patterns for embedded systems, particularly for systems with complex behavior dependent on current state and input events. State machine architectures make system behavior explicit, analyzable, and maintainable.

State Machine Fundamentals

A state machine consists of a finite set of states, transitions between states triggered by events, and actions associated with states or transitions. At any time, the machine is in exactly one state, and events cause transitions to new states according to defined rules.

States represent distinct modes of operation or stages in a process. Each state captures a stable condition where the system awaits events. States often have associated behaviors including entry actions performed when entering the state, exit actions performed when leaving, and ongoing activities performed while in the state.

Transitions connect states and specify conditions under which the system moves from one state to another. A transition is triggered by an event and may be conditional on guard expressions. Transitions may have associated actions that execute during the state change.

Events are stimuli that the state machine responds to, including hardware interrupts, timer expirations, messages from other components, or user inputs. The state machine's response to an event depends on its current state.

Flat State Machines

Flat state machines represent system behavior as a single set of states without hierarchical organization. This simplest form of state machine works well for systems with modest complexity and clear state distinctions.

Implementation of flat state machines typically uses a state variable storing the current state, a transition table or switch statement mapping state and event combinations to actions and next states, and an event processing function that looks up the appropriate response to each event.

Table-driven implementations store transitions in data structures, enabling concise specification and potential runtime configuration. The table maps each combination of current state and event to a next state and action. This approach scales well and separates transition logic from action implementation.

Switch-based implementations use nested switch statements or if-else chains to determine behavior based on state and event. This approach is straightforward for simple machines but becomes unwieldy as complexity grows.

Flat state machines have limitations with complex systems. Large numbers of states create explosion in transition specifications. Common behaviors across multiple states must be duplicated. These limitations motivate hierarchical state machine approaches.

Hierarchical State Machines

Hierarchical state machines, also called statecharts, extend flat state machines with nested states that enable behavior sharing and complexity management. A superstate can contain substates, with transitions applying to all substates unless overridden.

State hierarchy reduces specification complexity through inheritance. If a superstate defines a transition for an event, all substates inherit that transition unless they explicitly define their own response. This eliminates duplicate transitions across related states.

Entry and exit actions in hierarchical machines execute through the hierarchy. Entering a nested state executes entry actions from the outermost superstate through to the target state. Exiting executes exit actions from the current state through to the common ancestor with the target state.

History states enable returning to a superstate in the same configuration as when it was last exited. Shallow history returns to the last active substate at one level; deep history restores the complete nested state configuration.

Orthogonal regions enable concurrent state behavior within a single state machine. A state can contain multiple independent regions that evolve independently in response to events. This models concurrent aspects of system behavior without requiring multiple separate state machines.

State Pattern Implementation

The state pattern from object-oriented design implements state machines using polymorphism. Each state is represented by a class or structure with function pointers, and state transitions change the active state object:

A state interface defines functions for each event the machine handles. Concrete state implementations provide state-specific behavior for each event. The state machine context maintains the current state and delegates events to it.

This pattern localizes state-specific behavior in state implementations rather than distributing it across switch statements. Adding new states requires creating new state implementations without modifying existing code. The pattern also makes state-specific data natural to manage.

In C, the state pattern is implemented using structures containing function pointers. Each state has an instance of the structure with function pointers set to appropriate handlers. State transitions update the pointer to the current state structure.

Event Processing

How events reach state machines significantly affects system architecture:

Direct call: Events are processed immediately when detected, with callers invoking state machine functions directly. This approach is simple but can create deep call stacks and makes event ordering dependent on detection order.

Event queues: Events are placed in queues and processed by the state machine asynchronously. This decouples event detection from processing, enables event prioritization, and bounds call stack depth. Event queues are common in RTOS-based systems.

Active objects: Each state machine has its own thread and event queue, processing events independently of other system components. This pattern provides strong encapsulation and is well-suited to concurrent systems.

Event processing must address reentrancy and concurrency. If event processing can be interrupted by new events, the state machine must handle concurrent access correctly. Disabling interrupts, using locks, or deferring events until processing completes are common strategies.

State Machine Analysis

State machine representations enable formal analysis that improves system quality:

Reachability analysis: Determines whether all states can be reached from the initial state and whether the machine can reach undesired states. Unreachable states indicate specification errors.

Deadlock detection: Identifies states from which no transitions are possible, which may indicate missing transitions or terminal states.

Completeness checking: Verifies that every combination of state and event has a defined response, preventing undefined behavior.

Model checking: Verifies that the state machine satisfies specified properties such as safety properties (bad things never happen) and liveness properties (good things eventually happen).

Tools can automatically analyze state machine specifications, finding problems before implementation. This analysis is particularly valuable for safety-critical systems where state machine behavior must be demonstrated correct.

Event-Driven Architecture

Event-driven architecture organizes systems around the production, detection, and consumption of events. This paradigm is natural for embedded systems that must respond to external stimuli, hardware interrupts, and asynchronous conditions.

Event-Driven Concepts

In event-driven systems, components communicate primarily through events rather than direct function calls. An event represents something that has happened, such as a button press, timer expiration, data arrival, or state change.

Event producers generate events when significant occurrences are detected. Producers do not know or care which components will handle the events. This decoupling enables adding new event consumers without modifying producers.

Event consumers register interest in specific events and receive notification when those events occur. Consumers process events according to their requirements, potentially producing new events in response.

The event infrastructure manages the flow of events from producers to consumers. This may include event queues, dispatchers, and mechanisms for event filtering and routing.

Publish-Subscribe Pattern

The publish-subscribe pattern implements event distribution where publishers produce events without knowledge of subscribers, and subscribers receive events without knowledge of publishers:

Publishers announce events to the event system, identifying events by type, topic, or channel. The event system distributes events to all registered subscribers for that event type.

Subscribers register interest in specific event types and provide callback functions or other mechanisms for receiving events. When matching events occur, the event system invokes the registered callbacks.

This pattern supports loose coupling and extensibility. New publishers and subscribers can be added without modifying existing components. The event system mediates all communication, enabling features like event logging, filtering, and transformation.

Implementation considerations include subscription management (how subscribers register and unregister), event delivery order (whether order is guaranteed), and synchronization (whether callbacks execute synchronously or asynchronously).

Event Queues and Dispatching

Event queues buffer events between production and consumption, providing temporal decoupling and enabling asynchronous processing:

Queue implementations range from simple circular buffers for single producer/consumer scenarios to more sophisticated structures supporting multiple producers, multiple consumers, and priority handling.

Event dispatchers remove events from queues and route them to appropriate handlers. Dispatch strategies include sequential processing (handling events in queue order), priority-based processing (handling high-priority events first), and parallel processing (using multiple threads to handle events concurrently).

Queue sizing requires balancing memory usage against the risk of overflow. Queues must be large enough to handle event bursts but small enough to fit in available memory. Overflow handling strategies include dropping oldest events, dropping newest events, or blocking producers.

In RTOS environments, event queues are often implemented using operating system message queues, which provide built-in synchronization and blocking semantics.

Callback Mechanisms

Callbacks provide the mechanism by which event consumers receive notifications:

Function pointers: Consumers register function pointers that the event system calls when events occur. This is the most common callback mechanism in C-based embedded systems.

Event objects: Rather than calling functions, the event system creates event objects placed in consumer-specific queues. Consumers poll their queues or are signaled when events arrive.

Handler tables: Event types are mapped to handler functions through tables, enabling efficient lookup of the appropriate handler for each event type.

Callback execution context affects system design. Callbacks may execute in interrupt context (requiring fast execution and limited operations), in producer context (blocking the producer until handling completes), or in dedicated consumer context (enabling independent scheduling).

Active Object Pattern

The active object pattern encapsulates an object, its thread, and its event queue into a cohesive unit. Each active object has complete control over its internal state because only its thread accesses that state:

Active objects receive events through their event queues rather than through direct method calls. This serializes all access to the object's state, eliminating concurrency issues within the object.

The active object's thread runs an event loop that dequeues events and calls appropriate handlers. Handlers update internal state and may post events to other active objects but return promptly to process the next event.

This pattern combines event-driven and concurrent programming in a safe, manageable way. Active objects can execute concurrently (different objects process events simultaneously) while avoiding the complexity of protecting shared state.

Active object frameworks provide infrastructure for creating and managing active objects, including event queue management, thread creation, and event dispatching. Popular frameworks include QP (Quantum Platform) and similar implementations.

Bare-Metal Architecture

Bare-metal systems operate without an operating system, requiring architectures that manage all system resources directly. While simpler in some respects, bare-metal systems require careful architectural decisions to achieve reliability and maintainability.

Super Loop Architecture

The super loop (or main loop) architecture is the simplest bare-metal pattern. An infinite main loop repeatedly checks for work and processes it:

The main loop polls each subsystem in sequence, allowing each to perform incremental work. Subsystems must be designed for cooperative multitasking, doing limited work each iteration and returning promptly to allow other subsystems to execute.

Timing in super loop systems depends on loop execution time. If the loop takes too long, time-sensitive operations may miss deadlines. If it executes faster than needed, the processor wastes power on unnecessary polling.

Advantages of super loop architecture include simplicity, deterministic timing analysis, no context switch overhead, and minimal memory requirements. It works well for simple systems with modest timing requirements.

Limitations become apparent in complex systems. Adding functionality increases loop time, potentially breaking timing for existing subsystems. Long operations block the entire system. Priority handling requires explicit design rather than automatic scheduling.

Time-Triggered Architecture

Time-triggered architecture schedules activities based on the passage of time rather than events. A timer tick drives execution, with activities assigned to specific time slots:

A periodic timer generates ticks at fixed intervals. The tick handler maintains a schedule table and invokes activities according to their scheduled times. Activities execute to completion within their time slots.

This architecture provides deterministic, analyzable timing. Because activities run at predetermined times, worst-case timing analysis is straightforward. Time-triggered systems are well-suited to safety-critical applications requiring timing certification.

Activities must be designed to complete within their time slots. Long operations must be broken into increments that fit within available time. Overruns must be detected and handled appropriately.

The schedule table specifies which activities run at which ticks. Static schedules are defined at design time and cannot change. Dynamic scheduling allows runtime modification but adds complexity.

Interrupt-Driven Architecture

Interrupt-driven systems use hardware interrupts as the primary execution mechanism. Interrupts preempt main loop execution to handle time-critical events:

Interrupt service routines (ISRs) handle hardware events with minimum latency. ISRs should be short, doing only what is necessary to acknowledge the interrupt and capture time-critical data. Extended processing is deferred to lower-priority contexts.

The interrupt/main split divides work between interrupt context and main context. ISRs capture events and set flags or enqueue data. The main loop checks flags and processes deferred work.

Priority management uses hardware interrupt priorities and careful design to ensure critical operations preempt less critical ones. Priority inversion can occur if high-priority code is blocked by low-priority code holding shared resources.

Shared data between interrupt and main contexts requires protection. Disabling interrupts, using atomic operations, or using communication structures designed for concurrent access prevents data corruption.

Cooperative Scheduling

Cooperative scheduling in bare-metal systems uses explicit task structures without preemption. Tasks voluntarily yield control, and a simple scheduler selects the next task to run:

Tasks are functions that maintain state between invocations, performing incremental work and yielding when waiting for resources or when other tasks should run. Task state may be maintained in static variables or explicit context structures.

The scheduler maintains a list of tasks and cycles through them, invoking each in turn. Priority-based schedulers invoke higher-priority tasks first; round-robin schedulers give equal time to all tasks.

Cooperative scheduling avoids the complexity of preemption while providing task-like abstraction. Context switch is simply returning from one task function and calling another, requiring minimal overhead and no special hardware support.

The cooperative nature requires discipline. Tasks must yield regularly and must not block on long operations. A single misbehaving task can prevent all other tasks from executing.

RTOS-Based Architecture

Real-Time Operating Systems provide infrastructure for concurrent, time-bounded embedded systems. RTOS-based architectures leverage operating system services while following patterns appropriate to real-time constraints.

Task Architecture

RTOS-based systems partition functionality into tasks (or threads) that execute concurrently under operating system control:

Task decomposition determines how system functionality is divided among tasks. Criteria for task separation include different priority requirements (time-critical vs. background processing), different timing characteristics (periodic vs. event-driven), logical separation of concerns, and encapsulation of blocking operations.

Task priorities determine which task runs when multiple tasks are ready. Priority assignment considers criticality and timing requirements. Rate monotonic analysis and similar techniques help determine appropriate priorities for systems with periodic tasks.

Task sizing balances granularity against overhead. Too many small tasks increase context switch overhead and memory consumption. Too few large tasks reduce scheduling flexibility and complicate design. Typical embedded systems have relatively few tasks compared to general-purpose systems.

Synchronization Patterns

Tasks that share resources or communicate require synchronization to prevent race conditions and ensure correct operation:

Mutual exclusion: Mutexes protect shared resources, ensuring only one task accesses the resource at a time. Proper mutex usage requires consistent acquisition order to prevent deadlock and consideration of priority inversion.

Signaling: Semaphores and events enable tasks to signal each other when conditions change. A producer signals when data is available; a consumer waits for the signal before processing.

Message passing: Message queues transfer data between tasks without shared memory. The queue handles synchronization internally, and tasks communicate through well-defined message interfaces.

Reader-writer locks: When many readers and few writers access shared data, reader-writer locks allow concurrent reads while ensuring exclusive write access.

Priority Inversion Mitigation

Priority inversion occurs when a high-priority task is blocked by a low-priority task holding a needed resource. This can cause deadline misses even when sufficient processing capacity exists:

Priority inheritance: When a low-priority task holds a mutex needed by a high-priority task, the low-priority task temporarily inherits the high priority. This reduces blocking time by preventing medium-priority tasks from preempting the lock holder.

Priority ceiling: Each mutex has a ceiling priority equal to the highest priority of any task that might acquire it. Tasks holding the mutex run at the ceiling priority, preventing any task that might need the mutex from running.

Design-level mitigation: Minimizing shared resources and critical sections reduces priority inversion opportunities. Using non-blocking algorithms and message passing instead of shared memory eliminates some sources of priority inversion.

Temporal Partitioning

In systems with mixed criticality levels, temporal partitioning ensures that less critical functions cannot interfere with more critical ones:

Time partitions allocate specific time windows to different subsystems. During its partition, a subsystem has guaranteed access to the processor. Other partitions cannot preempt this time, ensuring temporal isolation.

Budget enforcement prevents any partition from exceeding its allocated time. If a partition exhausts its budget, it is suspended until its next scheduled window.

Partition scheduling determines the order and timing of partitions. Static schedules repeat cyclically; dynamic scheduling can adapt to changing conditions while maintaining guarantees.

ARINC 653 defines temporal partitioning for avionics systems, and similar concepts appear in automotive (AUTOSAR) and other safety-critical domains.

Safety-Critical Architecture

Safety-critical embedded systems require architectures that support fault tolerance, enable certification, and provide evidence of correctness. Architectural patterns for safety-critical systems emphasize isolation, redundancy, and analyzability.

Partitioning and Isolation

Safety-critical architectures isolate components to prevent faults from propagating:

Spatial partitioning: Memory protection prevents components from accessing memory allocated to other components. Hardware MMU or MPU enforcement ensures that software faults cannot corrupt other components' data.

Temporal partitioning: Time budget enforcement prevents components from consuming more than their allocated processing time. Other components are guaranteed their time allocations regardless of failures.

Communication isolation: Components communicate only through defined interfaces with validation. No hidden channels exist for fault propagation.

Partitioned systems enable mixed-criticality deployments where components of different Safety Integrity Levels (SIL) or Design Assurance Levels (DAL) coexist on shared hardware while maintaining appropriate isolation.

Redundancy Patterns

Redundancy enables continued operation despite component failures:

N-modular redundancy: Multiple identical components process the same inputs, and a voter selects the correct output based on majority agreement or other criteria. Triple modular redundancy (TMR) is common, detecting and masking single failures.

Diverse redundancy: Redundant components use different designs, algorithms, or implementations to prevent common-mode failures. If one design has a defect, the different design should not have the same defect.

Standby redundancy: Backup components wait in standby mode and take over when primary components fail. Hot standby components run continuously; cold standby components are activated on failure.

Redundancy management includes failure detection, voting or selection logic, and reconfiguration when failures occur. The management infrastructure itself must be highly reliable.

Defensive Programming Patterns

Defensive programming assumes that errors will occur and designs to detect and handle them:

Input validation: All inputs are validated before use, including data from sensors, communication channels, and internal interfaces. Invalid inputs are rejected or handled safely.

Assertions and invariants: Code explicitly checks assumptions and reports violations. Compile-time and runtime assertions catch errors early and prevent propagation.

Defensive data structures: Critical data is protected with redundancy, checksums, or error-correcting codes. Corruption is detected before affected data causes failures.

Watchdog supervision: Watchdog timers detect software hangs or excessive execution time. Failure to service the watchdog triggers recovery actions.

Recovery Patterns

Safety-critical systems must recover from failures to maintain operation:

Graceful degradation: When failures occur, the system continues operating with reduced functionality rather than failing completely. Less critical functions are shed to preserve critical ones.

Checkpointing: System state is periodically saved to enable recovery without full restart. After failure, execution resumes from the last checkpoint.

Reset and restart: Software reset returns the system to a known good state. Restart sequences reinitialize hardware and software, clearing transient faults.

Mode transitions: The system transitions to safe modes with limited functionality when failures are detected. Safe modes are designed to maintain safety even with degraded capability.

Communication Architecture

Communication architecture defines how software components, tasks, and external systems exchange information. Clear communication patterns support loose coupling, enable testing, and simplify integration.

Interprocess Communication

Tasks and components within a system communicate through various mechanisms:

Shared memory: Components access common memory regions, requiring synchronization to prevent race conditions. Shared memory is efficient for large data transfers but creates tight coupling.

Message queues: Components send and receive messages through queues that handle synchronization internally. Message passing is flexible and supports loose coupling but adds overhead.

Remote procedure calls: One component invokes functions in another, with infrastructure handling the communication details. RPC provides familiar programming models but can hide communication costs.

Publish-subscribe: Components publish events that other components subscribe to, without direct connection between publishers and subscribers.

Data-Centric Architecture

Data-centric architectures focus on shared data rather than point-to-point communication:

A shared data space holds system data accessible to all components. Components read and write data to the shared space rather than communicating directly with each other.

The data space handles persistence, consistency, and access control. Components can work with data without knowing which other components produce or consume it.

Data Distribution Service (DDS) implements data-centric architecture for distributed systems, providing publish-subscribe semantics with quality-of-service guarantees.

This architecture pattern supports loose coupling and enables adding new components without modifying existing ones. It works well for systems where multiple components need access to common data.

Protocol Stack Architecture

Communication protocol stacks follow layered architecture principles, with each layer providing services to the layer above:

Physical layer handling manages the physical communication medium including timing, signaling, and error detection at the bit level.

Data link layer handling manages frame boundaries, addressing, and basic error recovery for direct link communication.

Network layer handling manages routing and addressing for communication across multiple links or networks.

Transport layer handling manages end-to-end reliability, flow control, and segmentation.

Application protocols implement domain-specific communication semantics using transport services.

Protocol implementations balance conformance to standards against resource constraints. Embedded implementations often omit features not needed for specific applications while maintaining interoperability for implemented features.

Memory Architecture

Memory architecture decisions affect system reliability, performance, and maintainability. Embedded systems have unique memory constraints that shape architectural choices.

Static Versus Dynamic Allocation

The choice between static and dynamic memory allocation has significant architectural implications:

Static allocation: Memory is allocated at compile time, with sizes and locations fixed in the binary. Static allocation guarantees that memory is always available and eliminates fragmentation. Safety-critical systems often require static allocation to ensure deterministic behavior.

Dynamic allocation: Memory is allocated from a heap at runtime, enabling flexible memory usage. Dynamic allocation requires careful management to prevent leaks, fragmentation, and allocation failures. Many safety standards prohibit or restrict dynamic allocation.

Pool allocation: A compromise approach preallocates fixed-size blocks that are allocated and freed at runtime. Pool allocation combines the flexibility of dynamic allocation with the predictability of static allocation.

Memory Protection

Memory protection prevents components from corrupting each other's memory, supporting reliability and security:

Memory Protection Units (MPU) in microcontrollers enable defining memory regions with access permissions. Violations trigger exceptions that can trigger recovery actions.

Memory Management Units (MMU) in more capable processors provide virtual memory, enabling complete memory isolation between processes and flexible memory mapping.

Software-only protection uses runtime checks and careful coding to prevent memory errors. While not as robust as hardware protection, it can improve reliability in systems without protection hardware.

Memory Layout

Careful memory layout supports reliability and enables efficient access:

Code and data separation places code in read-only memory (flash) and data in RAM. This prevents code corruption and enables execute-in-place operation.

Stack placement affects fault containment. Placing stacks to grow toward protected memory regions enables detection of stack overflow before system corruption.

Critical data placement positions important data in regions with error detection or correction, when such regions are available.

Linker scripts specify memory layout, defining sections for code, data, stacks, and heaps. Well-designed linker scripts support memory protection and efficient access.

Architectural Documentation

Documenting software architecture captures design decisions and enables communication among stakeholders. Effective documentation supports development, maintenance, and certification activities.

Architectural Description

Architectural descriptions document the system's structure and behavior using appropriate notations and views:

Block diagrams show major components and their relationships, providing an overview of system structure.

State diagrams document behavioral aspects, showing system modes and transitions.

Sequence diagrams show interactions between components over time, clarifying dynamic behavior.

Data flow diagrams show how information moves through the system, important for understanding processing stages.

Deployment diagrams map software to hardware, showing how components are allocated to processors and memory.

Design Rationale

Documenting design rationale captures why decisions were made, not just what was decided:

Design decisions record the choices made and the alternatives considered. Understanding what was rejected helps maintainers understand the constraints on changes.

Trade-off analysis documents how competing requirements were balanced. This analysis justifies decisions and guides similar future decisions.

Assumption documentation records assumptions that underlie architectural decisions. When assumptions change, dependent decisions can be revisited.

Interface Specifications

Interface specifications define the contracts between components:

Function signatures specify parameters, return values, and calling conventions.

Behavioral specifications describe what functions do, including preconditions, postconditions, and invariants.

Error handling specifications define how errors are reported and what callers should do in response.

Thread safety specifications indicate which functions can be called concurrently and what synchronization is required.

Summary

Embedded software architecture provides the structural foundation for reliable, maintainable, and efficient embedded systems. Architectural decisions shape system properties that are difficult to change later, making thoughtful architecture essential for project success.

Layered architecture manages complexity through separation of concerns, with each layer providing services to layers above while hiding implementation details. Hardware abstraction, device drivers, operating system services, middleware, and application layers each address specific responsibilities.

Component-based design complements layering with vertical organization, enabling reuse and independent development. Clear interfaces, managed dependencies, high cohesion, and low coupling characterize well-designed component architectures.

State machine architectures make complex behavior explicit and analyzable. Flat state machines handle simple cases; hierarchical state machines manage complexity through state nesting and behavior inheritance. Event-driven architectures extend state machine concepts to system-wide organization.

Bare-metal architectures require explicit management of all system resources, using patterns including super loops, time-triggered scheduling, and interrupt-driven designs. RTOS-based architectures leverage operating system services for concurrent execution, with careful attention to synchronization and timing.

Safety-critical architectures emphasize isolation, redundancy, and defensive programming to achieve reliability requirements. Partitioning prevents fault propagation, redundancy enables continued operation despite failures, and recovery patterns restore normal operation after transient faults.

Communication and memory architectures address how components exchange information and how memory is organized and protected. These fundamental aspects affect system reliability, performance, and security.

Documenting architecture captures decisions for future reference and enables communication among stakeholders. Multiple views, design rationale, and interface specifications support development and maintenance throughout the system lifecycle.

Effective embedded software architecture requires balancing multiple concerns including functionality, performance, reliability, maintainability, and resource constraints. Understanding architectural patterns and principles enables engineers to make informed decisions that support long-term project success.