Skip to content
约 0 字 · 预计阅读 0 分钟

应用层如何监听底层变化?从轮询到观察者模式

要点速览

  • 轮询是应用层主动拉取,简单直接但耦合度高
  • 回调是底层主动推送,实时性好但扩展性有限
  • 观察者模式是订阅-通知机制,解耦彻底,支持一对多
  • 选择方案的原则:从简单开始,按需升级

如果你正在纠结"一个事件要通知多个模块"的问题,直接跳到观察者模式

前置知识

阅读本文前,你需要了解:

  • C 语言函数指针的基本用法
  • 嵌入式开发中中断和主循环的概念

本文不假设你了解:

  • 任何设计模式
  • RTOS 相关知识

问题引入

做嵌入式开发,有个问题几乎每个项目都会遇到:底层硬件状态变了,应用层怎么知道?

一个真实的场景

假设你在做一个温控系统:

  • 底层有个温度传感器驱动,每隔一段时间采集一次温度值
  • 应用层需要根据温度来控制风扇转速——温度高了加速,温度低了减速

问题来了:应用层怎么拿到最新的温度值?

你可能会说,这还不简单,直接读就行了。没错,但"怎么读"、"什么时候读",这里面的门道其实不少。处理得粗糙,代码能跑但难维护;处理得讲究,代码不仅清晰,后面改需求也不怕。

今天就来聊聊嵌入式开发中,应用层监听底层变化的几种常见做法,从最朴素的到比较优雅的,一步步来。


方式一:轮询——最直觉的笨办法

轮询(Polling),说白了就是应用层主动、反复地去问底层:"数据变了没?变了没?"

就像你等快递,每隔五分钟就打开手机查一次物流。能不能拿到快递?能。累不累?累。

代码实现

c
// 片段:轮询方式的核心逻辑
void app_task(void)
{
    static int last_temp = 0;

    while (1) {
        int cur_temp = drv_temp_read();  // 直接调用底层驱动读温度

        if (cur_temp != last_temp) {
            last_temp = cur_temp;
            fan_adjust(cur_temp);         // 温度变了,调整风扇
        }

        delay_ms(100);  // 每100ms查一次
    }
}

上述代码实现了轮询方式的核心逻辑:

函数说明:

app_task 是应用层的主任务函数,在主循环中反复执行。

逐行解释:

static int last_temp = 0 - 用静态变量保存上次的温度值,用于判断是否发生变化。

drv_temp_read() - 直接调用底层驱动接口读取当前温度。

if (cur_temp != last_temp) - 只有温度变化时才执行后续逻辑,避免无意义的操作。

delay_ms(100) - 轮询间隔,决定了检测的频率。

问题分析

这段代码能用吗?当然能用。但问题也很明显:

问题说明
紧耦合app_task 直接调用了 drv_temp_read(),如果换个传感器,应用层也得改
CPU 空转大部分时间温度根本没变,但你还是在不停地读
实时性靠缘分如果轮询间隔是 100ms,那最坏情况下你要等 99ms 才能发现变化

适用场景

轮询适合什么场景?数据变化频率高、对实时性要求不高、系统简单的情况。

比如一个只有几个外设的裸机系统,轮询完全够用,没必要搞复杂。但如果你的系统有十几个底层模块都需要监听,全用轮询,主循环就会变成一坨"查询大杂烩",维护起来头疼。


方式二:回调函数——"别找我,有事我叫你"

既然轮询是"应用层主动去问",那能不能反过来?底层数据变了,主动通知应用层。这就是回调(Callback)的思路。

换成快递的例子:你不用一直刷手机了,快递到了快递员直接给你打电话。

驱动层实现

c
// 片段:驱动层的回调机制
typedef void (*temp_callback_t)(int temp);

static temp_callback_t g_cb = NULL;

// 应用层调用此函数注册回调
void drv_temp_register_cb(temp_callback_t cb)
{
    g_cb = cb;
}

// 驱动内部:中断或定时采集后调用
void drv_temp_isr(void)
{
    int temp = read_sensor_hw();
    if (g_cb) {
        g_cb(temp);  // 数据变了,通知应用层
    }
}

上述代码实现了驱动层的回调注册和触发机制:

类型定义说明:

类型说明
temp_callback_t回调函数指针类型,指向"返回 void、接受 int 参数"的函数

变量说明:

变量类型说明
g_cbtemp_callback_t全局回调函数指针,初始化为 NULL

函数说明:

drv_temp_register_cb - 供应用层调用,注册回调函数。

drv_temp_isr - 驱动内部调用,通常在中断服务程序中执行。if (g_cb) 检查是关键,防止空指针调用导致崩溃。

应用层实现

c
// 片段:应用层注册回调
void on_temp_changed(int temp)
{
    fan_adjust(temp);  // 收到通知,直接处理
}

void app_init(void)
{
    drv_temp_register_cb(on_temp_changed);  // 注册回调
}

上述代码展示了应用层如何注册回调:

函数说明:

on_temp_changed - 回调函数,符合驱动层定义的函数签名。

app_init - 初始化时注册回调,建立应用层和驱动层的联系。

优缺点分析

和轮询相比,回调有几个明显的好处:

优势说明
实时性好数据一变就通知,不用等轮询间隔
CPU 不白忙没有变化时,应用层可以安心做别的事
耦合降低应用层不需要知道驱动内部怎么采集数据

但回调也不是没毛病:

问题说明
接口约定驱动层要"认识"应用层的函数签名,双方要约定好接口
单一回调上面的例子只存了一个 g_cb,如果风扇模块和报警模块都想监听温度变化呢?你得改成数组,管理起来就复杂了
中断上下文如果回调在中断里执行,处理逻辑不能太重,否则影响系统实时性

回调函数在实际项目中用得非常多,尤其是驱动和中间件之间的交互。但当"一个事件需要通知多个模块"的需求出现时,简单的回调就显得力不从心了。


方式三:观察者模式——一处变化,多方响应

继续用快递的比喻:这次不是你一个人等快递,而是你、你室友、你女朋友都在等同一个包裹。快递员不可能一个个打电话,最好的办法是大家都"订阅"了物流通知,包裹一到,所有人同时收到短信。

这就是**观察者模式(Observer Pattern)**的核心思想:被观察的对象维护一个订阅者列表,状态变化时遍历列表逐个通知。

观察者框架实现

c
// 片段:观察者模式的核心框架
typedef void (*observer_func_t)(int value);

#define MAX_OBSERVERS  8

typedef struct {
    observer_func_t observers[MAX_OBSERVERS];
    int count;
} subject_t;

void subject_init(subject_t *sub)
{
    sub->count = 0;
}

// 订阅:把自己的处理函数加入列表
void subject_attach(subject_t *sub, observer_func_t func)
{
    if (sub->count < MAX_OBSERVERS) {
        sub->observers[sub->count++] = func;
    }
}

// 通知:遍历列表,逐个调用
void subject_notify(subject_t *sub, int value)
{
    for (int i = 0; i < sub->count; i++) {
        sub->observers[i](value);
    }
}

上述代码实现了观察者模式的核心框架:

结构体成员说明:

成员类型说明
observersobserver_func_t[8]观察者函数指针数组,最多存储 8 个回调
countint当前已注册的观察者数量

函数说明:

subject_init - 初始化被观察对象,清空观察者列表。

subject_attach - 添加观察者。if (sub->count < MAX_OBSERVERS) 防止数组越界。

subject_notify - 通知所有观察者。遍历数组,逐个调用回调函数。

驱动层集成

c
// 片段:驱动层使用观察者模式
static subject_t temp_subject;

void drv_temp_init(void)
{
    subject_init(&temp_subject);
}

// 提供给应用层的订阅接口
void drv_temp_subscribe(observer_func_t func)
{
    subject_attach(&temp_subject, func);
}

void drv_temp_isr(void)
{
    int temp = read_sensor_hw();
    subject_notify(&temp_subject, temp);  // 通知所有订阅者
}

上述代码展示了驱动层如何集成观察者模式:

关键改动:

drv_temp_subscribe 替代原来的 drv_temp_register_cb,语义更清晰。

subject_notify 替代直接调用 g_cb,实现一对多通知。

应用层使用

c
// 片段:多个模块订阅同一事件
// 风扇模块
void fan_on_temp_changed(int temp) {
    fan_adjust(temp);
}

// 报警模块
void alarm_on_temp_changed(int temp) {
    if (temp > 80) alarm_trigger();
}

// 显示模块
void display_on_temp_changed(int temp) {
    lcd_show_temp(temp);
}

void app_init(void)
{
    drv_temp_subscribe(fan_on_temp_changed);
    drv_temp_subscribe(alarm_on_temp_changed);
    drv_temp_subscribe(display_on_temp_changed);
}

上述代码展示了多个模块如何订阅同一事件:

模块说明:

模块回调函数处理逻辑
风扇模块fan_on_temp_changed根据温度调整风扇转速
报警模块alarm_on_temp_changed温度超过 80°C 触发报警
显示模块display_on_temp_changed更新 LCD 显示

优势分析

观察者模式的优势非常明显:

优势说明
一对多通知一个事件源可以同时通知任意多个模块,新增模块只需 subscribe 一行代码
彻底解耦驱动层完全不知道谁订阅了自己,应用层各模块之间也互不干扰
扩展性强后面如果要加个"数据记录模块",只需要写个新函数然后 subscribe,其他代码一行不用动

注意事项

在嵌入式场景下也有需要注意的地方:

  • 订阅者数组大小是固定的(MAX_OBSERVERS),要根据实际需求设定
  • 通知的执行顺序就是订阅顺序,如果对顺序有要求需要额外处理
  • 同样要注意中断上下文的问题,必要时在通知函数中只做标记,把真正的处理放到任务里

三种方式横向对比

说了这么多,到底该用哪种?没有银弹,看场景。

方式耦合度实时性扩展性复杂度适用场景
轮询简单系统、模块少
回调一对一通知
观察者一对多通知

我在实际项目中的经验是:别一上来就用最复杂的方案。

如果你的系统只有两三个模块需要通信,回调就足够了。但如果你发现自己在不同地方写了好几个回调注册函数,而且它们监听的是同一个事件,那就是该考虑观察者模式的时候了。


再往前一步:消息队列与事件总线

其实如果你用的是 RTOS,还有一种更"正式"的方式——消息队列。底层把数据变化封装成一条消息丢进队列,应用层从队列里取消息处理。这样做的好处是天然支持异步处理,不用担心中断上下文的问题。

RTOS 消息队列示例

c
// 伪代码:RTOS消息队列方式
// 驱动层:发送消息
void drv_temp_isr(void)
{
    msg_t msg = { .type = MSG_TEMP, .value = read_sensor_hw() };
    queue_send(&app_queue, &msg);  // 丢进队列就完事
}

// 应用层:等待消息
void app_task(void *arg)
{
    msg_t msg;
    while (1) {
        queue_recv(&app_queue, &msg, WAIT_FOREVER);  // 阻塞等待
        switch (msg.type) {
            case MSG_TEMP:  fan_adjust(msg.value); break;
            case MSG_KEY:   handle_key(msg.value);  break;
            // ...
        }
    }
}

上述伪代码展示了 RTOS 消息队列的基本用法:

优势:

  • 异步处理:中断只负责发消息,不做重操作
  • 统一入口:所有事件通过同一个队列处理
  • 天然线程安全:RTOS 提供的队列机制自带同步

再进一步抽象,你甚至可以实现一个事件总线(Event Bus),所有模块都通过总线发布和订阅事件,彼此完全不知道对方的存在。不过这就超出今天的讨论范围了。


总结

回顾一下,从轮询到回调再到观察者,本质上是一个解耦的过程

方式核心思想解耦程度
轮询应用层主动拉取
回调底层主动推送
观察者订阅-通知机制

你会发现,这些方案背后其实都有经典的软件设计思想在支撑:

  • 观察者模式本身就是 GoF 23 种设计模式之一
  • 回调本质上是策略模式的一种简化形式
  • 消息队列则和中介者模式有异曲同工之妙

嵌入式开发不像互联网应用那样有成熟的框架帮你把设计模式"包好",很多时候你得自己动手搭。如果你能系统地掌握设计模式,很多看起来"棘手"的架构问题,其实前人早就给出了漂亮的解法。

本节要点

记住这三点:

  1. 从简单方案开始,按需升级——不要过度设计
  2. 解耦的核心是"谁依赖谁"——让变化的一方通知不变的一方
  3. 设计模式不是炫技,是解决实际问题的工具

更新日志

日期内容
2026-03-27初稿发布

参考资料

[1] 观察者模式. https://refactoringguru.cn/design-patterns/observer

相关主题

基于 VitePress 构建