Introduction

Embedded engineering systems impose strict constraints on memory, power, and real-time behaviour. In such environments, the Singleton pattern—a design principle that restricts a class to a single instance and provides a global point of access—can be a powerful tool for managing shared hardware resources, communication channels, and system-wide state. However, applying this pattern incorrectly can introduce subtle bugs, degrade performance, and increase power consumption. This article expands the standard Singleton guidelines into a set of production-oriented best practices tailored for embedded systems, covering lazy initialization, thread safety, memory efficiency, and common pitfalls. Each section includes concrete examples and references to industry standards to help you build robust, maintainable embedded firmware.

Understanding the Singleton Pattern in Embedded Systems

The Singleton pattern ensures that exactly one object of a class exists at any time. In embedded systems, this is especially valuable for representing peripherals, device drivers, and resource managers that must maintain a consistent global view. Typical candidates include:

  • UART/USART controllers – only one instance should manage the transmit and receive buffers.
  • ADC (Analog-to-Digital Converter) drivers – multiple clients need to read the same conversion results without duplication.
  • Power management modules – a single point of decision for entering sleep or active modes.
  • Real-time clock (RTC) access – a single clock source should be synchronized by all tasks.
  • Task schedulers in bare-metal systems – a single loop or interrupt handler dispatches all cooperative tasks.

Without a Singleton, developers often resort to global variables or static structures, which can lead to inconsistent state across modules. The Singleton pattern enforces a disciplined access method, but its implementation must be adapted to the constraints of embedded hardware: limited stack and heap space, lack of dynamic memory allocation in some contexts, and the presence of interrupts and real-time operations.

A common misconception is that the Singleton pattern is simply a "global variable in a fancy dress." In embedded systems, the pattern must be implemented with careful control over instantiation timing, thread safety (including interrupt contexts), and power-aware behaviour. The following sections dissect the best practices that separate a sound Singleton from a dangerous one.

Best Practices for Implementation

1. Use Lazy Initialization with Power Awareness

Lazy initialization means the Singleton instance is created only when it is first accessed. This approach conserves memory and processor cycles during startup, which is critical in battery-powered or resource-constrained devices. Consider a UART driver that is rarely used in a low-power sensor node: delaying its creation until a serial command arrives can save several hundred bytes of RAM and avoid initializing the peripheral clock unnecessarily.

Example in C (using a static variable):

// uart_driver.h
typedef struct {
    volatile uint32_t *base_addr;
    // ... other fields
} UART_HandleTypeDef;

UART_HandleTypeDef* UART_GetInstance(void);

// uart_driver.c
#include "uart_driver.h"
#include "chip_peripherals.h"

UART_HandleTypeDef* UART_GetInstance(void) {
    static UART_HandleTypeDef instance;
    static int initialized = 0;
    if (!initialized) {
        instance.base_addr = (uint32_t*)UART1_BASE;
        // Perform peripheral-specific configuration
        UART_Configure(instance.base_addr);
        initialized = 1;
    }
    return &instance;
}

Note that the initialization flag is a plain integer. In single-threaded, no-interrupt environments this is safe, but additional measures are needed for concurrent access (see next section).

Lazy initialization also allows the embedded system to defer power-hungry peripheral power-up until absolutely necessary. If the Singleton manages a high-current device (e.g., a GSM modem or Wi-Fi module), creating the instance later reduces average power consumption. However, be cautious: if the lazily created Singleton is accessed inside an interrupt service routine that requires deterministic timing, the first access may incur a large latency. In such cases, eager initialization (creating the instance during system initialisation) might be more appropriate.

Link: For a deeper discussion on lazy vs. eager initialization in resource-constrained systems, see Embedded Artistry’s Singleton pattern overview.

2. Ensure Thread Safety for Multi-Tasking and Interrupts

Embedded systems often mix a main loop, interrupt service routines (ISRs), and sometimes an RTOS (Real-Time Operating System). When a Singleton is accessed from multiple contexts, race conditions can corrupt its state—especially during lazy initialization. The classic example: two tasks call GetInstance() simultaneously, both see initialized == 0, and both try to configure the hardware, causing double initialization or data corruption.

Thread safety in embedded environments differs from desktop systems:

  • Interrupts cannot use blocking mutexes – a mutex that might yield the CPU will cause a deadlock if called from an ISR. Instead, use critical sections (disable interrupts during the critical region) or lock-free atomic operations.
  • RTOS tasks – use a mutex or semaphore to guard the Singleton access. If the RTOS supports priority inheritance, use it to avoid priority inversion.
  • Bare-metal with cooperative scheduling – if the Singleton is only accessed from the main loop, no extra protection is needed, but verify that ISRs never call the Singleton directly.

Example: Thread-safe Singleton with CMSIS-RTOS mutex

#include "cmsis_os2.h"
#include "singleton.h"

static GPIO_TypeDef* instance = NULL;
static osMutexId_t mutex_id;

void Singleton_Init(void) {
    mutex_id = osMutexNew(NULL);
}

GPIO_TypeDef* Singleton_GetInstance(void) {
    osMutexAcquire(mutex_id, osWaitForever);
    if (instance == NULL) {
        instance = (GPIO_TypeDef*)GPIOA_BASE;
        GPIO_ConfigureInstance(instance);
    }
    osMutexRelease(mutex_id);
    return instance;
}

In an ISR, a safer approach is to use atomic operations (e.g., __sync_bool_compare_and_swap in GCC) or a lock-free state machine. For simplicity, many production systems perform eager initialization before enabling interrupts, avoiding runtime concurrency entirely.

Link: The ARM CMSIS documentation provides guidelines for thread-safe peripherals: CMSIS-Core (ARM) thread safety.

3. Keep the Singleton Lightweight – No Heap, No Complex Constructors

Embedded systems often have limited heap memory, and many safety-critical projects ban dynamic allocation altogether (MISRA-C:2012 Rule 21.3). Therefore, Singletons should be statically allocated or placed in dedicated memory regions. Avoid using malloc() or new, as fragmentation and out-of-memory errors can lead to hard-to-find failures.

The Singleton’s initialization should be minimal:

  • Store a base address or handle of the peripheral.
  • Set default configuration parameters (e.g., baud rate, clock divider).
  • Do not start peripherals that consume power until a client explicitly calls an operation (e.g., UART_Transmit()).

In C++, you can implement a Meyer’s Singleton using a static local variable, which is guaranteed to be created exactly once (C++11 and later guarantee thread-safe static initialization). However, be aware that the destructor may never be called if the system uses a halt() loop, and the static initialisation order across translation units can be tricky. A simpler and more deterministic approach for embedded C++ is to use a placement new into a static buffer, but that still requires care.

Example of a lightweight Singleton in C++ (Meyer’s):

class UART {
public:
    static UART& getInstance() {
        static UART instance;  // C++11 thread-safe by default
        return instance;
    }
private:
    UART() {
        // lightweight – only store address, no peripheral init
        base_ = (uint32_t*)UART1_BASE;
    }
    uint32_t* base_;
};

This pattern uses zero heap memory and incurs only the cost of a static pointer and a single check. However, if the constructor performs time-consuming operations, it should be moved to a separate initialization method that the user calls explicitly after power is stable.

4. Avoid Circular Dependencies and Tight Coupling

Because Singletons provide global access, they can encourage a "god object" design where many modules directly fetch the same instance. This makes code difficult to test and maintain. Best practice is to inject the Singleton’s interface (or pointer) into the modules that need it, rather than having them call GetInstance() internally. Use a dependency injection approach where possible: at startup, create the Singleton and pass it to other modules via their initialization functions.

For example, instead of:

void Sensor_Task(void) {
    UART_Transmit(UART_GetInstance(), "Hello");
}

Prefer:

void Sensor_Task(UART_HandleTypeDef* uart) {
    UART_Transmit(uart, "Hello");
}

This decouples the task from the Singleton’s access point, making it easy to inject a mock UART during testing.

Common Pitfalls to Avoid

Global State Overuse

Over-reliance on Singletons often leads to hidden dependencies that complicate unit testing and reusability. In embedded systems, this can be especially harmful when the Singleton manages power states or interrupts that affect other modules. Mitigation: Limit the number of Singletons to one or two per subsystem (e.g., a Clock manager and an Error logger). Where possible, replace Singletons with dependency injection or a service locator pattern that is still testable.

Ignoring Power Constraints

Creating a Singleton that initialises a high-power peripheral during system start-up can waste energy in low-duty-cycle applications. For instance, a GPS receiver Singleton should be created only when a navigation task is active. Solution: Implement a "shallow" Singleton that only holds a handle, and provide explicit power-on/power-off methods that the user calls as needed. Combine lazy creation (peripheral handle) with lazy power-up.

Neglecting Proper Cleanup

In many embedded systems, the Singleton outlives the application—there is no "shutdown" phase. However, if the system supports dynamic loading (e.g., bootloader to application) or runtime reconfiguration, the Singleton may need to release resources. Memory leaks from Singletons are rare in static allocation, but peripheral registers left in an active state can drain power or cause conflicts when reinitialising. Practice: Provide a Deinit() method that resets the hardware and optionally resets the instance flag. Document that the Singleton is not meant to be destroyed, only de-initialised.

Testability Challenges

Hardwired singleton access calls (e.g., MySingleton::getInstance()) make it impossible to replace the instance with a test double. This violates the open/closed principle and discourages writing firmware tests. Approach: Expose a global variable or use a setter for injection during testing (guarded by a #ifdef UNIT_TEST). Alternatively, use the "Singleton but with factory" pattern: have a static function pointer that can be redirected to a mock in tests.

Link: Test-driven development for embedded C is covered in Test-Driven Development for Embedded C by James W. Grenning (provides patterns for decoupling singletons).

Implementation Patterns in C and C++

C – Static Local with Explicit Locking

The common C pattern uses a static variable inside a function, guarded by a mutex or interrupt disable. This is simple but requires careful handling of the guard:

// Recommended for single-core with interrupts disabled during init
MyPeripheral* MyPeripheral_GetInstance(void) {
    static MyPeripheral inst;
    static bool initialized = false;
    if (!initialized) {
        // Disable interrupts
        __disable_irq();
        if (!initialized) {  // double-check after lock
            MyPeripheral_InitHardware(&inst);
            initialized = true;
        }
        __enable_irq();
    }
    return &inst;
}

This double-checked locking pattern works only if the compiler does not reorder stores and if the architecture guarantees atomic reads/writes for the flag (typically a 32-bit aligned word on ARM Cortex-M). For extra safety, use volatile and possibly a memory barrier.

C++ – Static Local with constexpr and RAII

The C++11 Meyer’s Singleton is thread-safe by the standard, but be aware that static local variables may have a hidden locking mechanism that consumes stack. For extremely constrained systems, consider a simple static member variable initialised with a constructor that is called early in main() (eager initialisation). In both cases, rule of thumb: keep the constructor constexpr or trivial.

Conclusion

The Singleton pattern remains a useful tool in embedded systems when applied with discipline. By adhering to lazy initialization with power awareness, implementing proper thread safety for the target concurrency model, and maintaining a lightweight footprint, developers can avoid the classic pitfalls of global state, power waste, and testing difficulty. Always question whether a Singleton is truly necessary—often a simple global structure with explicit access functions can serve the same purpose without the extra ceremony. For those cases where pattern purity is justified, the practices outlined in this article will help you create robust, maintainable embedded firmware that works reliably under the harshest constraints.

Key takeaways:

  • Use lazy initialization to conserve resources, but prefetch eagerly for time-critical paths.
  • Lock the Singleton with the appropriate mechanism for your RTOS or interrupt architecture; never block inside an ISR.
  • Avoid heap allocation – prefer static memory.
  • Design for testability by injecting the Singleton’s interface rather than calling global accessors.
  • Provide explicit de-initialisation routines for power management and reconfiguration.

Further reading: