Post

Design Patterns in Embedded Systems 3

Design Patterns in Embedded Systems 3

Structural Patterns in Embedded Systems

Structural design patterns help organize code and interfaces, making embedded firmware modular, flexible, and easier to extend. These patterns are especially important for managing complex hardware interactions and building scalable architectures in C.


Callback Method Pattern

Purpose:
The Callback Method Pattern allows you to pass a function pointer to another module or driver, so it can “call back” into your code when an event occurs. This is foundational for event-driven programming in embedded systems, such as handling interrupts, timers, or communication events.

Embedded Use Case:
Register a callback to be executed when a button is pressed, a timer expires, or data is received via UART.

Example:

Suppose you want your STM32 firmware to notify your application code whenever a button is pressed, but the application should decide what happens (such as toggling an LED, sending a message, etc.).

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
#include "stm32f4xx_hal.h"
#include <stdint.h>

#define MAX_BUTTON_CALLBACKS 5

typedef void (*ButtonCallback)(void);

static ButtonCallback button_callbacks[MAX_BUTTON_CALLBACKS];
static uint8_t callback_count = 0;

// API to register a callback
void register_button_callback(ButtonCallback cb) {
    if (callback_count < MAX_BUTTON_CALLBACKS) {
        button_callbacks[callback_count++] = cb;
    }
}

// ISR or EXTI callback (called by HAL when button is pressed)
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
    if (GPIO_Pin == GPIO_PIN_13) { // Suppose the button is on PC13
        for (uint8_t i = 0; i < callback_count; ++i) {
            button_callbacks[i](); // Call each registered callback
        }
    }
}

// Example application callback functions
void on_button_toggle_led(void) {
    HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); // Toggle user LED
}

void on_button_send_uart(void) {
    uint8_t msg[] = "Button pressed!\r\n";
    extern UART_HandleTypeDef huart2;
    HAL_UART_Transmit(&huart2, msg, sizeof(msg) - 1, 100);
}

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

    register_button_callback(on_button_toggle_led);
    register_button_callback(on_button_send_uart);

    while (1) {
        // Main loop does not need to poll the button!
    }
}
*/

Key Points:

  • Application code registers callback functions using register_button_callback.
  • When the hardware interrupt for the button is triggered, all registered callbacks are executed.
  • This pattern decouples the hardware logic from the application logic, making code flexible and reusable.

Inheritance Method Pattern

Purpose:
Simulates object-oriented inheritance in C, allowing you to create base modules that can be extended for specific functionality.

Embedded Use Case:
Create a base driver for sensors, then extend it for specific types (temperature, pressure, etc.) with additional features.

Example:

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
// Base sensor "class"
typedef struct {
    void (*init)(void*);
    int  (*read)(void*);
} SensorBase;

// Derived temperature sensor "class"
typedef struct {
    SensorBase base;
    // Additional fields
    ADC_HandleTypeDef* adc_handle;
} TempSensor;

// Implementation for temp sensor
void temp_sensor_init(void* self) {
    TempSensor* sensor = (TempSensor*)self;
    HAL_ADC_Start(sensor->adc_handle);
}

int temp_sensor_read(void* self) {
    TempSensor* sensor = (TempSensor*)self;
    // Read and convert ADC value
    return HAL_ADC_GetValue(sensor->adc_handle);
}

// Usage
/*
ADC_HandleTypeDef hadc1;
TempSensor temp = {
    .base = { .init = temp_sensor_init, .read = temp_sensor_read },
    .adc_handle = &hadc1
};

temp.base.init(&temp);
int value = temp.base.read(&temp);
*/

Key Points:

  • Function pointers in the base struct allow derived modules to override behavior.
  • Additional fields and logic can be added per type.

Virtual API Method Pattern

Purpose:
Provides an abstraction layer where different implementations can be swapped at runtime using function pointers, similar to virtual methods in OOP.

Embedded Use Case:
Select between different communication protocols (e.g., UART, SPI) using the same API.

Example:

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
typedef struct {
    void (*send)(void*, const char*, int);
    void (*receive)(void*, char*, int);
    void* context;
} CommAPI;

// UART implementation
void uart_send(void* context, const char* data, int len) {
    UART_HandleTypeDef* huart = (UART_HandleTypeDef*)context;
    HAL_UART_Transmit(huart, (uint8_t*)data, len, 100);
}

void uart_receive(void* context, char* buf, int len) {
    UART_HandleTypeDef* huart = (UART_HandleTypeDef*)context;
    HAL_UART_Receive(huart, (uint8_t*)buf, len, 100);
}

// SPI implementation
void spi_send(void* context, const char* data, int len) {
    SPI_HandleTypeDef* hspi = (SPI_HandleTypeDef*)context;
    HAL_SPI_Transmit(hspi, (uint8_t*)data, len, 100);
}

void spi_receive(void* context, char* buf, int len) {
    SPI_HandleTypeDef* hspi = (SPI_HandleTypeDef*)context;
    HAL_SPI_Receive(hspi, (uint8_t*)buf, len, 100);
}

// Usage
/*
UART_HandleTypeDef huart2;
SPI_HandleTypeDef hspi1;

CommAPI uart_api = { uart_send, uart_receive, &huart2 };
CommAPI spi_api  = { spi_send, spi_receive, &hspi1 };

uart_api.send(uart_api.context, "Hello UART", 10);
spi_api.send(spi_api.context, "Hello SPI", 9);
*/

Key Points:

  • The API is generic and implementation is selected at runtime.
  • This makes code portable and easily extendable for new protocols.

Conclusion

Structural patterns help you build scalable, modular, and maintainable embedded C projects. Whether you’re handling hardware events, simulating inheritance, or building flexible APIs, these patterns make your firmware robust and adaptable.


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

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