回调函数与模块解耦:函数指针作为接口

各位同学,今天我们来聊一个嵌入式开发中绕不开的话题——回调函数。说实话,我刚入行那会儿,对回调函数也是一知半解。直到有一次,我在做一个多传感器采集项目,需要同时处理温度、湿度、气压三种数据,每种传感器的处理逻辑完全不同。如果硬编码,代码会变成一团乱麻。嗯,就是那次经历,让我真正理解了回调函数的价值。

为什么需要回调函数?

先问大家一个问题:你写了一个定时器模块,希望定时时间到了之后,执行不同的操作——有时候是点亮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);
    }
}
注意: 回调函数可能在中断上下文中被调用。如果你的回调函数里做了耗时操作(比如打印、动态内存分配),可能会引发严重问题。我曾经在中断回调里调用了printf,结果系统直接卡死。后来我改用标志位+主循环处理的方式才解决。

实战:实现一个事件驱动模块

好了,理论讲完了,我们来动手实现一个完整的事件驱动模块。这个模块支持:

  • 最多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_register() 中断/轮询 事件回调链表 回调节点1 (优先级0) 回调节点2 (优先级1) ... 更多节点 事件发生 event_trigger(id) 回调执行 按优先级依次调用 应用层注册回调 → 硬件层触发事件 → 模块按优先级调用回调
避坑指南: 我曾经在回调函数里直接调用了 event_trigger,结果造成了递归调用,栈空间直接爆了。如果你需要在回调里触发其他事件,建议用消息队列做异步处理,或者加一个递归深度限制。

回调函数的常见应用场景

场景 说明 我的建议
定时器回调 定时时间到后执行用户函数 回调中不要做耗时操作
按键检测 按键按下/释放/长按触发不同处理 用状态机+回调组合
通信协议栈 收到数据包后调用上层处理函数 注意线程安全
传感器驱动 数据就绪后通知应用层 考虑DMA+回调模式

回调函数是C语言模块化编程的基石。它让模块之间的耦合度降到最低,每个模块只关心自己的职责。你想想看,如果没有回调,我们得写多少if-else和switch-case?代码会变得多难维护?

好了,这一章的内容就到这里。回调函数看似简单,但用好它需要一些实践。我建议你在自己的项目里试着封装一个事件驱动模块,哪怕只是控制几个LED,也能体会到回调带来的便利。

记住: 好的架构不是设计出来的,而是在不断重构中打磨出来的。回调函数就是你手中的一把利器,用好了,代码会变得优雅而灵活。

公众号:蓝海资料掘金营,微信deep3321