Open In App

Design Patterns for Embedded Systems in C

Last Updated : 05 Feb, 2024
Improve
Improve
Like Article
Like
Save
Share
Report

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.

Design-Patterns-for-Embedded-Systems-in-C

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.

Creational-Design-Pattern-new-copy

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:

C




#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,

  • We have a Product interface that defines the common operations for all products, and a ConcreteProduct that implements these operations.
  • Creator interface declares the createProduct factory method.
  • ConcreteCreator provides a concrete implementation of this factory method for creating ConcreteProduct instances.

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.

C




#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:

  • MyObject is a structure that encapsulates data and functionality.
  • The printValue function acts as a method for the object, printing its value.
  • The initializeObject function initializes an instance of MyObject and sets its initial value.

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):

C




// 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):

C




// 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:

  • By the help of this Process, external code can include temperature_sensor.h and use the functions declared in the header file without knowing the details of the TemperatureSensor structure.
  • The actual implementation details are hidden in the temperature_sensor.c file, providing a clean separation between the interface and the implementation.

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.

C




#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:

  • Basically, In this example, we have a Singleton struct representing the Singleton class. The instance of this class (instance) is declared as a static variable to ensure there is only one instance.
  • The getSingletonInstance function provides a way to access the singleton instance.
  • The initializeSingleton function is used to initialize any necessary data or perform other setup tasks.

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.

Structural-Design-Pattern-new-copy

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.

C




#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:

  • We define a callback function type (CallbackFunction) using a function pointer.
  • The registerCallback function takes a callback function as an argument and stores it for later use.
  • We simulate the occurrence of an event (e.g., button press) by calling the registerCallback function.
  • The registered callback is then called, simulating the execution of actions associated with the event.

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.

C




#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,

  • The Sensor structure serves as a generic interface for different types of sensors.
  • The structure contains function pointers for initialization, reading, and cleanup operations.
  • Concrete sensor types (e.g., temperature sensor and pressure sensor) implement these operations, and their instances can be used interchangeably through the generic Sensor interface.

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.

C




// 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,

  • MotorAdapter allows the old motor control library to be used with the interface of the new library without modifying the existing code of the old library.
  • It contains the different type of functionality like powerON, powerOf, create New Motor Controller.
  • The Virtual API Design Pattern helps in maintaining compatibility and reusability in embedded C systems.

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:

C




#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:

  • Implementor is an interface that declares the operations that concrete implementors must implement.
  • ConcreteImplementorA and ConcreteImplementorB are concrete implementations of the Implementor interface.
  • Abstraction is an interface that declares the operations that concrete abstractions must implement.
  • RefinedAbstraction is a refined abstraction that extends the abstraction interface.
  • Main function demonstrates how you can use the Bridge Pattern to decouple abstractions from implementations and switch implementations dynamically.

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.

C




#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:

  • In this example demonstrates a simple scheduler that iterates over an array of tasks, checks if each task should be executed based on its interval, and then executes it if needed.
  • The tasks run in a cooperative manner, meaning each task must yield control to the scheduler voluntarily. In a real-world scenario, this could be implemented using timer interrupts or a more sophisticated scheduler in an RTOS.

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:

C




#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,

  • The Spinlock structure contains a locked flag that indicates whether the lock is currently held or not.
  • The spinlock_acquire function uses the __sync_lock_test_and_set built-in function to atomically set the locked flag to true and return the previous value.
  • If the previous value was already true, indicating that the lock was held, the function enters a spin loop until the lock becomes available. The spinlock_release function simply sets the locked flag back to false to release the lock.

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:

C




#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,

  • A simple mutex using a binary semaphore. The lockMutex function is responsible for acquiring the mutex, and the unlockMutex function releases it.
  • The critical section between these two calls is protected, ensuring that only one thread can access it at a time.
  • The pthread library is used for thread management. Make sure to link your program with the pthread library when compiling (-lpthread flag).

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:

C




#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 :

  • We have a sensor reading represented by the variable sensorValue.
  • We set threshold values (thresholdLow and thresholdHigh) to define the acceptable range for the sensor reading.
  • The if, else if, and else statements are used to check different conditions based on the sensor value.
  • Depending on the condition met, different actions are taken. For an embedded system, these actions might involve adjusting system parameters, activating or deactivating devices, or triggering alarms.

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:

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:

Design-Pattern-for-Embedded-System-in-C-(1)

In this example,

  • We have a Context struct that contains a function pointer stateHandler pointing to the current state’s handler function. We then define two state functions (state1 and state2).
  • The initializeContext function sets the initial state to state1, and the changeState function updates the state to the specified function.
  • The performAction function is called to execute the behavior associated with the current state.
  • In the main function, we create a Context, initialize it with state1, perform an action, change the state to state2, and perform another action.

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.



Like Article
Suggest improvement
Share your thoughts in the comments

Similar Reads