嵌入式开发工程必备的六大设计模式
设计模式是软件开发中前人总结的经典“套路”,用于解决特定场景下的代码设计问题。在嵌入式开发中,合理使用设计模式能极大提升代码的可维护性、可扩展性和可移植性。
但要特别注意:嵌入式资源有限(ROM/RAM),不能生搬硬套(比如避免滥用malloc动态内存)。下面我结合C语言(GD32常用) 和嵌入式实战场景,为你详解这6种模式。
1. 单例模式 (Singleton)
**目的:**保证一个类(结构体)在系统中只有一个实例,并提供一个全局访问点。
**嵌入式场景:**管理唯一的硬件资源,如系统日志模块、唯一的外设(如RTC) 或全局配置管理器。
C语言实现(核心技巧:静态局部变量):
// sys_config.h
typedef struct {
int baud_rate;
int sys_tick;
} SysConfig_t;
// sys_config.c
SysConfig_t* GetSysConfig(void) {
// 静态局部变量,存放在静态存储区,只会被初始化一次
static SysConfig_t instance = {
.baud_rate = 115200,
.sys_tick = 0
};
return &instance; // 返回统一指针
}
// 使用:任何地方调用 GetSysConfig() 拿到的都是同一个地址
GetSysConfig()->baud_rate = 9600;
注意事项:
在裸机(无RTOS)下安全;在RTOS多任务下,若涉及写操作,需加互斥锁保护。
2. 工厂模式 (Factory)
**目的:**定义一个用于创建对象(结构体)的接口,让子类/具体模块决定实例化哪一个类。将“创建”和“使用”分离。
**嵌入式场景:**系统挂载了多个不同型号的传感器(如温度传感器有DS18B20、SHT30),应用层不需要知道具体型号,只需向“工厂”要一个“温度传感器对象”。
C语言实现(核心技巧:函数指针 + 条件分支):
// 抽象传感器接口
typedef struct {
int (*init)(void);
float (*read)(void);
} SensorOps_t;
// 工厂:根据类型号返回具体的操作函数集
SensorOps_t* SensorFactory_Create(int type) {
static SensorOps_t ds18b20_ops = {DS18B20_Init, DS18B20_Read};
static SensorOps_t sht30_ops = {SHT30_Init, SHT30_Read};
switch(type) {
case TYPE_DS18B20: return &ds18b20_ops;
case TYPE_SHT30: return &sht30_ops;
default: return NULL;
}
}
// 应用层使用
SensorOps_t* temp_sensor = SensorFactory_Create(TYPE_SHT30);
temp_sensor->init();
float temp = temp_sensor->read();
优势:
新增传感器时,只需修改工厂函数,上层应用代码(init/read调用)完全不用改。
3. 观察者模式 (Observer)
**目的:**定义对象间的一对多依赖关系。当一个对象(主题)状态改变时,所有依赖它的对象(观察者)都会自动收到通知并更新。
**嵌入式场景:**按键中断触发,需要同时通知“LED闪烁模块”、“蜂鸣器模块”和“界面显示模块”。
C语言实现(核心技巧:回调函数链表):
#define MAX_OBSERVERS 5
typedef void (*NotifyCallback)(int event);
typedef struct {
NotifyCallback callbacks[MAX_OBSERVERS];
int count;
} Subject_t;
void Subject_Attach(Subject_t* sub, NotifyCallback cb) {
if (sub->count < MAX_OBSERVERS) sub->callbacks[sub->count++] = cb;
}
void Subject_Notify(Subject_t* sub, int event) {
for (int i = 0; i < sub->count; i++) {
sub->callbacks[i](event); // 逐个通知观察者
}
}
// 使用示例
Subject_t key_subject;
Subject_Attach(&key_subject, LED_Blink);
Subject_Attach(&key_subject, Buzzer_Beep);
// 按键中断触发时:
Subject_Notify(&key_subject, KEY_PRESSED);
注意:
观察者模式通常是同步调用的(在通知者线程中执行),执行时长不能阻塞。
4. 状态模式 (State)
**目的:**允许对象在内部状态改变时改变其行为。将复杂的 if-else / switch-case 状态机解耦,把每个状态的行为封装到独立的“状态对象”中。
**嵌入式场景:**复杂的通信协议握手(如TCP状态)、菜单导航界面、电源管理(运行/休眠/故障)。
C语言实现(核心技巧:函数指针表):
// 定义状态操作接口
typedef struct StateMachine StateMachine_t;
typedef struct {
void (*handle)(StateMachine_t* machine, int event);
} State_t;
// 定义状态机上下文
struct StateMachine {
const State_t* current_state;
int data;
};
// 具体状态A的handle函数
static void StateA_Handle(StateMachine_t* machine, int event) {
if (event == EVT_NEXT) {
printf("State A -> State B");
machine->current_state = &state_b; // 切换状态
}
}
// 使用
machine.current_state = &state_a;
machine.current_state->handle(&machine, EVT_NEXT);
优势:
新增“State C”时,无需修改旧状态逻辑,符合“开闭原则”。
5. 发布-订阅模式 (Publish-Subscribe)
**目的:**这是观察者模式的“进阶解耦版”。发布者(Publisher)不直接通知订阅者(Subscriber),而是通过一个中间件(消息总线/事件队列) 转发。发布者和订阅者完全不知道对方的存在。
**嵌入式场景:**RTOS环境下的跨任务通信(如FreeRTOS的消息队列)、GUI框架的事件分发。
区别对比(重点!):
(1)观察者模式:同步,主题持有观察者指针,直接调用回调(两者耦合在一起)。
(2)发布-订阅:异步,通过中间层(队列)传递。发布者只管发消息,订阅者只管收消息。
C语言实现(基于RTOS队列):
// 发布者任务
void Task_Sensor(void *pv) {
int temp = ReadTemp();
xQueueSend(g_event_queue, &temp, 0); // 只管发,不管谁收
}
// 订阅者任务1(界面)
void Task_Display(void *pv) {
int temp;
while(1) {
if (xQueueReceive(g_event_queue, &temp, portMAX_DELAY)) {
UpdateLCD(temp);
}
}
}
// 订阅者任务2(存储)
void Task_Storage(void *pv) {
int temp;
while(1) {
if (xQueueReceive(g_event_queue, &temp, portMAX_DELAY)) {
SaveToSD(temp);
}
}
}
6. 适配器模式 (Adapter)
**目的:**将一个类的接口转换成客户希望的另一个接口。解决接口不兼容的问题,让原本不能一起工作的类可以一起工作。
**嵌入式场景:**硬件抽象层(HAL) 的终极体现。例如,你的代码原先是基于ST库的 HAL_GPIO_WritePin,现在要移植到GD32库的 gpio_bit_write,不需要改应用层,只需写一个适配器。
C语言实现(核心技巧:中间层封装):
// 目标接口(应用层期望的)
typedef struct {
void (*write_pin)(uint8_t level);
} GpioAdapter_t;
// 适配器实现:将GD32特有的API,适配成目标接口
static void GD32_WritePin_Impl(uint8_t level) {
// 内部调用GD32原厂库函数
gpio_bit_write(GPIOA, GPIO_PIN_8, (bit_status)level);
}
GpioAdapter_t CreateLedAdapter(void) {
GpioAdapter_t adapter;
adapter.write_pin = GD32_WritePin_Impl;
return adapter;
}
// 应用层:永远调用 adapter.write_pin(1),感受不到底层是GD32还是ST。
优势:
这是代码跨平台移植的利器。
总结:嵌入式开发怎么选?
| 设计模式 | 核心价值 | 嵌入式推荐场景 | 资源消耗(CPU/RAM) |
|---|---|---|---|
| 单例模式 | 唯一性管理 | 系统配置、日志、唯一外设 | 极低 |
| 工厂模式 | 创建与使用分离 | 多型号传感器/屏幕驱动抽象 | 低(仅函数指针) |
| 观察者模式 | 同步一对多通知 | 按键触发联动、简单中断分发 | 低(维护回调数组) |
| 状态模式 | 复杂状态逻辑解耦 | 通信协议、菜单、电源管理 | 中(需维护多个状态结构体) |
| 发布-订阅 | 异步完全解耦 | 带RTOS的多任务通信(队列) | 较高(需RAM做消息队列) |
| 适配器模式 | 接口转换 | HAL层移植、老代码适配新库 | 极低(纯封装) |
如果你是新手,建议优先掌握 适配器模式(用来封装外设驱动)和 状态模式(用来写按键/协议逻辑),这两者立竿见影。等项目引入RTOS后,发布-订阅(消息队列)自然就会成为你的主力工具。
更多推荐
所有评论(0)