Design Patterns in Embedded Systems 4
Behavioral and Concurrency Patterns in Embedded Systems
Behavioral and concurrency patterns focus on how tasks, modules, or algorithms interact and coordinate in embedded systems. These patterns are essential for building responsive, robust, and scalable firmware when dealing with real-time events, multitasking, and resource sharing.
Bridge Method Pattern
Purpose:
The Bridge Pattern decouples abstraction from implementation, allowing both to evolve independently. This is useful for separating hardware-specific code from application logic.
Embedded Use Case:
Suppose you want to use the same API for different hardware modules (e.g., different display or sensor chips), but with swappable implementations.
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
typedef struct {
void (*draw_pixel)(void*, int, int, int);
void* context;
} DisplayAPI;
// Hardware-specific implementation for LCD
void lcd_draw_pixel(void* context, int x, int y, int color) {
// Write to LCD controller
}
// Hardware-specific implementation for OLED
void oled_draw_pixel(void* context, int x, int y, int color) {
// Write to OLED controller
}
// Usage
/*
LCD_HandleTypeDef hlcd;
OLED_HandleTypeDef holed;
DisplayAPI lcd_api = { lcd_draw_pixel, &hlcd };
DisplayAPI oled_api = { oled_draw_pixel, &holed };
// Application code can use either API without knowing the hardware
lcd_api.draw_pixel(lcd_api.context, 10, 10, 0xFFFF);
oled_api.draw_pixel(oled_api.context, 20, 20, 0x00FF);
*/
Key Points:
- Abstraction (API) and implementation (hardware-specific functions) are decoupled.
- New hardware modules can be added without changing application code.
- Improves portability and scalability for multi-platform projects.
Concurrency Method Pattern
Purpose:
Manages multiple tasks, events, or threads. In embedded systems, this is often implemented using RTOS primitives or custom schedulers.
Embedded Use Case:
Running multiple tasks (e.g., sensor reading, communication, display update) concurrently on STM32 FreeRTOS.
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
#include "FreeRTOS.h"
#include "task.h"
void sensor_task(void* params) {
while (1) {
// Read sensor
vTaskDelay(100);
}
}
void comm_task(void* params) {
while (1) {
// Handle communication
vTaskDelay(50);
}
}
void display_task(void* params) {
while (1) {
// Update display
vTaskDelay(200);
}
}
// Usage in main.c
/*
int main(void) {
HAL_Init();
// ...SystemClock_Config(), peripheral init, etc...
xTaskCreate(sensor_task, "Sensor", 128, NULL, 1, NULL);
xTaskCreate(comm_task, "Comm", 128, NULL, 1, NULL);
xTaskCreate(display_task, "Display", 128, NULL, 1, NULL);
vTaskStartScheduler();
while (1) {}
}
*/
Key Points:
- Tasks run in parallel, scheduled by FreeRTOS or another RTOS.
- Each task can handle a different module or feature independently.
- Improves responsiveness and makes handling asynchronous events easier.
Spinlock Method Pattern
Purpose:
Implements busy-waiting for resource access. Used in simple or low-latency situations where tasks must wait for a resource to be free.
Embedded Use Case:
Protect a shared resource (like a buffer) between ISR and main loop.
Example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
volatile int lock = 0;
void acquire_lock(void) {
while (__atomic_test_and_set(&lock, __ATOMIC_ACQUIRE)) {
// Busy wait
}
}
void release_lock(void) {
__atomic_clear(&lock, __ATOMIC_RELEASE);
}
// Usage
/*
acquire_lock();
// Critical section
release_lock();
*/
Key Points:
- Ensures exclusive access to resources in real-time contexts.
- Simple to implement, suitable for very short critical sections.
- Can waste CPU cycles; use for quick operations only.
Mutex Method Pattern
Purpose:
Provides mutual exclusion for shared resources, preventing data corruption in concurrent environments.
Embedded Use Case:
Protect access to a shared UART or data buffer in a multitasking (RTOS) environment.
Example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "FreeRTOS.h"
#include "semphr.h"
SemaphoreHandle_t uart_mutex;
// Usage
/*
uart_mutex = xSemaphoreCreateMutex();
if (xSemaphoreTake(uart_mutex, portMAX_DELAY) == pdTRUE) {
// Safe access to UART
xSemaphoreGive(uart_mutex);
}
*/
Key Points:
- Mutexes are more efficient than spinlocks for longer, blocking operations.
- Essential for safe multitasking.
Conditional Method Pattern
Purpose:
Synchronizes tasks using condition variables or flags, enabling coordination between producer and consumer tasks.
Embedded Use Case:
Signal a task to process new data when it arrives.
Example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
volatile int data_ready = 0;
void producer_task(void* params) {
// Produce data
data_ready = 1;
}
void consumer_task(void* params) {
while (!data_ready) {
// Wait for data
vTaskDelay(10);
}
// Process data
data_ready = 0;
}
Key Points:
- Useful for event signaling between tasks.
- Can be implemented with flags or RTOS event groups.
Behavioral Method Pattern
Purpose:
Encapsulates algorithms or behaviors, allowing them to be changed independently from the objects that use them. Often implemented in embedded C with function pointers.
Embedded Use Case:
Switch communication protocols or algorithms at runtime.
Example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typedef void (*ProcessFunc)(void);
void process_uart(void) {
// UART processing code
}
void process_spi(void) {
// SPI processing code
}
ProcessFunc current_process = process_uart;
// Usage
/*
current_process(); // Calls UART processing
current_process = process_spi;
current_process(); // Calls SPI processing
*/
Key Points:
- Enables runtime flexibility and code reuse.
- Easy to swap algorithms or behaviors.
Conclusion
Behavioral and concurrency patterns are essential for building responsive, reliable, and maintainable embedded firmware. Whether you need to manage tasks, synchronize events, or swap algorithms, these patterns provide proven solutions for real-time embedded systems.