一、UI 刷新困境:扫不完的雷
做嵌入式开发,你一定遇到过这样的场景:一个全局变量,被 N 个模块修改,然后要通知 M 个模块更新。
// 传统做法:改一处,通知多处
void update_temperature(int new_temp) {
current_temp = new_temp; // 修改数据
LCD_UpdateTemp(new_temp); // 刷新屏幕
Flash_SaveTemp(new_temp); // 保存到存储
Led_UpdateColor(new_temp); // 更新LED
BLE_SendTemp(new_temp); // 蓝牙发送
// ... 还有更多
}
问题来了:
-
新增一个显示设备(比如 OLED),要修改所有修改数据的地方
-
漏掉一个通知?Bug 就等着你
-
代码像蜘蛛网,牵一发而动全身
这就是 Push 模式 的困境:谁修改数据,谁负责通知所有关心的人。
二、换个思路:从 Push 到 Pull 的误解
有人会想:"那我让显示模块自己定时去读数据不就行了?"
// 定时器每秒读一次
void timer_callback() {
int temp = get_current_temp(); // 主动读取
LCD_UpdateTemp(temp);
}
这确实解耦了,但带来了新问题:
-
轮询开销大:不管数据变没变,都在读
-
实时性差:最坏情况要等一个周期
-
浪费资源:CPU 和总线带宽被无效操作占用
我们需要的是:数据变了,自动通知。
三、观察者模式:让数据会"说话"
核心思想:让数据成为被观察的对象,谁关心就订阅谁。
/* 观察者模式的比喻 */
// 数据是"主播",模块是"观众"
// 主播开播 → 所有观众收到通知
// 新观众加入 → 只需订阅,不用改主播代码
3.1 极简实现
// 观察者回调函数类型
typedef void (*notify_func)(void* data);
// 被观察对象
struct temperature_sensor {
int value;
notify_func observers[10]; // 最多10个观察者
int count;
};
// 数据变化时
void temp_changed(struct temperature_sensor* sensor, int new_value) {
sensor->value = new_value;
// 自动通知所有观察者
for (int i = 0; i < sensor->count; i++) {
sensor->observers[i](&new_value);
}
}
// LCD订阅温度变化
lcd_subscribe_temp(temp_changed_callback);
// 蓝牙也订阅同样的温度变化
ble_subscribe_temp(temp_changed_callback);
优势:
-
新增观察者:只需订阅,不修改数据源
-
数据源:只负责通知,不关心谁在看
-
真正的解耦
四、从观察者到 MVC:架构的演进
4.1 MVC 思想:各司其职
MVC 是观察者模式的升级版,明确划分了三个角色:
┌─────────────────────────────────────┐
│ 用户操作/传感器数据 │
└────────────────┬────────────────────┘
│
▼
┌────────────────┐
│ Controller │ ← 处理输入,修改Model
└────────────────┘
│
▼
┌────────────────┐
│ Model │ ← 存储数据,数据变化时通知View
└────────────────┘
│
┌───────┴────────┐
▼ ▼
┌─────────────┐ ┌─────────────┐
│ View 1 │ │ View 2 │ ← 显示数据,订阅Model变化
│ (LCD屏) │ │ (手机APP) │
└─────────────┘ └─────────────┘
实际例子:温控器
/* Model - 纯粹的数据 */
typedef struct {
int current_temp; // 当前温度
int target_temp; // 目标温度
int heater_power; // 加热器功率
// 观察者列表
void (*on_change)(void*);
} TemperatureModel;
/* View - 只负责显示 */
void lcd_view_init() {
// 订阅Model变化
model_subscribe(&temp_model, lcd_update_callback);
}
void lcd_update_callback(TemperatureModel* model) {
// 只做显示相关的事情
lcd_show_temp(model->current_temp);
lcd_show_target(model->target_temp);
}
/* Controller - 处理输入 */
void button_handler(int button) {
if (button == UP_BUTTON) {
// 修改Model
model_set_target_temp(&temp_model,
temp_model.target_temp + 1);
// 不需要手动调用显示更新!
// Model变化会自动通知所有View
}
}
4.2 MVVM:更现代的架构
MVVM 是 MVC 的变种,特别适合有双向绑定的场景:
┌─────────────────────────────────────┐
│ 用户操作/传感器数据 │
└────────────────┬────────────────────┘
│
┌────────┴─────────┐
▼ ▼
┌─────────────┐ ┌─────────────────┐
│ View │ │ ViewModel │ ← 中间层,为View提供数据
│ (UI组件) │ │ (绑定View和Model)│
└──────┬──────┘ └─────────────────┘
│ │
└───────绑定─────────┘
│
▼
┌────────────────┐
│ Model │ ← 纯数据
└────────────────┘
MVVM 的关键是"绑定" :View 和 ViewModel 自动同步,不用写 view.update(data)这样的代码。
五、嵌入式中的实战:从理论到代码
5.1 场景:智能温控器
假设我们要做一个温控器:
-
LCD 显示当前温度
-
手机APP可远程查看/控制
-
按键可本地设置
-
温度超标要声光报警
传统写法的问题:
// 传统做法:处处耦合
void set_temperature(int new_temp) {
current_temp = new_temp;
// 要记得更新所有地方!
update_lcd();
save_to_flash();
send_to_bluetooth();
check_alarm();
update_heater_pid();
// 忘记任何一个都可能出Bug
}
MVC 改造后:
// Model定义
typedef struct {
int temperature;
int target;
// 观察者支持
List observers;
} TempModel;
// Controller: 处理按键
void on_up_button() {
temp_model.target++; // 只修改Model
// Model会自动通知所有观察者
}
// View 1: LCD显示
void lcd_view_init() {
// 订阅感兴趣的数据
model_add_observer(&temp_model, lcd_temp_changed);
model_add_observer(&temp_model, lcd_target_changed);
}
// View 2: 蓝牙模块
void ble_view_init() {
// 订阅同样的数据
model_add_observer(&temp_model, ble_send_temp_update);
}
5.2 更实际的代码示例
/* 智能属性:自动通知的变量 */
typedef struct {
int value;
ObserverList observers;
} SmartInt;
// 设置值(自动通知)
void smart_int_set(SmartInt* obj, int new_value) {
if (obj->value != new_value) { // 只有真正变化才通知
obj->value = new_value;
// 通知所有观察者
for_each_observer(obj->observers, observer) {
observer->callback(new_value, observer->context);
}
}
}
/* 使用示例 */
SmartInt current_temp = {0};
SmartInt target_temp = {0};
// LCD订阅
lcd_subscribe(¤t_temp, lcd_temp_callback);
lcd_subscribe(&target_temp, lcd_target_callback);
// 蓝牙也订阅同样的温度
ble_subscribe(¤t_temp, ble_temp_callback);
// 某个地方修改温度(如传感器读取)
void sensor_update() {
int new_temp = read_sensor();
smart_int_set(¤t_temp, new_temp); // 自动通知所有订阅者
}
六、在 LVGL 中应用 MVC/MVVM
LVGL 是一个流行的嵌入式 GUI 库,天然适合 MVC/MVVM。
6.1 简单绑定示例
// 创建滑块控制目标温度
lv_obj_t* slider = lv_slider_create(lv_scr_act());
lv_slider_set_range(slider, 0, 100);
// 滑块变化 → 修改Model
lv_obj_add_event_cb(slider, [](lv_event_t* e) {
int value = lv_slider_get_value(e->target);
model_set_target_temp(&temp_model, value);
}, LV_EVENT_VALUE_CHANGED, NULL);
// Model变化 → 更新滑块
model_add_observer(&temp_model.target, [](int value, void* ctx) {
lv_slider_set_value((lv_obj_t*)ctx, value, LV_ANIM_ON);
}, slider);
6.2 完整温控器 UI 示例
// Model
typedef struct {
SmartInt current_temp; // 当前温度
SmartInt target_temp; // 目标温度
SmartBool heating; // 加热状态
} ThermostatModel;
// View: 创建UI并绑定
void create_thermostat_ui(ThermostatModel* model) {
// 1. 温度显示标签
lv_obj_t* temp_label = lv_label_create(lv_scr_act());
// 绑定:Model → Label
smart_int_add_observer(&model->current_temp,
[](int value, void* ctx) {
char buf[32];
snprintf(buf, sizeof(buf), "当前: %d°C", value);
lv_label_set_text((lv_obj_t*)ctx, buf);
}, temp_label);
// 2. 目标温度滑块
lv_obj_t* slider = lv_slider_create(lv_scr_act());
lv_slider_set_range(slider, 10, 30);
// 双向绑定
// View → Model
lv_obj_add_event_cb(slider,
[](lv_event_t* e) {
lv_obj_t* s = lv_event_get_target(e);
int value = lv_slider_get_value(s);
ThermostatModel* m = (ThermostatModel*)lv_event_get_user_data(e);
smart_int_set(&m->target_temp, value);
}, LV_EVENT_VALUE_CHANGED, model);
// Model → View
smart_int_add_observer(&model->target_temp,
[](int value, void* ctx) {
lv_slider_set_value((lv_obj_t*)ctx, value, LV_ANIM_ON);
}, slider);
// 3. 加热状态指示灯
lv_obj_t* led = lv_led_create(lv_scr_act());
// 绑定加热状态
smart_bool_add_observer(&model->heating,
[](bool is_on, void* ctx) {
if (is_on) {
lv_led_on((lv_obj_t*)ctx);
lv_obj_set_style_bg_color((lv_obj_t*)ctx,
lv_palette_main(LV_PALETTE_RED), 0);
} else {
lv_led_off((lv_obj_t*)ctx);
}
}, led);
}
七、性能考量与优化
嵌入式系统资源有限,需要考虑:
7.1 内存优化
// 紧凑型观察者(节省内存)
typedef struct {
uint8_t callback_id; // 回调函数ID
void* context; // 上下文指针
} CompactObserver;
// 用数组代替链表(如果观察者数量已知)
struct {
CompactObserver observers[MAX_OBSERVERS];
uint8_t count;
} observer_list;
7.2 避免过度更新
// 防抖:避免频繁更新
void smart_int_set_debounce(SmartInt* obj, int new_value, int delay_ms) {
if (obj->value != new_value) {
obj->value = new_value;
// 延迟通知,合并快速连续变化
if (!obj->debounce_timer_active) {
start_timer(delay_ms, notify_observers, obj);
obj->debounce_timer_active = true;
}
}
}
7.3 按需更新
// 只有可见的View才更新
void lcd_temp_callback(int value, void* ctx) {
if (is_screen_active(SCREEN_MAIN)) { // 只在主屏幕更新
update_temp_display(value);
}
// 否则跳过,节省CPU
}
八、总结:思维转变的价值
从"我要通知谁"到"谁想被通知"
| 传统思维 | 观察者/MVC思维 |
|---|---|
| 我改了数据,得通知A、B、C | 我改了数据,谁订阅了就会知道 |
| 新增功能要改多处代码 | 新增功能只需订阅关心数据 |
| 数据流是发散的 | 数据流是收敛的 |
| 容易遗漏通知 | 不会遗漏(订阅了就有) |
| 耦合度高,难维护 | 解耦,易维护 |
什么时候用?
-
✅ 多个模块关心同一数据:温度被 LCD、蓝牙、存储、报警共同关注
-
✅ 需要多种显示方式:既有本地屏幕,又有远程APP
-
✅ 数据源不确定:温度可能来自传感器、网络、用户设置
-
✅ 需要良好的扩展性:未来可能增加新功能
-
❌ 简单单次操作:只需要执行一次的操作
-
❌ 资源极其紧张:连几十字节内存都没有
-
❌ 对实时性要求极高:不能有任何延迟
最终效果
用上观察者/MVC 后,你的代码会变成这样:
// 初始化时建立关系
void system_init() {
// 数据源
TemperatureModel model = {0};
// 谁关心温度?订阅就好
lcd_subscribe_temp(&model);
ble_subscribe_temp(&model);
alarm_subscribe_temp(&model);
logger_subscribe_temp(&model);
// 新增功能?继续订阅
oled_subscribe_temp(&model);
mqtt_subscribe_temp(&model);
// 修改数据时,只需:
model.current_temp = read_sensor();
model_notify(&model); // 一行通知,所有订阅者自动更新
}
代码变得干净、清晰、可扩展。新增功能就像订报纸------告诉系统你对什么感兴趣,更新会自动送到。
这种架构思维,不仅能用在 UI 刷新上,还能扩展到:
-
网络数据同步
-
配置管理
-
事件系统
-
状态机通知
这才是嵌入式架构设计的进阶之路。