应用层如何监听底层变化?从轮询到观察者模式
要点速览
- 轮询是应用层主动拉取,简单直接但耦合度高
- 回调是底层主动推送,实时性好但扩展性有限
- 观察者模式是订阅-通知机制,解耦彻底,支持一对多
- 选择方案的原则:从简单开始,按需升级
如果你正在纠结"一个事件要通知多个模块"的问题,直接跳到观察者模式。
前置知识
阅读本文前,你需要了解:
- C 语言函数指针的基本用法
- 嵌入式开发中中断和主循环的概念
本文不假设你了解:
- 任何设计模式
- RTOS 相关知识
问题引入
做嵌入式开发,有个问题几乎每个项目都会遇到:底层硬件状态变了,应用层怎么知道?
一个真实的场景
假设你在做一个温控系统:
- 底层有个温度传感器驱动,每隔一段时间采集一次温度值
- 应用层需要根据温度来控制风扇转速——温度高了加速,温度低了减速
问题来了:应用层怎么拿到最新的温度值?
你可能会说,这还不简单,直接读就行了。没错,但"怎么读"、"什么时候读",这里面的门道其实不少。处理得粗糙,代码能跑但难维护;处理得讲究,代码不仅清晰,后面改需求也不怕。
今天就来聊聊嵌入式开发中,应用层监听底层变化的几种常见做法,从最朴素的到比较优雅的,一步步来。
方式一:轮询——最直觉的笨办法
轮询(Polling),说白了就是应用层主动、反复地去问底层:"数据变了没?变了没?"
就像你等快递,每隔五分钟就打开手机查一次物流。能不能拿到快递?能。累不累?累。
代码实现
// 片段:轮询方式的核心逻辑
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)的思路。
换成快递的例子:你不用一直刷手机了,快递到了快递员直接给你打电话。
驱动层实现
// 片段:驱动层的回调机制
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_cb | temp_callback_t | 全局回调函数指针,初始化为 NULL |
函数说明:
drv_temp_register_cb - 供应用层调用,注册回调函数。
drv_temp_isr - 驱动内部调用,通常在中断服务程序中执行。if (g_cb) 检查是关键,防止空指针调用导致崩溃。
应用层实现
// 片段:应用层注册回调
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)**的核心思想:被观察的对象维护一个订阅者列表,状态变化时遍历列表逐个通知。
观察者框架实现
// 片段:观察者模式的核心框架
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);
}
}上述代码实现了观察者模式的核心框架:
结构体成员说明:
| 成员 | 类型 | 说明 |
|---|---|---|
observers | observer_func_t[8] | 观察者函数指针数组,最多存储 8 个回调 |
count | int | 当前已注册的观察者数量 |
函数说明:
subject_init - 初始化被观察对象,清空观察者列表。
subject_attach - 添加观察者。if (sub->count < MAX_OBSERVERS) 防止数组越界。
subject_notify - 通知所有观察者。遍历数组,逐个调用回调函数。
驱动层集成
// 片段:驱动层使用观察者模式
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,实现一对多通知。
应用层使用
// 片段:多个模块订阅同一事件
// 风扇模块
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 消息队列示例
// 伪代码: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 种设计模式之一
- 回调本质上是策略模式的一种简化形式
- 消息队列则和中介者模式有异曲同工之妙
嵌入式开发不像互联网应用那样有成熟的框架帮你把设计模式"包好",很多时候你得自己动手搭。如果你能系统地掌握设计模式,很多看起来"棘手"的架构问题,其实前人早就给出了漂亮的解法。
本节要点
记住这三点:
- 从简单方案开始,按需升级——不要过度设计
- 解耦的核心是"谁依赖谁"——让变化的一方通知不变的一方
- 设计模式不是炫技,是解决实际问题的工具
更新日志
| 日期 | 内容 |
|---|---|
| 2026-03-27 | 初稿发布 |
参考资料
[1] 观察者模式. https://refactoringguru.cn/design-patterns/observer