Post

Design Patterns in Embedded Systems 2

Design Patterns in Embedded Systems 2

Creational Patterns in Embedded Systems

Creational design patterns focus on how objects, modules, or resources are created and managed in software. In embedded systems, these patterns are essential for organizing code, managing hardware resources, and improving maintainability, especially in resource-constrained environments.


Factory Method Pattern

Purpose:
The Factory Method Pattern provides a way to create objects without specifying the exact class of object that will be created. This is useful for abstracting hardware drivers or sensor interfaces.

Embedded Use Case:
Suppose you have multiple types of sensors (temperature, pressure, etc.) and want to instantiate the correct driver depending on the sensor type.

Example: Suppose your board has a temperature sensor connected to ADC1 and a pressure sensor connected to I2C1. You want to create driver objects for each sensor type using a factory function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#include "stm32f4xx_hal.h"
#include <stdint.h>

// Sensor interface
typedef struct {
    void (*init)(void);
    int  (*read)(void);
} Sensor;

// Temperature sensor implementation (ADC)
void temp_sensor_init(void) {
    // Initialize ADC1 for temperature sensor
    // Example: HAL_ADC_Start(), HAL_ADC_ConfigChannel(), etc.
}

int temp_sensor_read(void) {
    // Read value from ADC1
    // Example: HAL_ADC_GetValue(), convert to temperature
    return value; 
}

// Pressure sensor implementation (I2C)
void pressure_sensor_init(void) {
    // Initialize I2C1 for pressure sensor
    // Example: HAL_I2C_Init(), etc.
}

int pressure_sensor_read(void) {
    // Read value from pressure sensor via I2C1
    // Example: HAL_I2C_Master_Receive(), convert to pressure
    return value; 
}    

// Sensor type enum
typedef enum {
    SENSOR_TEMPERATURE,
    SENSOR_PRESSURE
} SensorType;

// Factory method
Sensor* create_sensor(SensorType type) {
    static Sensor temp_sensor = {
        .init = temp_sensor_init,
        .read = temp_sensor_read
    };
    static Sensor pressure_sensor = {
        .init = pressure_sensor_init,
        .read = pressure_sensor_read
    };
    switch (type) {
        case SENSOR_TEMPERATURE:
            return &temp_sensor;
        case SENSOR_PRESSURE:
            return &pressure_sensor;
        default:
            return NULL;
    }
}

/* Usage in main.c 

    int main(void) {
        HAL_Init();
        // ...SystemClock_Config(), GPIO init, etc...

        Sensor* temp = create_sensor(SENSOR_TEMPERATURE);
        temp->init();
        int temperature = temp->read();

        Sensor* pressure = create_sensor(SENSOR_PRESSURE);
        pressure->init();
        int pressure_val = pressure->read();

        // Use temperature and pressure_val as needed...
        while (1) {}
    }
*/

Key Points:

  • Each sensor implementation provides its own init and read functions.
  • The factory function returns the appropriate driver object based on the requested sensor type.
  • This approach scales well when adding more sensors or peripherals.

Object Method Pattern

Purpose:
Encapsulates data and functions that operate on that data, simulating object-oriented behavior in C, where true classes are not available.

Embedded Use Case:
You want to create reusable modules, such as a GPIO handler, that keeps its state and provides related operations.

Example:

Suppose you want to create an object-like structure for controlling GPIO pins on an STM32 board. Each “GPIO object” knows its pin and provides methods to set, clear, or toggle its state.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include "stm32f4xx_hal.h"
#include <stdint.h>

// Define the GPIO object structure
typedef struct {
    GPIO_TypeDef* port;
    uint16_t pin;
    void (*set)(struct GPIO_Object* self);
    void (*clear)(struct GPIO_Object* self);
    void (*toggle)(struct GPIO_Object* self);
} GPIO_Object;

// Implementation functions
void gpio_set(GPIO_Object* self) {
    HAL_GPIO_WritePin(self->port, self->pin, GPIO_PIN_SET);
}

void gpio_clear(GPIO_Object* self) {
    HAL_GPIO_WritePin(self->port, self->pin, GPIO_PIN_RESET);
}

void gpio_toggle(GPIO_Object* self) {
    HAL_GPIO_TogglePin(self->port, self->pin);
}

// Object initializer ("constructor")
GPIO_Object create_gpio_object(GPIO_TypeDef* port, uint16_t pin) {
    GPIO_Object obj;
    obj.port = port;
    obj.pin = pin;
    obj.set = gpio_set;
    obj.clear = gpio_clear;
    obj.toggle = gpio_toggle;
    return obj;
}

/* Usage in main.c
int main(void) {
    HAL_Init();
    // ...SystemClock_Config(), GPIO init, etc...

    // Create an object for the user LED (e.g., PA5)
    GPIO_Object led = create_gpio_object(GPIOA, GPIO_PIN_5);

    // Set, clear, and toggle LED using object methods
    led.set(&led);      // Turn LED on
    led.clear(&led);    // Turn LED off
    led.toggle(&led);   // Toggle LED

    while (1) {
        led.toggle(&led);
        HAL_Delay(500);
    }
}
*/

Key Points:

  • The object bundles the pin, port, and actions into one logical unit.
  • Function pointers allow object-like method calls (led.set(&led)).
  • Easily reusable for other GPIOs and peripherals.

Opaque Method Pattern

Purpose:
Hides the internal details of a module from its users, exposing only a handle and a set of function pointers. This is a common approach to creating reusable drivers and APIs in embedded C.

Embedded Use Case:
You want to provide a UART driver where users interact only with a handle, not the internals of the driver structure.

Example:

Suppose you want to make a UART driver that hides its internal structure, exposing only a handle and its API. This is typical in STM32 HAL and LL libraries.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
name=example_opaque_method.c
// uart_driver.h
#ifndef UART_DRIVER_H
#define UART_DRIVER_H

#include "stm32f4xx_hal.h"
#include <stdint.h>

// Forward declaration (opaque type)
typedef struct UART_Driver UART_Driver;

// API functions
UART_Driver* uart_create(USART_TypeDef* instance, uint32_t baudrate);
void uart_send(UART_Driver* driver, const char* data);
void uart_destroy(UART_Driver* driver);

#endif // UART_DRIVER_H

// uart_driver.c
#include "uart_driver.h"
#include <stdlib.h>

// Internal structure definition
struct UART_Driver {
    UART_HandleTypeDef huart;
    // Add other internal state variables, buffers, etc.
};

UART_Driver* uart_create(USART_TypeDef* instance, uint32_t baudrate) {
    UART_Driver* driver = malloc(sizeof(UART_Driver));
    driver->huart.Instance = instance;
    driver->huart.Init.BaudRate = baudrate;
    // ...other init code (parity, stop bits, etc.)...
    HAL_UART_Init(&driver->huart);
    return driver;
}

void uart_send(UART_Driver* driver, const char* data) {
    // Example: send data using HAL_UART_Transmit
    HAL_UART_Transmit(&driver->huart, (uint8_t*)data, strlen(data), 100);
}

void uart_destroy(UART_Driver* driver) {
    // Example: cleanup, deinit if needed
    HAL_UART_DeInit(&driver->huart);
    free(driver);
}

// Usage in main.c
/*
#include "uart_driver.h"

int main(void) {
    HAL_Init();
    // ...SystemClock_Config(), GPIO init, etc...

    UART_Driver* uart2 = uart_create(USART2, 115200);
    uart_send(uart2, "Hello, UART!\r\n");

    uart_destroy(uart2);

    while (1) {}
}
*/

Key Points:

  • Users never access the internal structure directly—only through a pointer (UART_Driver*).
  • Source files (.c) define the full structure, while header files (.h) only forward-declare it.
  • Maintenance and API safety are improved, and internal changes do not affect user code.

Singleton Method Pattern

Purpose:
Ensures that only one instance of a particular resource or peripheral exists in the system, avoiding conflicts and ensuring proper resource management.

Embedded Use Case:
You need to guarantee that only one UART or SPI driver is active at a time.

Example: Suppose you need to guarantee that only one UART2 handler exists, and all parts of your application use the same instance. If the handler is already initialized, subsequent requests will return the same instance.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include "stm32f4xx_hal.h"
#include <stdint.h>

// Singleton structure for UART2
typedef struct {
    UART_HandleTypeDef huart;
    uint8_t initialized;
} UART2_Singleton;

// Getter for the singleton instance
UART2_Singleton* get_uart2_instance(void) {
    static UART2_Singleton instance;
    if (!instance.initialized) {
        instance.huart.Instance = USART2;
        instance.huart.Init.BaudRate = 115200;
        instance.huart.Init.WordLength = UART_WORDLENGTH_8B;
        instance.huart.Init.StopBits = UART_STOPBITS_1;
        instance.huart.Init.Parity = UART_PARITY_NONE;
        instance.huart.Init.Mode = UART_MODE_TX_RX;
        instance.huart.Init.HwFlowCtl = UART_HWCONTROL_NONE;
        instance.huart.Init.OverSampling = UART_OVERSAMPLING_16;
        HAL_UART_Init(&instance.huart);
        instance.initialized = 1;
    }
    return &instance;
}

// Usage in main.c
/*
int main(void) {
    HAL_Init();
    // ...SystemClock_Config(), GPIO init, etc...

    UART2_Singleton* uart2 = get_uart2_instance();
    uint8_t msg[] = "Hello from Singleton UART!\r\n";
    HAL_UART_Transmit(&uart2->huart, msg, sizeof(msg) - 1, 100);

    // Later, anywhere else in the code:
    UART2_Singleton* uart2_again = get_uart2_instance();
    // uart2_again points to the same instance as uart2

    while (1) {}
}
*/

Key Points:

  • The singleton instance is static and initialized only once.
  • All code parts requesting UART2 use the same handler and configuration.
  • Prevents resource conflicts and double-initialization.

Conclusion

Creational patterns bring structure, scalability, and reliability to embedded C projects. Whether you’re building sensor drivers, hardware abstraction layers, or resource managers, these patterns help you develop cleaner, more maintainable firmware. Mastering them will set the foundation for advanced embedded architecture and robust code design.


Stay tuned for our next post on Structural Design Patterns in Embedded Systems!

This post is licensed under CC BY 4.0 by the author.