嵌入式UI刷新:观察者模式实战

一、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);
}

这确实解耦了,但带来了新问题:

  1. 轮询开销大:不管数据变没变,都在读

  2. 实时性差:最坏情况要等一个周期

  3. 浪费资源: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(&current_temp, lcd_temp_callback);
lcd_subscribe(&target_temp, lcd_target_callback);

// 蓝牙也订阅同样的温度
ble_subscribe(&current_temp, ble_temp_callback);

// 某个地方修改温度(如传感器读取)
void sensor_update() {
    int new_temp = read_sensor();
    smart_int_set(&current_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 刷新上,还能扩展到:

  • 网络数据同步

  • 配置管理

  • 事件系统

  • 状态机通知

这才是嵌入式架构设计的进阶之路。

相关推荐
纳祥科技2 小时前
NX6802,4路音频DAC芯片,具备90dB 动态范围 -90 dB THD+N
单片机·音视频
海雅达手持终端PDA2 小时前
海雅达 Model 10X 工业平板赋能2026新能源汽车全链条数字化升级方案
android·物联网·5g·汽车·能源·制造·平板
安庆平.Я2 小时前
STM32——DMA
stm32·单片机·嵌入式硬件
摆摊的豆丁3 小时前
AWS IoT MQTT File Streams 性能优化分析
物联网·性能优化·freertos·aws
北京耐用通信3 小时前
水处理PH监测难题如何破?耐达讯自动化Profibus光纤链路模块来解答
人工智能·科技·物联网·网络协议·自动化·信息与通信
梁下轻语的秋缘3 小时前
初学者避坑指南:Mac 虚拟机搭建 Keil5 STM32 环境 + 解决 ST-Link USB Command Error 报错
windows·stm32·macos
DLGXY3 小时前
STM32——OLED显示屏(五)
stm32·单片机·嵌入式硬件
CQ_YM3 小时前
ARM之uart
c语言·arm开发·单片机·嵌入式硬件
AAAAA92403 小时前
物联网模组在农业环境监测中的应用与价值
物联网·农业