Supplementing the Previous Article on the Observer Pattern: Application of Embedded Design Patterns in the Observer Pattern
In resource-constrained embedded system development, the application of design patterns can significantly enhance the maintainability, reliability, and execution efficiency of the code. Although C language does not support object-oriented features, various design patterns can still be implemented through techniques such as structures, function pointers, and state management, providing embedded engineers with efficient development tools. This article will delve into seven commonly used patterns in C language embedded programming, including state machines, resource pools, event-driven, cyclic execution, interrupt service, facade, and producer-consumer patterns, each accompanied by detailed implementation examples and application scenario analyses.
1. State Machine Pattern
The state machine pattern is one of the most widely used design patterns in embedded systems. It organizes complex system behaviors into clear state transition processes by enumerating states and transition rules. The core idea of the state machine pattern is to decompose system behavior into discrete states and the conditions for transitions between states, making the code structure easier to understand and maintain while avoiding complex nested conditional statements.
Implementation Example: STM32 LED Flow Light Control
// State definitions
typedef enum {
led_off,
led_blink,
led_flow,
led_breath
} led_state_t;
// State machine structure
typedef struct {
led_state_t current_state;
void (*on_stateEnter)(void); // Function to handle entering the state
void (*stateHandler)(void); // State handling function
void (*on_stateExit)(void); // Function to handle exiting the state
} led_StateMachine;
// State handling function collection
void led_OffEnter(void) {
GPIO_SetBits(GPIOA, GPIO_Pin_5);
}
void led_OffHandler(void) {
// Idle state, no handling required
}
void led_OffExit(void) {
// Preparation for exiting idle state
}
void led_BlinkEnter(void) {
// Initialize blink timer
}
void led_BlinkHandler(void) {
static uint32_t lastToggle = 0;
if (HAL_GetTick() - lastToggle > 500) {
GPIO_ToggleBits(GPIOA, GPIO_Pin_5);
lastToggle = HAL_GetTick();
}
}
void led_BlinkExit(void) {
// Clear blink timer
}
// State machine initialization
void led_FsmInit(led_StateMachine *fsm) {
fsm->current_state = led_off;
fsm->on_stateEnter = led_OffEnter;
fsm->stateHandler = led_OffHandler;
fsm->on_stateExit = led_OffExit;
}
// State transition function
void led_FsmTransition(led_StateMachine *fsm, led_state_t new_state) {
// Exit current state
fsm->on_stateExit();
// Enter new state
switch (new_state) {
case led_off:
fsm->on_stateEnter = led_OffEnter;
fsm->stateHandler = led_OffHandler;
fsm->on_stateExit = led_OffExit;
break;
case led_blink:
fsm->on_stateEnter = led_BlinkEnter;
fsm->stateHandler = led_BlinkHandler;
fsm->on_stateExit = led_BlinkExit;
break;
// Other state handling...
}
fsm->current_state = new_state;
fsm->on_stateEnter(); // Execute the enter function of the new state
}
// Main loop handling the state machine
void main() {
led_StateMachine ledFsm;
led_FsmInit(&ledFsm);
while (1) {
// State transition based on conditions
if (button_presses) {
led_FsmTransition(&ledFsm, led_blink);
} else if (SW2_presses) {
led_FsmTransition(&ledFsm, led_flow);
}
// Execute current state handling
ledFsm.stateHandler();
// Other tasks...
}
}
Application Scenarios:
- Communication protocol state management: such as connection, disconnection, and transmission state transitions for UART, SPI, I2C, etc.
- Sensor data acquisition processes: managing states such as initialization, waiting for triggers, data acquisition, and processing.
- Device control processes: such as state transitions for motor control including start, acceleration, stabilization, deceleration, and stop.
Advantages:
- Reduces code complexity, avoiding multi-layer nested if-else statements.
- Improves code readability and maintainability, with clear state transition logic.
- Reduces memory usage, as state machines typically use enumerations and function pointers.
- Enhances execution efficiency, as state handling functions can be called directly, reducing conditional checking overhead.
2. Resource Pool Pattern
The resource pool pattern is a common solution in embedded systems for addressing dynamic memory allocation issues. The core idea of this pattern is to pre-allocate fixed-size memory blocks to form a memory pool, which can be reused during program execution, avoiding frequent dynamic memory allocation and deallocation. This is particularly important in resource-constrained embedded environments, effectively reducing memory fragmentation and improving memory utilization.
Implementation Example: Fixed-Size Memory Pool Implementation
// Memory pool structure definition
typedef struct {
size_t block_size; // Size of memory block
size_t num_blocks; // Number of memory blocks
size_t used_blocks; // Number of used memory blocks
void *mem_base; // Base address of memory pool
void **free_list; // Free memory block linked list
} memory_pool_t;
// Memory pool initialization function
memory_pool_t *memory_pool_init(size_t block_size, size_t num_blocks) {
memory_pool_t *pool = (memory_pool_t *)malloc(sizeof(memory_pool_t));
if (!pool) return NULL;
// Allocate memory pool and free list
pool->mem_base = malloc(block_size * num_blocks);
pool->free_list = malloc(sizeof(void *) * num_blocks);
if (!pool->mem_base || !pool->free_list) {
free(pool);
return NULL;
}
pool->block_size = block_size;
pool->num_blocks = num_blocks;
pool->used_blocks = 0;
// Build free list
for (size_t i = 0; i < num_blocks; i++) {
pool->free_list[i] = (void *)(pool->mem_base + i * block_size);
}
return pool;
}
// Allocate memory block from the memory pool
void *memory_pool_alloc(memory_pool_t *pool) {
if (pool->used_blocks >= pool->num_blocks) return NULL;
// Take the first free block
void *block = pool->free_list[0];
// Build new free list
for (size_t i = 0; i < pool->num_blocks - 1; i++) {
pool->free_list[i] = pool->free_list[i + 1];
}
pool->free_list[pool->num_blocks - 1] = NULL;
pool->used_blocks++;
return block;
}
// Free memory block back to the pool
void memory_pool_free(memory_pool_t *pool, void *block) {
if (!block || block < pool->mem_base || block >= pool->mem_base + pool->block_size * pool->num_blocks) {
return; // Invalid memory block, do not process
}
// Insert memory block at the head of the free list
for (size_t i = pool->num_blocks - 1; i > 0; i--) {
pool->free_list[i] = pool->free_list[i - 1];
}
pool->free_list[0] = block;
pool->used_blocks--;
}
// Memory pool destruction function
void memory_pool_destroy(memory_pool_t *pool) {
if (pool) {
free(pool->mem_base);
free(pool->free_list);
free(pool);
}
}
// Usage example: Create and use memory pool
void main() {
// Initialize memory pool
memory_pool_t *data_pool = memory_pool_init(64, 32); // 32 blocks of 64 bytes
// Allocate memory blocks
void *block1 = memory_pool_alloc(data_pool);
void *block2 = memory_pool_alloc(data_pool);
// Use memory blocks
if (block1) {
// Fill data...
memory_pool_free(data_pool, block1); // Free back to pool
}
// Destroy memory pool after all tasks are done
memory_pool_destroy(data_pool);
}
Application Scenarios:
- Frequent creation/destruction of objects: such as RTOS task control blocks, network packets, etc.
- Memory-constrained environments: such as 8-bit microcontrollers, microcontrollers, and other resource-constrained platforms.
- Systems with high real-time requirements: avoiding unpredictable delays from dynamic memory allocation.
- Systems preventing memory fragmentation: such as long-running embedded devices.
Advantages:
- Reduces memory fragmentation and improves memory utilization.
- Avoids frequent calls to malloc/free, reducing memory management overhead.
- Improves allocation and deallocation efficiency, as fixed-size blocks do not require complex algorithms.
- Predictable memory usage, facilitating system resource planning.
- Supports thread-safe implementations (requires additional synchronization mechanisms).
3. Event-Driven Pattern
The event-driven pattern is an effective way to handle asynchronous events in embedded systems, its core idea is to implement system responses and handling of various events through event flag groups and message queues. This pattern is particularly suitable for systems that need to handle multiple external inputs (such as buttons, sensors, communications, etc.) simultaneously, effectively managing event priorities and response orders.
Implementation Example: Flag-Based Event-Driven System
// Define event flags
typedef enum {
EVENT_KEY_Pressed = 1 << 0, // Key pressed event
EVENT_UART_Received = 1 << 1, // UART data received event
EVENT_Timer_Expired = 1 << 2, // Timer expired event
// Other events...
} event_flag_t;
// Event handling function collection
typedef struct {
void (*on_key_Pressed)(void);
void (*onUART_Received)(void);
void (*onTimer_Expired)(void);
// Other event handling functions...
} event_handler_t;
// Global event flags
volatile event_flag_t event_Flags = 0;
// Event handling function collection instance
event_handler_t event_handlers = {
.on_key_Pressed = key_PressedHandler,
.onUART_Received = uArtHandler,
.onTimer_Expired = timerHandler,
// Other event handling functions...
};
// Interrupt service function sets event flags
void key_PressedIsr(void) __interrupt(5) {
event_Flags |= EVENT_KEY_Pressed; // Set key event flag
}
void uArtRxIsr(void) {
event_Flags |= EVENT_UART_Received; // Set UART receive event flag
}
// Main loop handling events
void main() {
// Initialize hardware and event handling
sys_init();
hal_init();
// Set event handling functions
event_handlers.on_key_Pressed = key_PressedHandler;
event_handlers.onUART_Received = uArtHandler;
while (1) {
// Check and handle events
if (event_Flags & EVENT_KEY_Pressed) {
event_handlers.on_key_Pressed(); // Handle key event
event_Flags &= ~EVENT_KEY_Pressed; // Clear flag
}
if (event_Flags & EVENT_UART_Received) {
event_handlers.onUART_Received(); // Handle UART receive event
event_Flags &= ~EVENT_UART_Received; // Clear flag
}
// Handle other non-event-driven tasks
do_background_tasks();
}
}
// Event handling function example
void key_PressedHandler(void) {
// Get key state and handle
uint8_t key = hal_get_key();
switch (key) {
case KEY1:
// Handle key 1 pressed...
break;
case KEY2:
// Handle key 2 pressed...
break;
// Other key handling...
}
}
Application Scenarios:
- Multi-event handling systems: such as systems that handle multiple events like buttons, UART, timers, etc. simultaneously.
- Real-time response requirements: scenarios that require quick responses to external events.
- Interrupt handling: processing events triggered by interrupts in the main loop.
- Event priority management: achieving reasonable processing order of events through priority settings of flags.
Advantages:
- Decouples event generation and event handling, improving code modularity.
- Supports parallel handling of multiple events, enhancing system responsiveness.
- Facilitates the addition of new events, providing good extensibility.
- Can be combined with interrupt service patterns for efficient real-time responses.
- Event handling logic is clear, making it easy to debug and maintain.
4. Cyclic Execution Pattern
The cyclic execution pattern is a common method for scheduling tasks in embedded systems, its core idea is to execute a series of tasks in a main loop according to fixed time intervals or priority order. This pattern is particularly suitable for resource-constrained embedded systems that do not require an RTOS, enabling simple task scheduling and resource management.
Implementation Example: Time-Slice Based Cyclic Execution Scheduling
// Task structure definition
typedef struct {
void (*task)(void); // Task function pointer
uint32_t period; // Task execution period (milliseconds)
uint32_t last_execution_time; // Last execution time
bool is_active; // Is the task active
} cyclicExecutive_task_t;
// Task list
cyclicExecutive_task_t tasks[] = {
{sys_Check, 1000, 0, true}, // System check task, executed every 1000ms
{data_Collect, 500, 0, true}, // Data collection task, executed every 500ms
{led_Control, 100, 0, true}, // LED control task, executed every 100ms
// Other tasks...
};
// Number of tasks
#define Num_Tasks ( sizeof(tasks) / sizeof(tasks[0]) )
// Main loop function
void cyclicExecutive() {
static uint32_t lastCheckTime = 0;
uint32_t currentTime = HAL_GetTick();
// Check if it is time to execute tasks
for (size_t i = 0; i < Num_Tasks; i++) {
if (tasks[i].is_active && (currentTime - tasks[i].last_execution_time) >= tasks[i].period) {
tasks[i].task(); // Execute task
tasks[i].last_execution_time = currentTime; // Update last execution time
}
}
}
// Usage example: Call cyclic execution scheduling in the main function
void main() {
// Initialize system
sys_init();
// Start cyclic execution scheduling
while (1) {
cyclicExecutive(); // Handle all periodic tasks
// Other non-periodic tasks...
}
}
Application Scenarios:
- Task scheduling in non-RTOS systems: such as 8-bit microcontrollers and other resource-constrained platforms.
- Periodic data collection and processing: such as timed sensor data collection.
- Timer function replacement: implementing software timer functionality.
- System status monitoring: periodically checking the status of various system modules.
- Software interrupt handling: simulating hardware interrupt software implementations.
Advantages:
- Simple to implement, requiring no complex scheduling algorithms.
- Low resource usage, suitable for memory-constrained systems.
- Predictable task execution order, facilitating debugging.
- Can be extended to priority scheduling, enhancing system flexibility.
- Can be used in conjunction with state machine patterns to achieve complex system behaviors.
5. Interrupt Service Pattern
The interrupt service pattern is the core mechanism for handling hardware events in embedded systems, its core idea is to quickly respond to hardware interrupt events through interrupt service routines (ISRs) and perform subsequent processing in the main program. This pattern is particularly suitable for systems that require real-time responses to external events, such as button presses, sensors, communications, etc.
Implementation Example: C51 Microcontroller External Interrupt Control LED
#include <reg51.h>
// Define the port connected to the LED
sbit led = P1^0;
// External interrupt 0 service function
void int0_isr(void) interrupt 0 {
// Protect the context (simple example, no extra protection needed)
// Interrupt handling code, e.g., control LED toggle
led = ~led;
// Restore context (simple example, no extra restoration needed)
}
// Main function
void main() {
// Initialize interrupt system
EA = 1; // Enable global interrupts
EX0 = 1; // Enable external interrupt 0
IT0 = 1; // Set external interrupt 0 to trigger on falling edge
// Initialize LED
led = 1;
while (1) {
// Main program code
// Can perform other tasks without interruption from interrupts
do_other_tasks();
}
}
</reg51.h>
Application Scenarios:
- External button handling: such as user input, emergency stop, etc.
- Sensor data acquisition: real-time acquisition of sensor data such as temperature, humidity, etc.
- Communication protocol handling: interrupt handling for UART, SPI, I2C, etc.
- Timer functions: using timer interrupts for time management and periodic task triggering.
- External device interrupts: such as ADC conversion completion, timer overflow, etc.
Advantages:
- Real-time response to hardware events, ensuring system reliability.
- Interrupt handling functions are usually short and efficient, avoiding blocking the main program.
- Can implement multi-level interrupt handling, supporting events of different priorities.
- Can be combined with event-driven patterns to achieve complex system behaviors.
- Reduces CPU resource waste, improving system efficiency.
6. Facade Pattern
The facade pattern is an effective design pattern for simplifying complex hardware interfaces in embedded systems, its core idea is to encapsulate multiple complex subsystem interfaces through a facade object, providing a unified high-level interface to the outside. This pattern is particularly suitable for systems that need to operate multiple hardware interfaces, reducing code complexity and improving maintainability.
Implementation Example: SPI Interface Facade Pattern Implementation
// SPI register operation subsystem
typedef struct {
void (*config)(uint8_t clock_Polarity, uint8_t clock_Phase);
void (*send)(uint8_t data);
uint8_t (*receive)(void);
} halSpiSubSystem_t;
// SPI controller low-level operations
void halSpiConfig(uint8_t clock_Polarity, uint8_t clock_Phase) {
// Configure SPI controller registers
// For example: set clock polarity, phase, baud rate, etc.
}
void halSpiSend(uint8_t data) {
// Send data to SPI controller
// For example: write data to SPI data register
}
uint8_t halSpiReceive(void) {
// Receive data from SPI controller
// For example: read from SPI data register and return data
}
// SPI facade structure definition
typedef struct {
halSpiSubSystem_t halSpi; // Low-level SPI operation subsystem
void *context; // Context information
} spiFacade_t;
// Facade initialization function
void spiFacadeInit(spiFacade_t *facade) {
// Initialize low-level SPI subsystem
facade->halSpi.config = halSpiConfig;
facade->halSpi.send = halSpiSend;
facade->halSpi.receive = halSpiReceive;
// Configure SPI parameters
facade->halSpi.config(SPI_Clock_Polarity, SPI_Clock_Phase);
}
// Facade send data function
void spiFacadeSend(spiFacade_t *facade, uint8_t data) {
// Use low-level SPI subsystem to send data
facade->halSpi.send(data);
}
// Facade receive data function
uint8_t spiFacadeReceive(spiFacade_t *facade) {
// Use low-level SPI subsystem to receive data
return facade->halSpi.receive();
}
// Usage example: Control SPI communication through facade
void main() {
// Initialize SPI facade
spiFacade_t SPI;
spiFacadeInit(&SPI);
// Use facade to send data
uint8_t data_to_send[] = {0x01, 0x02, 0x03, 0x04};
for (size_t i = 0; i < sizeof(data_to_send); i++) {
spiFacadeSend(&SPI, data_to_send[i]);
}
// Use facade to receive data
uint8_t received_data = spiFacadeReceive(&SPI);
}
Application Scenarios:
- Hardware interface encapsulation: such as unified interfaces for communication interfaces like SPI, I2C, UART, etc.
- Coordinated operation of multiple hardware: such as scenarios controlling multiple peripherals simultaneously.
- Isolation between system layer and application layer: separating low-level hardware operations from upper-level application logic.
- Driver layer abstraction: providing unified driver interfaces for different hardware platforms.
- Modular design: encapsulating complex subsystems into simple interfaces for easier modular development.
Advantages:
- Hides underlying complexity, providing simple and easy-to-use interfaces.
- Reduces system coupling, improving module independence.
- Facilitates system portability and hardware replacement.
- Supports coordinated operation of multiple subsystems.
- Improves code readability and maintainability.
7. Producer-Consumer Pattern
The producer-consumer pattern is a common design pattern for handling data streams in embedded systems, its core idea is to coordinate the data flow between producers and consumers through a shared buffer, ensuring safe and effective data transfer between them. This pattern is particularly suitable for scenarios that require handling asynchronous data streams, such as sensor data acquisition, communication data transmission, etc.
Implementation Example: Non-RTOS Circular Buffer Producer-Consumer
// Circular buffer structure definition
typedef struct {
uint8_t *buffer; // Buffer pointer
size_t size; // Buffer size
size_t head; // Write pointer
size_t tail; // Read pointer
bool is_full; // Buffer full flag
bool is_empty; // Buffer empty flag
} ring_Buffer_t;
// Circular buffer initialization function
void ring_BufferInit(ring_Buffer_t *buffer, uint8_t *mem, size_t size) {
buffer->buffer = mem;
buffer->size = size;
buffer->head = 0;
buffer->tail = 0;
buffer->is_full = false;
buffer->is_empty = true;
}
// Producer function: puts data into the buffer
bool ring_BufferProduce(ring_Buffer_t *buffer, uint8_t data) {
if (buffer->is_full) return false; // Buffer is full, cannot produce
// Put data into the buffer
buffer->buffer[buffer->head] = data;
buffer->head = (buffer->head + 1) % buffer->size; // Circular pointer
// Update status flags
if (buffer->head == buffer->tail) buffer->is_full = true;
buffer->is_empty = false;
return true;
}
// Consumer function: takes data out of the buffer
bool ring_BufferConsume(ring_Buffer_t *buffer, uint8_t *data) {
if (buffer->is_empty) return false; // Buffer is empty, cannot consume
// Take data out of the buffer
*data = buffer->buffer[buffer->tail];
buffer->tail = (buffer->tail + 1) % buffer->size; // Circular pointer
// Update status flags
if (buffer->head == buffer->tail) buffer->is_full = false;
if (buffer->tail == buffer->head) buffer->is_empty = true;
return true;
}
// Interrupt service function as producer
void sensor_interruptIsr(void) interrupt 2 {
volatile ring_Buffer_t *sensor_Buffer = (ring_Buffer_t *)0x8000; // Assume buffer address
// Get sensor data
uint8_t sensor_Data = hal_Get_sensor_Data();
// Put data into the buffer (produce)
if (!ring_BufferProduce(sensor_Buffer, sensor_Data)) {
// Buffer is full, handle error or discard data
}
}
// Main loop as consumer
void main() {
// Initialize circular buffer
uint8_t sensor_Buffer_Mem[64]; // 64-byte buffer
ring_Buffer_t sensor_Buffer;
ring_BufferInit(&sensor_Buffer, sensor_Buffer_Mem, sizeof(sensor_Buffer_Mem));
while (1) {
// Take data out of the buffer (consume)
uint8_t data;
if (ring_BufferConsume(&sensor_Buffer, &data)) {
// Process sensor data...
}
// Other tasks...
do_other_tasks();
}
}
Application Scenarios:
- Sensor data acquisition and processing: the producer is the sensor interrupt service function, and the consumer is the data processing function.
- Communication data transmission: the producer is the communication interrupt receive function, and the consumer is the data parsing function.
- Data sharing between multiple tasks: data transfer between different tasks.
- Real-time data stream processing: such as real-time acquisition and processing of audio, video, etc.
- Data buffering and caching: providing data buffering for subsequent processing.
Advantages:
- Decouples data production and consumption, improving system modularity.
- Avoids data loss by balancing production and consumption speeds through the buffer.
- Supports asynchronous processing, enhancing system responsiveness.
- Allows control over buffer size, preventing memory overflow.
- Facilitates the addition of synchronization mechanisms for safe data transfer in multi-task environments.
8. Combined Application of Patterns
In actual embedded system development, single design patterns are often insufficient to meet complex system requirements, and the combined use of multiple patterns can build a more efficient, reliable, and maintainable system architecture. Below is a comprehensive application case of multiple design patterns in an embedded system:
// System configuration
#define SENSOR_DATA_BUFFER_SIZE 128
#define LED_STATE_TRANSITION_PERIOD 100
// State machine pattern: LED control state machine
typedef enum {
led_Off,
led_On,
led_blink,
led_flow
} led_State;
// Resource pool pattern: pre-allocate LED state handling functions
typedef struct {
void (*ledHandler)(void);
} led_StateHandler;
led_StateHandler led_StateHandlers[4] = {
{led_OffHandler}, // led Off state handling function
{led_OnHandler}, // led On state handling function
{led_blinkHandler}, // led blink state handling function
{led_flowHandler} // led flow state handling function
};
// Event-driven pattern: event flag group
typedef enum {
EVENT_KEY_Pressed = 1 << 0,
EVENT_UART_Received = 1 << 1,
EVENT_Timer_Expired = 1 << 2
} event_Flag;
volatile event_Flag event_Flags = 0;
// Producer-consumer pattern: sensor data buffer
typedef struct {
uint8_t *buffer;
size_t size;
size_t head;
size_t tail;
bool is_full;
bool is_empty;
} ring_Buffer;
ring_Buffer sensor_Data_Buffer;
uint8_t sensor_Data_Mem[SENSOR_DATA_BUFFER_SIZE];
// Facade pattern: LED control facade
typedef struct {
led_State current_State;
void (*led_On)(void);
void (*led_Off)(void);
void (*led_blink)(void);
} led_Facade;
led_Facade led_Facade = {
.led_On = hal_led_On,
.led_Off = hal_led_Off,
.led_blink = hal_led_blink
};
// Interrupt service pattern: key interrupt
void key_PressedIsr(void) interrupt 1 {
event_Flags |= EVENT_KEY_Pressed;
}
// Interrupt service pattern: timer interrupt
void timer_ExpiredIsr(void) interrupt 2 {
event_Flags |= EVENT_Timer_Expired;
}
// Main loop: cyclic execution pattern
void main() {
// Initialize system
hal_init();
led_Facade.led_On();
ring_BufferInit(&sensor_Data_Buffer, sensor_Data_Mem, sizeof(sensor_Data_Mem));
// Set LED state machine
led_State current_State = led_Off;
uint32_t last_State_Time = 0;
// Set timer
hal_Set_Timer_Period(LED_STATE_TRANSITION_PERIOD);
while (1) {
// Check and handle events
if (event_Flags & EVENT_KEY_Pressed) {
// Key pressed, switch LED state
current_State = (current_State + 1) % 4;
event_Flags &= ~EVENT_KEY_Pressed;
// Record state transition time
last_State_Time = HAL_GetTick();
}
// Check timer interrupt flag
if (event_Flags & EVENT_Timer_Expired) {
event_Flags &= ~EVENT_Timer_Expired;
// Check if it is time to transition state
if (HAL_GetTick() - last_State_Time >= led_StateHandlers[current_State].period) {
// Execute LED state handling
led_StateHandlers[current_State].ledHandler();
}
}
// Check and handle sensor data
if (!ring_BufferIs_Empty(&sensor_Data_Buffer)) {
uint8_t data;
if (ring_BufferConsume(&sensor_Data_Buffer, &data)) {
// Process sensor data...
hal_Process_sensor_Data(data);
}
}
// Other tasks...
do_background_tasks();
}
}
Combined Application Analysis:
- Facade Pattern: Encapsulates LED control interface, hiding low-level hardware operations.
- State Machine Pattern: Manages transitions between different LED states (On, Off, Blink, Flow).
- Interrupt Service Pattern: Handles key and timer interrupt events.
- Event-Driven Pattern: Passes interrupt information to the main loop through event flag groups.
- Resource Pool Pattern: Pre-allocates LED state handling functions, avoiding dynamic memory allocation.
- Producer-Consumer Pattern: Sensor data acts as the producer, while the main loop acts as the consumer.
Advantages:
- Integrates the advantages of multiple patterns, constructing an efficient system architecture.
- Modular design, facilitating maintenance and expansion.
- Optimizes resource management, improving memory utilization.
- Ensures real-time responsiveness, guaranteeing system reliability.
- Code structure is clear, making it easy to understand and debug.
9. Conclusion and Best Practices
The application of design patterns in C language embedded programming can significantly enhance code quality and system performance. Choosing appropriate design patterns based on the characteristics of embedded systems (resource constraints, high real-time requirements, frequent hardware interactions) and using them in a reasonable combination is key to building efficient and reliable embedded systems.
Summarizing the advantages and applicable scenarios of each design pattern:
| Design Pattern | Main Advantages | Applicable Scenarios | Resource Usage |
|---|---|---|---|
| State Machine Pattern | Clear logic, low resource usage | Communication protocol state management, device control processes | Low |
| Resource Pool Pattern | Reduces memory fragmentation, improves utilization | Dynamic memory-constrained environments, frequent creation/destruction of objects | Medium |
| Event-Driven Pattern | Decouples event generation and handling, improves responsiveness | Multi-event handling systems, interrupt handling | Low |
| Cyclic Execution Pattern | Predictable task execution order, simple implementation | Task scheduling in non-RTOS systems | Low |
| Interrupt Service Pattern | Real-time response to hardware events, ensures system reliability | External button handling, sensor data acquisition | Low |
| Facade Pattern | Hides underlying complexity, reduces coupling | Hardware interface encapsulation, multi-subsystem coordinated operation | Low |
| Producer-Consumer Pattern | Decouples data production and consumption, balances processing speed | Sensor data acquisition and processing, communication data transmission | Medium |
Best Practice Recommendations:
- Select appropriate patterns based on system resources: In resource-constrained 8-bit microcontrollers, prioritize low resource usage patterns such as state machines, cyclic execution, and interrupt service.
- Use patterns in combination: Single patterns are often insufficient to meet complex requirements; reasonably combine multiple patterns to construct system architecture.
- Avoid over-design: Embedded systems typically have limited resources; the choice of design patterns should meet requirements while avoiding unnecessary complexity.
- Focus on real-time performance: When selecting and implementing design patterns, fully consider the real-time requirements of the system.
- Optimize code: When implementing design patterns, focus on code efficiency, reducing unnecessary memory usage and computational overhead.
By reasonably applying these design patterns, embedded engineers can build more efficient, reliable, and maintainable systems while reducing development and maintenance costs. Design patterns are not a silver bullet, but tools in a toolbox; only by selecting the appropriate patterns based on specific needs and applying them correctly can their value be fully realized. In actual development, continuously learning and practicing various design patterns will gradually form a design pattern library suitable for one’s projects, improving development efficiency and system quality.
Note: The content of this report is generated by Tongyi AI and is for reference only.