摘自:一枚嵌入式码农
目录
- 一个真实的场景
- 方式一:轮询------最直觉的笨办法
- 方式二:回调函数------"别找我,有事我叫你"
- 方式三:观察者模式------一处变化,多方响应
- 三种方式横向对比
- 再往前一步:消息队列与事件总线
- 最后
做嵌入式开发这些年,有个问题几乎每个项目都会遇到:底层硬件状态变了,应用层怎么知道?
一个真实的场景
假设你在做一个温控系统。底层有个温度传感器驱动,每隔一段时间采集一次温度值。应用层需要根据温度来控制风扇转速------温度高了加速,温度低了减速。
问题来了:应用层怎么拿到最新的温度值?
你可能会说,这还不简单,直接读就行了。没错,但"怎么读"、"什么时候读",这里面的门道其实不少。处理得粗糙,代码能跑但难维护;处理得讲究,代码不仅清晰,后面改需求也不怕。
今天就来聊聊嵌入式开发中,应用层监听底层变化的几种常见做法,从最朴素的到比较优雅的,一步步来。
方式一:轮询------最直觉的笨办法
轮询(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 直接调用了 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); // 数据变了,通知应用层
}
}
c
/* ---------- 应用层 ---------- */
void on_temp_changed(int temp)
{
fan_adjust(temp); // 收到通知,直接处理
}
void app_init(void)
{
drv_temp_register_cb(on_temp_changed); // 注册回调
}
看一下回调方式的交互流程:

和轮询相比,回调有几个明显的好处:
- 实时性好------数据一变就通知,不用等轮询间隔。
- CPU不白忙------没有变化时,应用层可以安心做别的事。
- 耦合降低了一些------应用层不需要知道驱动内部怎么采集数据。
但回调也不是没毛病:
- 驱动层要"认识"应用层的函数签名------虽然用了函数指针解耦,但驱动层还是得定义回调类型,双方要约定好接口。
- 只能注册一个回调------上面的例子只存了一个 g_cb,如果风扇模块和报警模块都想监听温度变化呢?你得改成数组,管理起来就复杂了。
- 中断上下文问题------如果回调在中断里执行,处理逻辑不能太重,否则影响系统实时性。
回调函数在实际项目中用得非常多,尤其是驱动和中间件之间的交互。但当"一个事件需要通知多个模块"的需求出现时,简单的回调就显得力不从心了。
方式三:观察者模式------一处变化,多方响应
继续用快递的比喻:这次不是你一个人等快递,而是你、你室友、你女朋友都在等同一个包裹。快递员不可能一个个打电话,最好的办法是大家都"订阅"了物流通知,包裹一到,所有人同时收到短信。
这就是**观察者模式(Observer Pattern)**的核心思想:被观察的对象维护一个订阅者列表,状态变化时遍历列表逐个通知。
在嵌入式里,我们可以用C语言这样实现:
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);
}
}
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); // 通知所有订阅者
}
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);
}
来看看观察者模式下的交互流程:

观察者模式的优势非常明显:
- 一对多通知------一个事件源可以同时通知任意多个模块,新增模块只需 subscribe 一行代码。
- 彻底解耦------驱动层完全不知道谁订阅了自己,应用层各模块之间也互不干扰。
- 扩展性强------后面如果要加个"数据记录模块",只需要写个新函数然后 subscribe,其他代码一行不用动。
当然,在嵌入式场景下也有需要注意的地方:
- 订阅者数组大小是固定的(MAX_OBSERVERS),要根据实际需求设定。
- 通知的执行顺序就是订阅顺序,如果对顺序有要求需要额外处理。
- 同样要注意中断上下文的问题,必要时在通知函数中只做标记,把真正的处理放到任务里。
三种方式横向对比
说了这么多,到底该用哪种?没有银弹,看场景。

画个整体的架构对比图:

我在实际项目中的经验是:别一上来就用最复杂的方案。如果你的系统只有两三个模块需要通信,回调就足够了。但如果你发现自己在不同地方写了好几个回调注册函数,而且它们监听的是同一个事件,那就是该考虑观察者模式的时候了。
再往前一步:消息队列与事件总线
其实如果你用的是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;
// ...
}
}
}
再进一步抽象,你甚至可以实现一个事件总线(Event Bus),所有模块都通过总线发布和订阅事件,彼此完全不知道对方的存在。不过这就超出今天的讨论范围了。
最后
回顾一下,从轮询到回调再到观察者,本质上是一个解耦的过程:
- 轮询------应用层主动拉取,简单直接,但耦合度高。
- 回调------底层主动推送,实时性好,但扩展性有限。
- 观察者------订阅-通知机制,解耦彻底,支持一对多。
你会发现,这些方案背后其实都有经典的软件设计思想在支撑。观察者模式本身就是 GoF 23种设计模式之一,而回调本质上是策略模式的一种简化形式,消息队列则和中介者模式有异曲同工之妙。
嵌入式开发不像互联网应用那样有成熟的框架帮你把设计模式"包好",很多时候你得自己动手搭。如果你能系统地掌握设计模式,很多看起来"棘手"的架构问题,其实前人早就给出了漂亮的解法。