回调函数与模块解耦:函数指针作为接口
各位同学,今天我们来聊一个嵌入式开发中绕不开的话题——回调函数。说实话,我刚入行那会儿,对回调函数也是一知半解。直到有一次,我在做一个多传感器采集项目,需要同时处理温度、湿度、气压三种数据,每种传感器的处理逻辑完全不同。如果硬编码,代码会变成一团乱麻。嗯,就是那次经历,让我真正理解了回调函数的价值。
为什么需要回调函数?
先问大家一个问题:你写了一个定时器模块,希望定时时间到了之后,执行不同的操作——有时候是点亮LED,有时候是发送数据,有时候是保存日志。你会怎么做?
最直接的办法:在定时器中断里写一个switch-case,根据某个标志位决定执行什么。但这样做的后果是什么?每次新增一个功能,你都得修改定时器模块的代码。这违反了开闭原则——对扩展开放,对修改关闭。
回调函数就是解决这个问题的。说白了,就是把「做什么」和「怎么做」分开。定时器模块只负责「定时到了就调用你给我的函数」,至于这个函数具体干什么,定时器模块不关心。
函数指针作为接口
在C语言里,函数指针就是函数的地址。你可以把它当作一个变量来传递。我个人习惯这样定义回调函数的类型:
// 定义回调函数类型
typedef void (*event_callback_t)(int event_id, void *user_data);
// 注册回调函数
void register_callback(event_callback_t cb, void *user_data);
// 触发回调
void trigger_event(int event_id);
你看,这里定义了一个函数指针类型 event_callback_t,它指向一个返回void、接受两个参数的函数。任何符合这个签名的函数,都可以被注册为回调。
void *user_data 参数,这样用户可以把任意数据传递给回调函数,灵活性会高很多。
回调函数的注册与触发
回调机制通常包含三个角色:
- 注册者:调用注册函数,把自己的回调函数告诉模块
- 模块:保存回调函数指针,在合适的时候调用它
- 触发者:触发事件,模块调用已注册的回调
我曾经在一个项目中,需要支持多个按键事件。每个按键按下、释放、长按,都要执行不同的操作。如果用if-else硬编码,代码会膨胀到无法维护。后来我用回调函数实现了事件驱动,每个按键只需要注册自己的处理函数,主循环只负责检测按键状态并触发事件。
// 事件驱动模块的核心结构
typedef struct {
event_callback_t callback;
void *user_data;
} event_handler_t;
static event_handler_t g_handlers[MAX_EVENTS];
// 注册回调
int event_register(int event_id, event_callback_t cb, void *user_data) {
if (event_id < 0 || event_id >= MAX_EVENTS) return -1;
g_handlers[event_id].callback = cb;
g_handlers[event_id].user_data = user_data;
return 0;
}
// 触发事件
void event_trigger(int event_id) {
if (g_handlers[event_id].callback) {
g_handlers[event_id].callback(event_id, g_handlers[event_id].user_data);
}
}
实战:实现一个事件驱动模块
好了,理论讲完了,我们来动手实现一个完整的事件驱动模块。这个模块支持:
- 最多32个事件ID
- 每个事件可以注册多个回调(链式调用)
- 支持优先级排序
先看头文件:
// event_driver.h
#ifndef EVENT_DRIVER_H
#define EVENT_DRIVER_H
#define MAX_EVENTS 32
#define MAX_CALLBACKS 8
typedef void (*event_callback_t)(int event_id, void *user_data);
typedef struct {
event_callback_t callback;
void *user_data;
int priority; // 优先级,数值越小优先级越高
} callback_node_t;
typedef struct {
callback_node_t callbacks[MAX_CALLBACKS];
int count;
} event_slot_t;
void event_driver_init(void);
int event_register(int event_id, event_callback_t cb, void *user_data, int priority);
void event_trigger(int event_id);
#endif
再看实现:
// event_driver.c
#include "event_driver.h"
#include <string.h>
static event_slot_t g_events[MAX_EVENTS];
void event_driver_init(void) {
memset(g_events, 0, sizeof(g_events));
}
int event_register(int event_id, event_callback_t cb, void *user_data, int priority) {
if (event_id < 0 || event_id >= MAX_EVENTS) return -1;
if (!cb) return -2;
event_slot_t *slot = &g_events[event_id];
if (slot->count >= MAX_CALLBACKS) return -3;
// 按优先级插入
int i = slot->count - 1;
while (i >= 0 && slot->callbacks[i].priority > priority) {
slot->callbacks[i + 1] = slot->callbacks[i];
i--;
}
slot->callbacks[i + 1].callback = cb;
slot->callbacks[i + 1].user_data = user_data;
slot->callbacks[i + 1].priority = priority;
slot->count++;
return 0;
}
void event_trigger(int event_id) {
if (event_id < 0 || event_id >= MAX_EVENTS) return;
event_slot_t *slot = &g_events[event_id];
for (int i = 0; i < slot->count; i++) {
if (slot->callbacks[i].callback) {
slot->callbacks[i].callback(event_id, slot->callbacks[i].user_data);
}
}
}
使用示例:
// main.c
#include "event_driver.h"
#include <stdio.h>
void on_button_press(int event_id, void *user_data) {
int *counter = (int *)user_data;
(*counter)++;
printf("Button pressed %d times\n", *counter);
}
void on_button_release(int event_id, void *user_data) {
printf("Button released\n");
}
int main() {
int press_count = 0;
event_driver_init();
event_register(1, on_button_press, &press_count, 0);
event_register(2, on_button_release, NULL, 0);
// 模拟按键事件
event_trigger(1); // 输出: Button pressed 1 times
event_trigger(1); // 输出: Button pressed 2 times
event_trigger(2); // 输出: Button released
return 0;
}
- 事件处理逻辑完全解耦,新增事件不需要修改模块代码
- 支持优先级,高优先级的回调先执行
- 通过
user_data传递上下文,灵活性强
事件驱动模块的核心逻辑
下面这张图展示了事件驱动模块的工作流程,我画出来帮助大家理解:
event_trigger,结果造成了递归调用,栈空间直接爆了。如果你需要在回调里触发其他事件,建议用消息队列做异步处理,或者加一个递归深度限制。
回调函数的常见应用场景
| 场景 | 说明 | 我的建议 |
|---|---|---|
| 定时器回调 | 定时时间到后执行用户函数 | 回调中不要做耗时操作 |
| 按键检测 | 按键按下/释放/长按触发不同处理 | 用状态机+回调组合 |
| 通信协议栈 | 收到数据包后调用上层处理函数 | 注意线程安全 |
| 传感器驱动 | 数据就绪后通知应用层 | 考虑DMA+回调模式 |
回调函数是C语言模块化编程的基石。它让模块之间的耦合度降到最低,每个模块只关心自己的职责。你想想看,如果没有回调,我们得写多少if-else和switch-case?代码会变得多难维护?
好了,这一章的内容就到这里。回调函数看似简单,但用好它需要一些实践。我建议你在自己的项目里试着封装一个事件驱动模块,哪怕只是控制几个LED,也能体会到回调带来的便利。
公众号:蓝海资料掘金营,微信deep3321