Skip to main content

Command Palette

Search for a command to run...

Design Pattern Pt 2: Singleton

Singleton: to ensure order in chaos

Published
7 min readView as Markdown

Introduction

In embedded systems, efficient resource management is crucial due to their limited memory, processing power, and real-time constraints. Managing shared resources such as hardware peripherals often leads to conflicts, redundant instances, or inconsistent states. The Singleton design pattern is a well-known solution to these issues. This article explores the Singleton pattern, its applications in embedded systems, and the problems it solves

Why Care About Singleton in Embedded Systems?

Imagine your embedded system is juggling multiple modules trying to access the same hardware peripheral simultaneously. Without a proper strategy, you face resource conflicts, wasted memory, and unpredictable behavior. Singleton design pattern, a simple yet powerful tool to ensure order in this chaos

What is the Singleton Design Pattern?

The Singleton design pattern ensures that:

  1. Only One Instance Exists: A class has only one instance during the program's lifetime.

  2. Global Access: Provides a centralized and consistent way to access this instance.

  3. Lazy Initialization: The instance is created only when it is first needed.

Singleton is commonly used in systems where shared resources must be accessed consistently and safely.


Issues Before Singleton

In my own experience, i’ve faced several challenges in managing shared resources:

  1. Resource Contention

    • Multiple modules might attempt to use a hardware peripheral like I2C or UART simultaneously, causing conflicts.

    • Example: Two separate UART drivers writing to the same register at the same time.

  2. Redundant Instances

    • Creating multiple instances of the same resource wastes valuable memory and processing power in embedded systems.

    • Example: Allocating separate memory buffers for each redundant instance.

  3. Inconsistent State

    • Different parts of the system might initialize and configure a resource differently, leading to unpredictable behavior.

    • Example: Module A configures a timer with one clock speed, while Module B configures it with another.

  4. Complex Initialization

    • Properly initializing shared resources across multiple modules becomes error-prone without centralized control.
  5. Global Access Challenges

    • Without Singleton, accessing a shared resource globally often requires passing references or relying on global variables, increasing code complexity.

How Singleton Solves These Issues

  1. Single Instance Guarantee

    • Ensures that only one instance of a resource exists, eliminating conflicts.
  2. Centralized Initialization

    • Singleton ensures resources are initialized correctly in one place, reducing errors.
  3. Efficient Resource Usage

    • Reuses a single instance, conserving memory and processing power.
  4. Controlled Access

    • Provides a consistent interface for accessing the resource, reducing misuse.
  5. Global Accessibility

    • Eliminates the need for passing references or creating global variables.

Implementation of Singleton in C

Example: Managing an I2C Bus

A Singleton ensures that only one instance of the I2C bus controller is created and reused.

#include <stdio.h>
#include <stdlib.h>

// Singleton I2C Controller
typedef struct {
    int initialized;
    // Add I2C-specific configuration and state here
} I2CController;

I2CController* i2c_instance = NULL;

I2CController* get_i2c_instance() {
    if (i2c_instance == NULL) {
        i2c_instance = (I2CController*)malloc(sizeof(I2CController));
        if (i2c_instance) {
            i2c_instance->initialized = 1;
            printf("I2C Controller Initialized\n");
        }
    }
    return i2c_instance;
}

void i2c_write(I2CController* i2c, const char* data) {
    if (i2c && i2c->initialized) {
        printf("I2C Write: %s\n", data);
    } else {
        printf("I2C Controller not initialized\n");
    }
}

int main() {
    // First access initializes the I2C Controller
    I2CController* i2c1 = get_i2c_instance();
    i2c_write(i2c1, "Hello, I2C!");

    // Subsequent accesses return the same instance
    I2CController* i2c2 = get_i2c_instance();
    i2c_write(i2c2, "Reusing I2C Instance!");

    return 0;
}

Applications of Singleton in Embedded Systems

  1. Hardware Peripheral Management

    • Singleton ensures safe and consistent access to peripherals like UART, SPI, or I2C.
  2. Configuration Management

    • Stores system-wide configurations such as clock settings or power modes in a single instance.
  3. Interrupt Management

    • Manages shared interrupt handlers or resources accessed by multiple tasks.
  4. Resource-Limited Systems

    • Optimizes memory and CPU usage by preventing redundant instances.

Potential Drawbacks

  1. Hidden Dependencies

    • Singleton can obscure dependencies between modules, making the system harder to understand and test.
  2. Testing Challenges

    • Mocking or replacing a Singleton in unit tests can be difficult.
  3. Tight Coupling

    • Modules relying on a Singleton are tightly coupled to its implementation, reducing flexibility.

When to Use Singleton

The Singleton pattern is most useful when:

  • A resource must be shared across the system (e.g., a hardware driver).

  • Consistent configuration of a component is critical.

  • Multiple instances would lead to conflicts or waste resources.

However, avoid using Singleton unnecessarily. If your system doesn’t require a shared resource or single-instance guarantee, implementing Singleton may introduce unnecessary complexity.


Singleton Design Pattern for Network Connection Management

In complex systems like IoT, embedded servers, or communication gateways, managing network connections across different modules can become complicated without a centralized control. The Singleton pattern provides a solution by ensuring a single, consistent instance for managing all network-related tasks across the system.

Why Use the Singleton Pattern?

  1. Consistency Across the System:

    • Ensures there is only one instance responsible for managing network connections, preventing conflicts and duplication.

    • Simplifies the overall design with a single point of control for all network-related tasks.

  2. Handling Multiple Protocols:

    • The Singleton helps manage different communication protocols (e.g., TCP/IP for reliable data transfer and UDP for low-latency communication) in a unified manner.
  3. Efficient Resource Management:

    • Simplifies resource allocation, error handling, and power management, especially in battery-powered devices or systems with intermittent connectivity.

Questions to Ask When Considering a Singleton Design

  1. Centralized Network Configurations:

    • Do I need centralized network configurations to ensure consistency across different modules?
  2. Preventing Redundant Initialization:

    • Do I need to prevent redundant network initialization or conflicting configurations, especially in a multi-tasking or multi-threaded environment?
  3. Simplifying Lifecycle Management:

    • Is it necessary to simplify the lifecycle management of network connections, including automatic reconnection and error recovery?
  4. Shared Resource Management:

    • Is a shared resource, like a network interface, UART, or sensor, being accessed by multiple parts of the system, requiring management to avoid conflicts?
  5. Consistency Across Modules:

    • Do multiple modules need to access the same resource, ensuring consistent state or configuration?
  6. Simplifying Access to Complex Resources:

    • Does the resource, like a network stack or Wi-Fi driver, require complex setup or interactions that need to be simplified?

How the Singleton Solves These Problems

  1. Global Access:

    • Any module in the system can access the Singleton instance without needing to create its own.
  2. Consistent State:

    • The connection state, like whether the device is connected to the network, is consistently shared across the system.
  3. Resource Efficiency:

    • Only one instance of the network manager exists, preventing memory wastage and conflicting accesses.

Implementation Example: Network Connection Manager Using Singleton

#include <stdio.h>
#include <stdlib.h>

// Singleton Class for Network Manager
typedef struct {
    int isConnected;
    void (*connect)(const char* ssid, const char* password);
    void (*disconnect)();
} NetworkManager;

// Private static instance
static NetworkManager* instance = NULL;

// Function implementations
void connectToNetwork(const char* ssid, const char* password) {
    printf("Connecting to %s with password %s...\n", ssid, password);
    instance->isConnected = 1;
}

void disconnectFromNetwork() {
    printf("Disconnecting from network...\n");
    instance->isConnected = 0;
}

// Singleton access method
NetworkManager* getNetworkManager() {
    if (instance == NULL) {
        instance = (NetworkManager*)malloc(sizeof(NetworkManager));
        instance->isConnected = 0;
        instance->connect = connectToNetwork;
        instance->disconnect = disconnectFromNetwork;
    }
    return instance;
}

// Main usage
int main() {
    NetworkManager* netMgr = getNetworkManager();
    netMgr->connect("MyWiFi", "password123");
    netMgr->disconnect();
    return 0;
}

Why the Singleton Works in This Case

  1. Global Access:

    • The NetworkManager instance can be accessed by any module or part of the system without creating new instances.
  2. Consistent State:

    • The isConnected state is shared and updated across all modules, ensuring that the network connection state is consistent.
  3. Resource Efficiency:

    • The NetworkManager instance is created only once, optimizing memory usage and preventing conflicts in managing network resources.

Conclusion

The Singleton design pattern addresses real-world challenges in embedded systems by ensuring consistent, efficient, and safe access to shared resources. While it provides significant advantages in resource-constrained environments, careful consideration is needed to avoid overuse and ensure maintainable, testable code. By understanding Singleton and its applications, designs reach robustness and efficiency.