Open In App

Design Patterns for Embedded Systems in C

When working with embedded systems in C, there are so many design patterns that are particularly very useful. Many design patterns can be applied to embedded systems development in C. In this article, we will discuss design patterns for Embedded Systems in C, let’s see all of them one by one with the help of examples.



1. What is a Design Pattern?

Design patterns are defined as the general reusable solutions to the common problems that occur during software development and software designing.



They provide a template for solving certain types of problems and help the developers structure the code in a way that makes it more modular, maintainable, and scalable.

2. Creational Design Pattern for Embedded Systems in C

In embedded systems development in C, creational design patterns are used to abstract the instantiation process of objects, providing flexibility in creating and configuring objects.

2.1 Factory Method Design Pattern

One commonly used creational design pattern is the Factory Method Pattern. Let’s see an example of how we can implement the Factory Method Pattern in C for an embedded system:




#include <stdio.h>
 
// Product interface
typedef struct {
    void (*operation)(void);
} Product;
 
// Concrete product
typedef struct {
    Product base;
} ConcreteProduct;
 
void concreteProductOperation(void) {
    printf("Concrete product operation\n");
}
 
// Creator interface
typedef struct {
    Product* (*createProduct)(void);
} Creator;
 
// Concrete creator
typedef struct {
    Creator base;
} ConcreteCreator;
 
Product* createConcreteProduct(void) {
    ConcreteProduct* product = (ConcreteProduct*)malloc(sizeof(ConcreteProduct));
    if (product == NULL) {
        // Handle memory allocation error
        return NULL;
    }
 
    // Initialize the product and set its operation function
    product->base.operation = concreteProductOperation;
 
    return &product->base;
}
 
int main() {
    // Client code uses the creator interface to create a product
    Creator* creator = (Creator*)malloc(sizeof(ConcreteCreator));
    if (creator == NULL) {
        // Handle memory allocation error
        return 1;
    }
 
    // Set the creator's createProduct function to the specific factory method
    creator->createProduct = createConcreteProduct;
 
    // Use the factory method to create a product
    Product* product = creator->createProduct();
 
    // Use the product
    if (product != NULL) {
        product->operation();
    }
 
    // Cleanup
    free(creator);
    free(product);
 
    return 0;
}

Output
Concrete product operation













Explaination of above Code:

In the above code,

2.2 Object Method Design Pattern

In the embedded systems programming in C, the Object Pattern is typically implemented using structures and functions that operate on those structures. While C does not have native support for object-oriented programming.

We can achieve similar concepts through a combination of structures and function pointers. Let’s see a simple example for illustrating the Object Pattern in C for an embedded system.




#include <stdio.h>
 
// Define a structure to represent an object
typedef struct {
    // Data members
    int value;
 
    // Function pointer for a method
    void (*print)(const struct MyObject *);
} MyObject;
 
// Method to print the value of the object
void printValue(const MyObject *obj) {
    printf("Object value: %d\n", obj->value);
}
 
// Function to initialize an object
void initializeObject(MyObject *obj, int initialValue) {
    obj->value = initialValue;
    obj->print = &printValue; // Assign the function pointer
}
 
int main() {
    // Create an instance of MyObject
    MyObject myObj;
 
    // Initialize the object
    initializeObject(&myObj, 42);
 
    // Use the object's method
    myObj.print(&myObj);
 
    return 0;
}

Output
Object value: 42













Explanation of the above Code:

2.3 Opaque Method Design Pattern

In the embedded systems programming, an opaque pattern is often used to hide the implementation details of a data structure or module from the outside world. This helps in encapsulating the internal details, providing a clean interface, and enhancing code maintainability.

The basic idea is to declare a structure in the source file that contains the implementation details, and then provide a header file with only the necessary information for using the module.

Problem Statement of Opaque Method Design Pattern

Let’s see the simple example of how we might use an opaque pattern in embedded C:Suppose we want to create a module for handling a temperature sensor, and we wants to keep the details of the sensor structure hidden from the external code.

temperature_sensor.h (Public header file):




// temperature_sensor.h
 
#ifndef TEMPERATURE_SENSOR_H
#define TEMPERATURE_SENSOR_H
 
// Forward declaration of the opaque structure
typedef struct TemperatureSensor TemperatureSensor;
 
// Function prototypes
TemperatureSensor* temperature_sensor_create(void);
void temperature_sensor_destroy(TemperatureSensor* sensor);
float temperature_sensor_read(TemperatureSensor* sensor);
 
#endif // TEMPERATURE_SENSOR_H

temperature_sensor.c (Private implementation file):




// temperature_sensor.c
 
#include "temperature_sensor.h"
 
// Define the actual structure of the TemperatureSensor
struct TemperatureSensor {
    // Include whatever internal data members are necessary
    float last_reading;
    // ... other members
};
 
TemperatureSensor* temperature_sensor_create(void) {
    TemperatureSensor* sensor = malloc(sizeof(TemperatureSensor));
    if (sensor != NULL) {
        // Initialize internal data members as needed
        sensor->last_reading = 0.0;
        // ... other initialization
    }
    return sensor;
}
 
void temperature_sensor_destroy(TemperatureSensor* sensor) {
    free(sensor);
}
 
float temperature_sensor_read(TemperatureSensor* sensor) {
    // Simulate reading from the sensor
    // In a real implementation, this would involve interacting with the hardware
    // Here, we'll just return a dummy value for demonstration purposes
    sensor->last_reading += 0.5;  // Simulating an incremental increase
    return sensor->last_reading;
}

Explanation of above Code:

2.4 Singleton Method Design Pattern

The Singleton Pattern is basically a type of design pattern that restricts the instantiation of a class to a single instance and provides a global point of access to that instance. This pattern is often used to control access to resources such as databases, file systems, or network connections.

In embedded systems, where resource utilization is crucial, the Singleton Pattern can be especially very useful.




#include <stdio.h>
 
// Singleton class definition
typedef struct {
    // Add your member variables here
    int data;
} Singleton;
 
// Static instance of the Singleton class
static Singleton instance;
 
// Function to get the instance of the Singleton class
Singleton* getSingletonInstance() {
    return &instance;
}
 
// Function to initialize the Singleton instance
void initializeSingleton() {
    // Perform any initialization here
    instance.data = 0;
}
 
// Example usage
int main() {
    // Initialize the Singleton instance
    initializeSingleton();
 
    // Get the Singleton instance
    Singleton* singleton = getSingletonInstance();
 
    // Access and modify data
    printf("Initial data: %d\n", singleton->data);
 
    // Modify data
    singleton->data = 42;
 
    // Access modified data
    printf("Modified data: %d\n", singleton->data);
 
    return 0;
}

Explanation of the above Code:

3. Structural Design Patterns for Embedded Systems in C

Structural design patterns in embedded systems help to organize and structure the code in a way that makes it more modular, flexible, and easier to maintain.

3.1 Callback Method Design Patterns

The callback pattern in embedded systems is a design pattern where a function (callback function) is registered to be called later when a specific event or condition occurs.

This pattern is commonly used in event-driven programming, where the flow of the program is determined by events rather than a linear sequence of statements.

Problem Statement of Callback Method Design Pattern:

Let’s see an example of implementing a simple callback pattern in C for an embedded system. In this example, we’ll create a system that performs some actions when a button is pressed.




#include <stdio.h>
 
// Callback function type definition
typedef void (*CallbackFunction)(void);
 
// Function to register a callback
void registerCallback(CallbackFunction callback) {
    // Store the callback function for later use
    // This could be an array of callbacks for multiple events
    CallbackFunction registeredCallback = callback;
 
    // Simulate registering the callback (e.g., in an interrupt setup)
    printf("Callback registered successfully\n");
 
    // Simulate an event triggering the callback (e.g., button press)
    printf("Event occurred, calling the callback\n");
    registeredCallback();
}
 
// Callback function 1
void callbackFunction1(void) {
    printf("Callback Function 1 executed\n");
    // Perform actions related to the event
}
 
// Callback function 2
void callbackFunction2(void) {
    printf("Callback Function 2 executed\n");
    // Perform other actions related to the event
}
 
int main() {
    // Register callbackFunction1 for the button press event
    registerCallback(callbackFunction1);
 
    // Register callbackFunction2 for another event
    registerCallback(callbackFunction2);
 
    return 0;
}

Output
Callback registered successfully
Event occurred, calling the callback
Callback Function 1 executed
Callback registered successfully
Event occurred, calling the callback
Callback Function 2 executed













Explanation of the above Code:

3.2 Inheritance Method Design Pattern

In embedded systems programming with C, inheritance is not directly supported as it is in some object-oriented languages like C++.

However, we can achieve similar functionality through other means, such as using structures and function pointers to simulate polymorphism.

Problem Statement of the Inheritance Method

Let’s see a simple example to illustrate this concept. Suppose we are working on an embedded system that involves different types of sensors (e.g., temperature sensor, pressure sensor, and light sensor). We want to create a generic interface for these sensors to simplify the handling of various sensor types in our system.




#include <stdio.h>
 
// Define a structure to represent the generic sensor
typedef struct {
    void (*initialize)(void);
    int (*read)(void);
    void (*cleanup)(void);
} Sensor;
 
// Define a function to perform some operations using a sensor
void performSensorOperations(Sensor* sensor) {
    sensor->initialize();
    int value = sensor->read();
    printf("Sensor value: %d\n", value);
    sensor->cleanup();
}
 
// Define a concrete implementation for a temperature sensor
void temperatureSensorInitialize(void) {
    printf("Initializing temperature sensor...\n");
}
 
int temperatureSensorRead(void) {
    printf("Reading temperature sensor...\n");
    // Simulate reading from the sensor
    return 25;
}
 
void temperatureSensorCleanup(void) {
    printf("Cleaning up temperature sensor...\n");
}
 
// Define a concrete implementation for a pressure sensor
void pressureSensorInitialize(void) {
    printf("Initializing pressure sensor...\n");
}
 
int pressureSensorRead(void) {
    printf("Reading pressure sensor...\n");
    // Simulate reading from the sensor
    return 1000;
}
 
void pressureSensorCleanup(void) {
    printf("Cleaning up pressure sensor...\n");
}
 
int main() {
    // Create instances of temperature and pressure sensors
    Sensor temperatureSensor = {
        .initialize = temperatureSensorInitialize,
        .read = temperatureSensorRead,
        .cleanup = temperatureSensorCleanup
    };
 
    Sensor pressureSensor = {
        .initialize = pressureSensorInitialize,
        .read = pressureSensorRead,
        .cleanup = pressureSensorCleanup
    };
 
    // Use the generic interface to perform operations on sensors
    performSensorOperations(&temperatureSensor);
    performSensorOperations(&pressureSensor);
 
    return 0;
}

Output
Initializing temperature sensor...
Reading temperature sensor...
Sensor value: 25
Cleaning up temperature sensor...
Initializing pressure sensor...
Reading pressure sensor...
Sensor value: 1000
Cleani...












Explanation of the above Code:

In this example,

3.3 Virtual API Method Design Pattern

The Virtual API design pattern, sometimes called the Virtual Function Table (VFT) pattern, is a valuable technique in embedded C programming for providing a common interface to different implementations of the same functionality.

Problem Statement of Virtual API Method Design Pattern:

Let’s consider a scenario where we have an existing library that provides functionality to control a motor, but we want to use a new motor control library that has a different interface. we can create an adapter to make the old library compatible with the new interface.




// Existing Motor Control Library
typedef struct {
    void (*startMotor)();
    void (*stopMotor)();
} OldMotorController;
 
void oldStartMotor() {
    // Code to start the motor in the old library
}
 
void oldStopMotor() {
    // Code to stop the motor in the old library
}
 
OldMotorController createOldMotorController() {
    OldMotorController controller;
    controller.startMotor = oldStartMotor;
    controller.stopMotor = oldStopMotor;
    return controller;
}
 
// New Motor Control Library
typedef struct {
    void (*powerOn)();
    void (*powerOff)();
} NewMotorController;
 
void newPowerOn() {
    // Code to power on the motor in the new library
}
 
void newPowerOff() {
    // Code to power off the motor in the new library
}
 
NewMotorController createNewMotorController() {
    NewMotorController controller;
    controller.powerOn = newPowerOn;
    controller.powerOff = newPowerOff;
    return controller;
}
 
// Adapter to make the old library compatible with the new interface
typedef struct {
    OldMotorController oldController;
} MotorAdapter;
 
void adapterPowerOn(MotorAdapter *adapter) {
    adapter->oldController.startMotor();
}
 
void adapterPowerOff(MotorAdapter *adapter) {
    adapter->oldController.stopMotor();
}
 
// Main function demonstrating the usage of the adapter
int main() {
    // Using the new motor control library directly
    NewMotorController newController = createNewMotorController();
    newController.powerOn();
    newController.powerOff();
 
    // Using the old motor control library through the adapter
    MotorAdapter adapter;
    adapter.oldController = createOldMotorController();
    adapterPowerOn(&adapter);
    adapterPowerOff(&adapter);
 
    return 0;
}

Explanation of the above Code:

In this example,

4. Other Design Patterns for Embedded System in C

4.1 Bridge Method Design Pattern

The Bridge Pattern is a structural design pattern that separates abstraction from implementation, allowing them to vary independently. This pattern is particularly useful in embedded systems where we may have different platforms, devices, or hardware that need to be supported.

Let’s see a simple example of the Bridge Pattern in C for embedded systems:




#include <stdio.h>
 
// Implementor interface
// This represents the implementation interface that concrete implementors will implement
typedef struct {
    void (*sendData)(const char *data);
} Implementor;
 
// Concrete Implementor A
typedef struct {
    Implementor implementor;
} ConcreteImplementorA;
 
void sendA(const char *data) {
    printf("Sending data using Implementor A: %s\n", data);
}
 
void initializeConcreteImplementorA(ConcreteImplementorA *this) {
    this->implementor.sendData = sendA;
}
 
// Concrete Implementor B
typedef struct {
    Implementor implementor;
} ConcreteImplementorB;
 
void sendB(const char *data) {
    printf("Sending data using Implementor B: %s\n", data);
}
 
void initializeConcreteImplementorB(ConcreteImplementorB *this) {
    this->implementor.sendData = sendB;
}
 
// Abstraction interface
// This represents the abstraction interface that concrete abstractions will implement
typedef struct {
    Implementor implementor;
    void (*send)(const char *data);
} Abstraction;
 
// Refined Abstraction
typedef struct {
    Abstraction abstraction;
} RefinedAbstraction;
 
void sendData(RefinedAbstraction *this, const char *data) {
    printf("Refined Abstraction is sending data: %s\n", data);
    this->abstraction.implementor.sendData(data);
}
 
void initializeRefinedAbstraction(RefinedAbstraction *this, Implementor *implementor) {
    this->abstraction.implementor = *implementor;
    this->abstraction.send = sendData;
}
 
int main() {
    ConcreteImplementorA concreteImplementorA;
    initializeConcreteImplementorA(&concreteImplementorA);
 
    ConcreteImplementorB concreteImplementorB;
    initializeConcreteImplementorB(&concreteImplementorB);
 
    RefinedAbstraction refinedAbstractionA;
    initializeRefinedAbstraction(&refinedAbstractionA, &concreteImplementorA.implementor);
    refinedAbstractionA.abstraction.send("Hello from Abstraction A");
 
    RefinedAbstraction refinedAbstractionB;
    initializeRefinedAbstraction(&refinedAbstractionB, &concreteImplementorB.implementor);
    refinedAbstractionB.abstraction.send("Hello from Abstraction B");
 
    return 0;
}

Explanation of the above Code:

4.2 Concurrency Method Design Pattern

Concurrency in embedded systems is often crucial to efficiently handle multiple tasks or events concurrently. One common approach is to use a Real-Time Operating System (RTOS) or a simple scheduler for task management.

Concurrency Method Design Pattern is used for Operating System, it is helpfull for single and multiple task, task may be synchronous and asynchronus. Let suppose if there is any user send the request and there is multiple request, so let suppose when there is thread1 goes for task then thread2 have to wait. So waiting period of the other task it takes some time to execute other. In this process we can use the Concurrency Method.

Problem Statement of Concurrency Method Design Pattern

In the above example of a simple concurrency pattern in embedded C using a cooperative multitasking approach without an RTOS. In this example, tasks are executed in a round-robin fashion.




#include <stdio.h>
#include <stdint.h>
 
// Define task function signatures
typedef void (*TaskFunction)(void);
 
// Define the task structure
typedef struct {
    TaskFunction function;
    uint32_t interval;  // Time between task executions
    uint32_t lastExecutionTime;
} Task;
 
// Example tasks
void task1(void) {
    printf("Task 1 executed\n");
}
 
void task2(void) {
    printf("Task 2 executed\n");
}
 
void task3(void) {
    printf("Task 3 executed\n");
}
 
// Array of tasks
Task tasks[] = {
    {task1, 1000, 0},  // Task 1 runs every 1000 milliseconds
    {task2, 500, 0},   // Task 2 runs every 500 milliseconds
    {task3, 2000, 0},  // Task 3 runs every 2000 milliseconds
};
 
// Number of tasks
#define NUM_TASKS (sizeof(tasks) / sizeof(Task))
 
// Simple scheduler
void scheduler(void) {
    // Get the current time (in a real system, this would be a timer value)
    uint32_t currentTime = 0;  // Replace this with actual timer value
 
    for (int i = 0; i < NUM_TASKS; ++i) {
        Task *currentTask = &tasks[i];
 
        // Check if it's time to execute the task
        if ((currentTime - currentTask->lastExecutionTime) >= currentTask->interval) {
            // Execute the task
            currentTask->function();
 
            // Update the last execution time
            currentTask->lastExecutionTime = currentTime;
        }
    }
}
 
int main() {
    // In a real system, this loop would run indefinitely
    for (int i = 0; i < 10; ++i) {
        // Simulate the passage of time
        // In a real system, this would be handled by interrupts or a timer
        // Here, we just increment the time variable for simplicity
        scheduler();
    }
 
    return 0;
}

Explanation of the above Code:

4.3 Spinlock Method Design Pattern

A spinlock is a synchronization mechanism commonly used in embedded systems and other low-level programming scenarios to protect shared resources from concurrent access.

Unlike traditional locks that may put a thread to sleep when it cannot acquire the lock, a spinlock repeatedly checks the lock’s availability in a loop until it becomes available.

Let’s see a simple example of a spinlock in C for an embedded system:




#include <stdint.h>
#include <stdbool.h>
 
typedef struct {
    volatile bool locked;
} Spinlock;
 
void spinlock_init(Spinlock* lock) {
    lock->locked = false;
}
 
void spinlock_acquire(Spinlock* lock) {
    // Spin until the lock is acquired
    while (__sync_lock_test_and_set(&lock->locked, true)) {
        // Optionally insert a delay here to reduce CPU usage
        // (Note: This might not be the best practice, as it depends on the specific platform and requirements)
    }
}
 
void spinlock_release(Spinlock* lock) {
    // Release the lock
    __sync_lock_release(&lock->locked);
}
 
// Example usage
int shared_resource = 0;
Spinlock resource_lock;
 
int main() {
    // Initialize the spinlock
    spinlock_init(&resource_lock);
 
    // Acquire the lock before accessing the shared resource
    spinlock_acquire(&resource_lock);
 
    // Access the shared resource
    shared_resource += 10;
 
    // Release the lock after accessing the shared resource
    spinlock_release(&resource_lock);
 
    return 0;
}

Explanation of the above Code:

In this example,

4.4 Mutex Method Design Pattern

In embedded systems programming, a mutex (short for mutual exclusion) is often used to ensure that only one task or thread can access a shared resource at a time. This is crucial to prevent data corruption and other synchronization issues.

Let’s see an example:




#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include <pthread.h>
 
// Define a simple structure to represent the mutex
typedef struct {
    bool locked;
    pthread_mutex_t mutex;
} Mutex;
 
// Function to initialize the mutex
void initMutex(Mutex *mutex) {
    mutex->locked = false;
    pthread_mutex_init(&mutex->mutex, NULL);
}
 
// Function to acquire the mutex
void lockMutex(Mutex *mutex) {
    pthread_mutex_lock(&mutex->mutex);
    while (mutex->locked) {
        // If the mutex is already locked, wait until it becomes available
        pthread_mutex_unlock(&mutex->mutex);
        pthread_mutex_lock(&mutex->mutex);
    }
    // Mark the mutex as locked
    mutex->locked = true;
    pthread_mutex_unlock(&mutex->mutex);
}
 
// Function to release the mutex
void unlockMutex(Mutex *mutex) {
    pthread_mutex_lock(&mutex->mutex);
    // Mark the mutex as unlocked
    mutex->locked = false;
    pthread_mutex_unlock(&mutex->mutex);
}
 
// Example usage of the mutex
Mutex myMutex;
 
void* threadFunction(void* arg) {
    int threadID = *((int*)arg);
 
    lockMutex(&myMutex);
    printf("Thread %d has acquired the mutex.\n", threadID);
    // Critical section - Access the shared resource
    printf("Thread %d is in the critical section.\n", threadID);
    unlockMutex(&myMutex);
 
    pthread_exit(NULL);
}
 
int main() {
    initMutex(&myMutex);
 
    pthread_t thread1, thread2;
 
    int id1 = 1, id2 = 2;
 
    pthread_create(&thread1, NULL, threadFunction, (void*)&id1);
    pthread_create(&thread2, NULL, threadFunction, (void*)&id2);
 
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);
 
    pthread_mutex_destroy(&myMutex.mutex);
 
    return 0;
}

Explanation of the above Code:

In this example,

4.5 Conditional Method Design Pattern

In embedded systems programming, conditional statements are frequently used to make decisions based on certain conditions. The most common conditional statements in the C programming language are if, else if, and else.

Problem Statement:

Let’s see an simple example of conditional patterns in C for embedded systems:




#include <stdio.h>
 
int main() {
    // Assume a sensor reading in an embedded system
    int sensorValue = 75;
 
    // Threshold values for decision-making
    int thresholdLow = 50;
    int thresholdHigh = 70;
 
    // Conditional statements to make decisions based on the sensor reading
    if (sensorValue < thresholdLow) {
        // Perform action when the sensor value is below the low threshold
        printf("Sensor value is too low. Take corrective action.\n");
    } else if (sensorValue >= thresholdLow && sensorValue <= thresholdHigh) {
        // Perform action when the sensor value is within acceptable range
        printf("Sensor value is within acceptable range. No action needed.\n");
    } else {
        // Perform action when the sensor value is above the high threshold
        printf("Sensor value is too high. Take corrective action.\n");
    }
 
    return 0;
}

Output
Sensor value is too high. Take corrective action.













Explanation :

4.6 Behavioral Method Design Pattern

In the embedded systems, behavioral patterns can be applied to improve the design and organization of software components. One commonly used behavioral pattern is the State Pattern. The State Pattern allows an object to alter its behavior when its internal state changes. This pattern is particularly useful in embedded systems where the system’s behavior is expected to change based on its state.

Let’s simple example of the State pattern in embedded C:




#include <stdio.h>
 
// Define the context that will change its behavior
typedef struct {
    void (*stateHandler)(); // Function pointer to the current state handler
} Context;
 
// Define the states
void state1() {
    printf("State 1\n");
}
 
void state2() {
    printf("State 2\n");
}
 
// Function to initialize the context with an initial state
void initializeContext(Context* context) {
    context->stateHandler = state1;
}
 
// Function to change the state of the context
void changeState(Context* context, void (*newState)()) {
    context->stateHandler = newState;
}
 
// Function to perform some action using the current state
void performAction(Context* context) {
    context->stateHandler();
}
 
int main() {
    // Create a context and initialize it with state1
    Context myContext;
    initializeContext(&myContext);
 
    // Perform an action using the initial state
    performAction(&myContext);
 
    // Change the state to state2
    changeState(&myContext, state2);
 
    // Perform an action using the updated state
    performAction(&myContext);
 
    return 0;
}

Output
State 1
State 2













UML Diagram of Behavioral Method Design Pattern:

In this example,

Conclusion

In summery while working with embedded systems, it’s essential to consider the resource constraints and real-time requirements. The chosen design patterns should align with the specific needs of the embedded system in terms of performance, memory usage, and responsiveness.


Article Tags :